9

Monocle 是一个很棒的库(而不是唯一一个),它实现了镜头模式,如果我们必须在巨大的嵌套对象中更改一个字段,那就太好了。就像在例子http://julien-truffaut.github.io/Monocle/

case class Street(number: Int, name: String)
case class Address(city: String, street: Street)
case class Company(name: String, address: Address)
case class Employee(name: String, company: Company)

以下样板

employee.copy(
  company = employee.company.copy(
    address = employee.company.address.copy(
      street = employee.company.address.street.copy(
        name = employee.company.address.street.name.capitalize // luckily capitalize exists
      )
    )
  )
)

可以很容易地替换为

import monocle.macros.syntax.lens._

employee
  .lens(_.company.address.street.name)
  .composeOptional(headOption)
  .modify(_.toUpper)

这是伟大的。据我了解,宏魔术将所有内容完全转换为与上述相同的代码。

但是,如果我想组合几个动作怎么办?如果我想一键更改街道名称、地址城市和公司名称怎么办?如下所示:

employee.copy(
  company = employee.company.copy(
    address = employee.company.address.copy(
      street = employee.company.address.street.copy(
        name = employee.company.address.street.name.capitalize // luckily capitalize exists
      ),
      city = employee.company.address.city.capitalize
    ),
    name = employee.company.name.capitalize
  )
)

如果我只是在这里重用镜头,我将有以下代码:

employee
  .lens(_.company.address.street.name).composeOptional(headOption).modify(_.toUpper)
  .lens(_.company.address.city).composeOptional(headOption).modify(_.toUpper)
  .lens(_.company.name).composeOptional(headOption).modify(_.toUpper)

最终将被翻译成三个 employee.copy(...).copy(...).copy(...)调用,而不仅仅是一个 employee.copy(...)。如何让它变得更好?

此外,应用一系列操作真的很棒。就像成对序列一样Seq[(Lens[Employee, String], String => String)],第一个元素是指向正确视场的透镜,第二个是修改它的函数。这将有助于从外部构建这样的操作序列。对于上面的例子:

val operations = Seq(
  GenLens[Employee](_.company.address.street.name) -> {s: String => s.capitalize},
  GenLens[Employee](_.company.address.city) -> {s: String => s.capitalize},
  GenLens[Employee](_.company.name) -> {s: String => s.capitalize}
)

或类似的东西...

4

1 回答 1

11

据我了解,宏魔术将所有内容完全转换为与上述相同的代码。

它没有。

这个简单的代码:

employee.lens(_.name)
  .modify(_.capitalize)

变成类似于那个怪物的东西*:

monocle.syntax.ApplyLens(employee,
    new monocle.PLens[Employee, Employee, String, String] {
      def get(e: Employee): String = e.name;
      def set(s: String): Employee => Employee = _.copy(s);
      def modify(f: String => String): Employee => Employee = e => e.copy(f(e.name))
    }
}).modify(_.capitalize)

这远非简单

employee.copy(name = employee.name.capitalize)

并包括三个冗余对象(匿名镜头类、语法糖的 ApplyLens 和从 中返回的 lambda modify)。我们跳过了更多capitalize直接使用而不是与组合headOption

所以不,没有免费的晚餐。然而,大多数时候,它已经足够好了,没有人关心额外的镜头对象和中间结果。


多重操作

如果它们的类型对齐(这里是Employeeto String),您可以从多个镜头构建一个 Traversal(收集镜头)

val capitalizeAllFields = Traversal.applyN(
  GenLens[Employee](_.name),
  GenLens[Employee](_.company.address.street.name),
  GenLens[Employee](_.company.address.city),
  GenLens[Employee](_.company.name)
).modify(_.capitalize)

这仍然会调用copy多次。为了提高效率,您可以使用Traversal.apply4et al。品种,这将需要您copy手动编写(我现在懒得这样做)。

最后,如果你想对不同类型的字段应用各种转换,你应该使用这个事实modifyset返回一个 type 的函数Employee => Employee。对于您的示例,这将是:

val operations = Seq(
  GenLens[Employee](_.company.address.street.name).modify(_.capitalize),
  GenLens[Employee](_.company.address.street.number).modify(_ + 42),
  GenLens[Employee](_.company.name).set("No Company Inc.")
)

val modifyAll = Function.chain(operations)

// does all above operations of course, with two extra copy calls
modifyAll(employee) 

desugar* - 这是Ammonite-REPL 中的简化输出。我跳过了modifyF,顺便说一句

于 2017-08-16T14:03:01.873 回答