如何在嵌套大小写 类 中查找和修改字段?
How to find and modify field in nested case classes?
用 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")))))
))
现在我想写这样一个方法,在doc
中添加一个新的version
:
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,但它不提供 filter
或 find
。我尴尬的解决方案是:
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
)
您可以使用 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)
大功告成。这实际上并不比您的实现更简洁,但它由更漂亮的可组合部分组成。
我们可以很好地实现 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
法律规定对于所有 t
, t.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
的用法是完全有效的,因为我们过滤了我们修改的不同字段。所以我们不能使谓词无效。
我刚刚用 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)
}
用 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")))))
))
现在我想写这样一个方法,在doc
中添加一个新的version
:
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,但它不提供 filter
或 find
。我尴尬的解决方案是:
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
)
您可以使用 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)
大功告成。这实际上并不比您的实现更简洁,但它由更漂亮的可组合部分组成。
我们可以很好地实现 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
法律规定对于所有 t
, t.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
的用法是完全有效的,因为我们过滤了我们修改的不同字段。所以我们不能使谓词无效。
我刚刚用 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)
}