2015-10-28 18 views
8

Definito alcune classi case annidate con List campi:Come trovare e modificare il campo in classi case nidificate?

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

ed un campione 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"))))) 
)) 

Ora voglio scrivere un tale metodo, che aggiungono un nuovo version ad un doc:

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

Sarò utilizzato come segue:

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

Non sono sicuro di come implementarlo in modo elegante. Ho provato con monocle, ma non fornisce filter o find. La mia soluzione imbarazzante è:

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

Esiste una soluzione migliore? (È possibile utilizzare qualsiasi libreria, non solo monocle)

risposta

7

Ho appena esteso Quicklens con il metodo eachWhere per gestire un tale scenario, questo particolare metodo sarebbe simile a questa:

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

È possibile utilizzare il tipo Index di Monocle per rendere la soluzione più pulita e più generica.

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) 

Questo dice: io so come cercare un progetto in uno spazio di lavoro utilizzando una stringa (il nome), e un documento in un progetto da una stringa (il percorso). Potresti anche inserire istanze Index come Index[List[Project], String, Project], ma dal momento che non possiedi il numero List probabilmente non è l'ideale.

successivo è possibile definire un Optional che unisce le due ricerche:

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

E poi il metodo:

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

E il gioco è fatto. Questo non è molto più conciso della tua implementazione, ma è composto da pezzi più piacevolmente componibili.

6

possiamo implementare addNewVersion con ottica abbastanza bene, ma c'è un 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) 

questo funzionerà, ma potreste aver notato l'uso di selectPrism che non è fornito da Monocle. Questo perché select non soddisfa le leggi Traversal che dichiarano che per tutto t, t.modify(f) compose t.modify(g) == t.modify(f compose g).

Un esempio contatore è:

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

Tuttavia, l'utilizzo di select in workspaceToVersions è completamente valida perché filtro su un campo diverso che modifichiamo. Quindi non possiamo invalidare il predicato.

Problemi correlati