2015-10-28 5 views
8

einige verschachtelte Fall Klassen definiert mit List Feldern:Wie finden und ändern Sie das Feld in geschachtelten Fallklassen?

@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]) 

und eine Probe 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"))))) 
)) 

Jetzt möchte ich, ein solches Verfahren schreiben, die eine neue version zu einem doc hinzu:

def addNewVersion(workspace: Workspace, projectName: String, docPath: String, version: Version): Workspace = { 
    ??? 
} 

Ich werde wie folgt verwendet:

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

Ich bin nicht sicher, wie man es auf eine elegante Art und Weise implementiert. Ich versuchte mit monocle, aber es stellt filter oder find nicht zur Verfügung. Meine peinliche Lösung ist:

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

Gibt es eine bessere Lösung? (Kann alle Bibliotheken verwenden, nicht nur monocle)

Antwort

7

I verlängert nur Quicklens mit dem eachWhere Methode, ein solches Szenario zu handhaben, diese spezielle Methode würde wie folgt aussehen:

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

Sie können Monocles Index Typ verwenden, um Ihre Lösung sauberer und generischer zu machen.

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) 

Dies sagt: Ich weiß, wie man ein Projekt in einem Arbeitsbereich suchen einen String (der Namen) verwendet wird, und ein Dokument in einem Projekt mit einer Schnur (der Weg). Sie könnten auch Index Instanzen wie Index[List[Project], String, Project] setzen, aber da Sie nicht besitzen List das ist wohl nicht ideal.

Als nächstes können Sie eine Optional definieren, die die zwei Lookups kombiniert:

def docLens(projectName: String, docPath: String): Optional[Workspace, Doc] = 
    index[Workspace, String, Project](projectName).composeOptional(index(docPath)) 

Und dann Ihre Methode:

def addNewVersion(
    workspace: Workspace, 
    projectName: String, 
    docPath: String, 
    version: Version 
): Workspace = 
    docLens(projectName, docPath).modify(doc => 
    doc.copy(versions = doc.versions :+ version) 
)(workspace) 

Und du bist fertig. Dies ist nicht wirklich prägnanter als Ihre Implementierung, aber es besteht aus besser zusammensetzbaren Stücken.

6

Wir addNewVersion mit Optik ganz gut umsetzen können, aber es gibt eine 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) 

Dies funktioniert aber Sie könnten die Verwendung von selectPrism bemerkt haben, die nicht von Monocle vorgesehen ist. Dies liegt daran, select erfüllt nicht Traversal Gesetze, die für alle t, t.modify(f) compose t.modify(g) == t.modify(f compose g).

Ein Gegenbeispiel ist:

val negative: Prism[Int, Int] = select[Int](_ < 0) 
(negative.modify(_ + 1) compose negative.modify(_ - 1))(-1) == 0 

die Verwendung von select in workspaceToVersions jedoch vollständig gültig ist, weil wir auf einem anderen Feld filtern, die wir ändern. Daher können wir das Prädikat nicht ungültig machen.

Verwandte Themen