Как найти и изменить поле во вложенных классах?
Определены некоторые вложенные классы дел с 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")))))
)))
Я не уверен, как это реализовать элегантно. Я пробовал с моноклем, но это не обеспечивает 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
)
3 ответа
Я только что расширил 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)
}
Мы можем реализовать 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
Тем не менее, использование select
в workspaceToVersions
полностью действителен, потому что мы фильтруем другое поле, которое мы модифицируем. Таким образом, мы не можем аннулировать предикат.
Вы можете использовать Монокль 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)
И вы сделали. На самом деле это не так кратко, как ваша реализация, но она состоит из более хорошо сочетаемых частей.