组合镜头系列

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

或类似的东西...

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和由modify返回的lambda)。我们直接使用capitalize而不是与headOption组合,省略了更多内容。

因此,没有免费的午餐。然而,在大多数情况下,它已经足够好了,而且没有人在意额外的镜头对象和中间结果。


多个操作

如果它们的类型对齐(这里是从EmployeeString),您可以从多个镜头构建一个 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.apply4等各种变体,这将要求您手动编写copy(我现在太懒了)。

最后,如果您想对不同类型的字段应用各种转换,则应使用modifyset返回类型为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) 

* - 这是Ammonite-REPL中desugar的简化输出。我省略了modifyF,顺便说一下。



网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接