如何在嵌套的case类中找到和修改字段?

9

定义了一些带有List字段的嵌套案例类:

@Lenses("_") case class Version(version: Int, content: String)
@Lenses("_") case class Doc(path: String, versions: List[Version])
@Lenses("_") case class Project(name: String, docs: List[Doc])
@Lenses("_") case class Workspace(projects: List[Project])

这是一个示例工作空间

val workspace = Workspace(List(
  Project("scala", List(
    Doc("src/a.scala", List(Version(1, "a11"), Version(2, "a22"))),
    Doc("src/b.scala", List(Version(1, "b11"), Version(2, "b22"))))),
  Project("java", List(
    Doc("src/a.java", List(Version(1, "a11"), Version(2, "a22"))),
    Doc("src/b.java", List(Version(1, "b11"), Version(2, "b22"))))),
  Project("javascript", List(
    Doc("src/a.js", List(Version(1, "a11"), Version(2, "a22"))),
    Doc("src/b.js", List(Version(1, "b11"), Version(2, "b22")))))
))

现在我想写一个方法,将新的版本添加到文档中:

def addNewVersion(workspace: Workspace, projectName: String, docPath: String, version: Version): Workspace = {
  ???
}

我将被用于以下方面:

  val newWorkspace = addNewVersion(workspace, "scala", "src/b.scala", Version(3, "b33"))

  println(newWorkspace == Workspace(List(
    Project("scala", List(
      Doc("src/a.scala", List(Version(1, "a11"), Version(2, "a22"))),
      Doc("src/b.scala", List(Version(1, "b11"), Version(2, "b22"), Version(3, "b33"))))),
    Project("java", List(
      Doc("src/a.java", List(Version(1, "a11"), Version(2, "a22"))),
      Doc("src/b.java", List(Version(1, "b11"), Version(2, "b22"))))),
    Project("javascript", List(
      Doc("src/a.js", List(Version(1, "a11"), Version(2, "a22"))),
      Doc("src/b.js", List(Version(1, "b11"), Version(2, "b22")))))
  )))

我不确定如何以优雅的方式实现它。我尝试使用Monocle,但它不提供filterfind。我的笨拙解决方案是:

def addNewVersion(workspace: Workspace, projectName: String, docPath: String, version: Version): Workspace = {
  (_projects composeTraversal each).modify(project => {
    if (project.name == projectName) {
      (_docs composeTraversal each).modify(doc => {
        if (doc.path == docPath) {
          _versions.modify(_ ::: List(version))(doc)
        } else doc
      })(project)
    } else project
  })(workspace)
}

有更好的解决方案吗?(可以使用任何库,不仅限于monocle

3个回答

9

我刚刚在Quicklens中添加了eachWhere方法来处理这种情况,这个特定的方法看起来像这样:

import com.softwaremill.quicklens._

def addNewVersion(workspace: Workspace, projectName: String, docPath: String, version: Version): Workspace = {
  workspace
    .modify(_.projects.eachWhere(_.name == projectName)
             .docs.eachWhere(_.path == docPath).versions)
    .using(vs => version :: vs)
}

6
我们可以使用"optics"非常好地实现"addNewVersion"功能,但是有一个需要注意的问题:
import monocle._
import monocle.macros.Lenses
import monocle.function._
import monocle.std.list._ 
import Workspace._, Project._, Doc._

def select[S](p: S => Boolean): Prism[S, S] =
   Prism[S, S](s => if(p(s)) Some(s) else None)(identity)

 def workspaceToVersions(projectName: String, docPath: String): Traversal[Workspace, List[Version]] =
  _projects composeTraversal each composePrism select(_.name == projectName) composeLens
    _docs composeTraversal each composePrism select(_.path == docPath) composeLens
    _versions

def addNewVersion(workspace: Workspace, projectName: String, docPath: String, version: Version): Workspace =
  workspaceToVersions(projectName, docPath).modify(_ :+ version)(workspace)

这将起作用,但您可能已经注意到使用了 select Prism,而它不是由Monocle提供的。这是因为 select 不满足 Traversal 法则,该法则规定对于所有 tt.modify(f) compose t.modify(g) == t.modify(f compose g)
一个反例是:
val negative: Prism[Int, Int] = select[Int](_ < 0)
(negative.modify(_ + 1) compose negative.modify(_ - 1))(-1) == 0

然而,在 workspaceToVersions 中使用 select 是完全有效的,因为我们过滤了一个不同的字段,而且我们修改了该字段。因此,我们不能使谓词失效。


5
您可以使用 Monocle 的 Index 类型来使您的解决方案更加简洁和通用。
import monocle._, monocle.function.Index, monocle.function.all.index

def indexListBy[A, B, I](l: Lens[A, List[B]])(f: B => I): Index[A, I, B] =
  new Index[A, I, B] {
    def index(i: I): Optional[A, B] = l.composeOptional(
      Optional((_: List[B]).find(a => f(a) == i))(newA => as =>
        as.map {
          case a if f(a) == i => newA
          case a => a
        }
      )
    )
  }

implicit val projectNameIndex: Index[Workspace, String, Project] =
  indexListBy(Workspace._projects)(_.name)

implicit val docPathIndex: Index[Project, String, Doc] =
  indexListBy(Project._docs)(_.path)

这段话说:我知道如何使用字符串(名称)在工作区中查找项目以及使用字符串(路径)在项目中查找文档。您还可以像 Index[List[Project], String, Project] 一样放置 Index 实例,但由于您不拥有 List,因此这可能不是最佳选择。
接下来,您可以定义一个Optional 来组合这两个查找操作:
def docLens(projectName: String, docPath: String): Optional[Workspace, Doc] =
  index[Workspace, String, Project](projectName).composeOptional(index(docPath))

然后是你的方法:

def addNewVersion(
  workspace: Workspace,
  projectName: String,
  docPath: String,
  version: Version
): Workspace =
  docLens(projectName, docPath).modify(doc =>
    doc.copy(versions = doc.versions :+ version)
  )(workspace)

完成了。这个实现并没有比你的更简洁,但它是由更好组合的部分组成的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接