【问题标题】:How to find and modify field in nested case classes?如何在嵌套案例类中查找和修改字段?
【发布时间】:2016-01-28 03:55:21
【问题描述】:

定义了一些带有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])

还有一个示例workspace

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")))))
))

现在我想写一个这样的方法,将新的version 添加到doc

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

【问题讨论】:

    标签: scala lenses monocle-scala


    【解决方案1】:

    我只是用eachWhere 方法扩展Quicklens 来处理这种情况,这个特殊的方法看起来像这样:

    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)
    }
    

    【讨论】:

      【解决方案2】:

      我们可以很好地使用光学实现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
      

      但是,selectworkspaceToVersions 中的使用是完全有效的,因为我们过滤了我们修改的不同字段。所以我们不能使谓词无效。

      【讨论】:

        【解决方案3】:

        您可以使用 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 实例放在Index[List[Project], String, Project] 之类的实例中,但由于您不拥有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)
        

        你就完成了。这并不比您的实现更简洁,但它由更易于组合的部分组成。

        【讨论】:

          猜你喜欢
          • 2016-01-20
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2017-01-07
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多