2015-10-28 34 views
8

Zdefiniowane kilka zagnieżdżonych klas przypadków z List dziedzinach:Jak znaleźć i zmodyfikować pole w zagnieżdżonych klasach spraw?

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

i próbki 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"))))) 
)) 

Teraz chcę napisać taką metodę, która dodać nowy version do doc:

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

Będę używany w następujący sposób:

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

Nie jestem pewien, jak go wdrożyć w elegancki sposób. Próbowałem z monocle, ale nie zapewnia ona filter ani find. Moje niezręczne rozwiązanie to:

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

Czy istnieje lepsze rozwiązanie? (Można używać dowolnych bibliotek, nie tylko monocle)

Odpowiedz

7

Właśnie rozszerzony Quicklens metodą eachWhere do obsługi takiego scenariusza, to szczególna metoda będzie wyglądać następująco:

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

Możesz użyć typu Monocle o numerze Index, aby Twoje rozwiązanie było czystsze i bardziej ogólne.

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) 

Ten mówi: wiem, jak spojrzeć na projekt w obszarze roboczym używając ciąg (nazwa) i doc w projekcie przez strunowy (ścieżka). Można również wstawić instancje takie jak Index, takie jak Index[List[Project], String, Project], ale ponieważ nie należysz do użytkownika List, prawdopodobnie nie jest to idealne rozwiązanie.

Następnie można zdefiniować Optional który łączy dwa wyszukiwań:

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

I wtedy metoda:

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

I gotowe. To nie jest tak naprawdę bardziej zwięzłe, niż twoja implementacja, ale składa się z ładniejszych kompozycyjnych elementów.

6

Możemy realizować addNewVersion z optyką całkiem ładnie, ale jest haczyka:

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) 

to będzie działać, ale można zauważyć wykorzystanie selectPrism który nie jest dostarczany przez Monocle. Wynika to z faktu, że select nie spełnia Traversal praw określających, że dla wszystkich t, t.modify(f) compose t.modify(g) == t.modify(f compose g).

przykładu licznik jest:

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

Jednakże użycie select w workspaceToVersions jest całkowicie uzasadnione, ponieważ filtr w innej dziedzinie, że modyfikacja. Nie możemy unieważnić predykatu.