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.