How to find and modify field in nested case classes?
I just extended Quicklens with the eachWhere
method to handle such a scenario, this particular method would look like this:
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)
}
We can implement addNewVersion
with optics quite nicely but there is a gotcha:
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)
This will work but you might have noticed the use of select
Prism
which is not provided by Monocle. This is because select
does not satisfy Traversal
laws that state that for all t
, t.modify(f) compose t.modify(g) == t.modify(f compose g)
.
A counter example is:
val negative: Prism[Int, Int] = select[Int](_ < 0)
(negative.modify(_ + 1) compose negative.modify(_ - 1))(-1) == 0
However, the usage of select
in workspaceToVersions
is completely valid because we filter on a different field that we modify. So we cannot invalidate the predicate.