2015-10-28 7 views
8

Определено несколько вложенных классов регистра с List полями:Как найти и изменить поле во вложенных классах классов?

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

и образец 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"))))) 
)) 

Теперь я хочу написать такой метод, который добавить новый version к doc:

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

Я буду использовать следующим образом:

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

Я не уверен, как реализовать его в элегантном стиле. Я пробовал с monocle, но он не дает filter или find. Мой неудобный раствор:

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

Есть ли лучшее решение? (Можно использовать любые библиотеки, а не только monocle)

ответ

7

Я просто продлил Quicklens с методом eachWhere для обработки такого сценария, данный метод будет выглядеть следующим образом:

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

Вы можете использовать тип Monocle Index, чтобы сделать ваше решение более чистым и более универсальным.

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) 

Это говорит: Я знаю, как смотреть на проект в рабочей области, используя строку (имя), и документ в проекте по строке (путь). Вы также можете поставить Index экземпляров, таких как Index[List[Project], String, Project], но так как вы не являетесь владельцем List, это, возможно, не идеально.

Далее вы можете определить Optional, который объединяет два Lookups:

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

И тогда ваш метод:

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

И вы сделали. Это не более красноречиво, чем ваша реализация, но она состоит из более приятных композиционных произведений.

6

Мы можем реализовать addNewVersion с оптикой довольно хорошо, но есть глюк:

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) 

Это будет работать, но вы могли заметить, использование selectPrism, который не предусмотрен Monocle. Это связано с тем, что select не удовлетворяет законам Traversal, которые утверждают, что для всех t, t.modify(f) compose t.modify(g) == t.modify(f compose g).

Счетчик пример:

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

Тем не менее, использование select в workspaceToVersions полностью справедливо, потому что мы фильтровать на другом поле, что мы изменить. Поэтому мы не можем аннулировать предикат.

 Смежные вопросы

  • Нет связанных вопросов^_^