[译] Lenses:可组合函数式编程的 Getter 和 Setter(第十九部分)

注意:本篇是“组合软件”这本书 的一部分,它将以系列博客的形式展开新生。它涵盖了 JavaScript(ES6+)函数式编程和可组合软件技术的最基础的知识。 < 上一篇 | << 从第一部分开始javascript

lens 是一对可组合的 getter 和 setter 纯函数,它会关注对象内部的一个特殊字段,而且会听从一系列名为 lens 法则的公理。将对象视为总体,字段视为局部。getter 以对象总体做为参数,而后返回 lens 所关注的对象的一部分。前端

// view = whole => part
复制代码

setter 则以对象总体做为参数,以及一个须要设置的值,而后返回一个新的对象总体,这个对象的特定部分已经更新。和一个简单设置对象成员字段的值的函数不一样,Lens 的 setter 是纯函数:java

// set = whole => part => whole
复制代码

注意:在本篇中,咱们将在代码示例中使用一些原生的 lenses,这样是为了对整体概念有更深刻的了解。而对于生产环境下的代码,你则应该看看像 Ramda 这样的通过充分测试的库。不一样的 lens 库的 API 也不一样,比起本篇给出的例子,更有可能用可组合性更强、更优雅的方法来描述 lenses。android

假设你有一个元组数组(tuple array),表明了一个包含 xyz 三点的坐标:ios

[x, y, z]
复制代码

为了能分别获取或者设置每一个字段,你能够建立三个 lenses。每一个轴一个。你能够手动建立关注每一个字段的 getter:git

const getX = ([x]) => x;
const getY = ([x, y]) => y;
const getZ = ([x, y, z]) => z;

console.log(
  getZ([10, 10, 100]) // 100
);
复制代码

一样,相应的 setter 也许会像这样:github

const setY = ([x, _, z]) => y => ([x, y, z]);

console.log(
  setY([10, 10, 10])(999) // [10, 999, 10]
);
复制代码

为何选择 Lenses?

状态依赖是软件中耦合性的常见来源。不少组件会依赖于共享状态的结构,因此若是你须要改变状态的结构,你就必须修改不少处的逻辑。数据库

Lenses 让你可以把状态的结构抽象,让它隐藏在 getters 和 setter 以后。为代码引入 lens,而不是丢弃你的那些涉及深刻到特定对象结构的代码库的代码。若是后续你须要修改状态结构,你可使用 lens 来作,而且不须要修改任何依赖于 lens 的代码。编程

这遵循了需求的小变化将只须要系统的小变化的原则。后端

背景

在 1985 年,“Structure and Interpretation of Computer Programs” 描述了用于分离对象结构与使用对象的代码的方法的 getter 和 setter 对(下文中称为 putget)。文章描述了如何建立通用的选择器,它们访问复杂变量,但却不依赖变量的表示方式。这种分离特性很是有用,由于它打破了对状态结构的依赖。这些 getter 和 setter 对有点像这几十年来一直存在于关系数据库中的引用查询。

Lenses 把 getter 和 setter 对作得更加通用,更有可组合性,从而更加延伸了这个概念。在 Edward Kmett 发布了为 Haskell 写的 Lens 库后,它们更加普及。他是受到了推论出了遍历表达了迭代模式的 Jeremy Gibbons 和 Bruno C. d. S. Oliveira,Luke Palmer 的 “accessors”,Twan van Laarhoven 以及 Russell O’Connor 的影响。

注意:一个很容易犯的错误是,将函数式 lens 的现代观念和 Anamorphisms 等同,Anamorphisms 基于 Erik Meijer,Maarten Fokkinga 和 Ross Paterson 1991 年发表的 “使用 Bananas,Lenses,Envelopes 和 Barbed Wire 的函数式编程”。“函数意义上的术语 ‘lens’ 指的是它看起来是总体的一部分。在递归结构意义上的术语 ‘lens’ 指的是 [( and )],它在语法上看起来有些像凹透镜。太长,请不用读。它们之间并无任何关系。” ~ Edward Kmett on Stack Overflow

Lens 法则

lens 法则实际上是代数公理,它们确保 lens 能良好运行。

  1. view(lens, set(lens, a, store)) ≡ a — 若是你将一组值设置到一个 store 里,而且立刻经过 lens 看到了值,你将能获取到这个被设置的值。
  2. set(lens, b, set(lens, a, store)) ≡ set(lens, b, store) — 若是你为 a 设置了一个 lens 值,而后立刻为 b 设置 lens 值,那么和你只设置了 b 的值的结果是同样的。
  3. set(lens, view(lens, store), store) ≡ store — 若是你从 store 中获取 lens 值,而后立刻将这个值再设置回 store 里,这个值就等于没有修改过。

在咱们深刻代码示例以前,记住,若是你在生产环境中使用 lenses,你应该使用通过充分测试的 lens 库。在 JavaScript 语言中,我知道的最好的是 Ramda。目前,为了更好的学习,咱们先跳过这部分,本身写一些原生的 lenses。

// 纯函数 view 和 set,它们能够配合任何 lens 一块儿使用:
const view = (lens, store) => lens.view(store);
const set = (lens, value, store) => lens.set(value, store);

// 一个将 prop 做为参数,返回 naive 的函数
// 经过 lens 存取这个 prop。
const lensProp = prop => ({
  view: store => store[prop],
  // 这部分代码是原生的,它只能为对象服务:
  set: (value, store) => ({
    ...store,
    [prop]: value
  })
});

// 一个 store 对象的例子。一个可使用 lens 访问的对象
// 一般被称为 “store” 对象
const fooStore = {
  a: 'foo',
  b: 'bar'
};

const aLens = lensProp('a');
const bLens = lensProp('b');

// 使用`view()` 方法来解构 lens 中的属性 `a` 和 `b`。
const a = view(aLens, fooStore);
const b = view(bLens, fooStore);
console.log(a, b); // 'foo' 'bar'

// 使用 `aLens` 来设置 store 中的值:
const bazStore = set(aLens, 'baz', fooStore);

// 查看新设置的值。
console.log( view(aLens, bazStore) ); // 'baz'
复制代码

咱们来证明下这些函数的 lens 法则:

const store = fooStore;

{
  // `view(lens, set(lens, value, store))` = `value`
  // 若是你把某个值存入 store,
  // 而后立刻经过 lens 查看这个值,
  // 你将会获取那个你刚刚存入的值
  const lens = lensProp('a');
  const value = 'baz';

  const a = value;
  const b = view(lens, set(lens, value, store));

  console.log(a, b); // 'baz' 'baz'
}

{
  // set(lens, b, set(lens, a, store)) = set(lens, b, store)
  // 若是你将一个 lens 值存入了 `a` 而后立刻又存入 `b`,
  // 那么和你直接存入 `b` 是同样的
  const lens = lensProp('a');

  const a = 'bar';
  const b = 'baz';

  const r1 = set(lens, b, set(lens, a, store));
  const r2 = set(lens, b, store);
  
  console.log(r1, r2); // {a: "baz", b: "bar"} {a: "baz", b: "bar"}
}

{
  // `set(lens, view(lens, store), store)` = `store`
  // 若是你从 store 中获取到一个 lens 值,而后立刻把这个值
  // 存回到 store,那么这个值不变
  const lens = lensProp('a');

  const r1 = set(lens, view(lens, store), store);
  const r2 = store;
  
  console.log(r1, r2); // {a: "foo", b: "bar"} {a: "foo", b: "bar"}
}
复制代码

组合 Lenses

Lenses 是可组合的。当你组合 lenses 的时候,获得的结果将会深刻对象的字段,穿过全部对象中字段可能的组合路径。咱们将从 Ramda 引入功能全面的 lensProp 来作说明:

import { compose, lensProp, view } from 'ramda';

const lensProps = [
  'foo',
  'bar',
  1
];

const lenses = lensProps.map(lensProp);
const truth = compose(...lenses);

const obj = {
  foo: {
    bar: [false, true]
  }
};

console.log(
  view(truth, obj)
);
复制代码

棒极了,可是其实还有不少使用 lenses 的组合值得咱们注意。让咱们继续深刻。

Over

在任何仿函数数据类型的状况下,应用源自 a => b 的函数都是可能的。咱们已经论述了,这个仿函数映射是**可组合的。**相似的,咱们能够在 lens 中对关注的值应用某个函数。一般状况下,这个值是同类型的,也是一个源于 a => a 的函数。lens 映射的这个操做在 JavaScript 库中通常被称为 “over”。咱们能够像这样建立它:

// over = (lens, f: a => a, store) => store
const over = (lens, f, store) => set(lens, f(view(lens, store)), store);

const uppercase = x => x.toUpperCase();

console.log(
  over(aLens, uppercase, store) // { a: "FOO", b: "bar" }
);
复制代码

Setter 遵照了仿函数规则:

{ // 若是你经过 lens 映射特定函数
  // store 不变
  const id = x => x;
  const lens = aLens;
  const a = over(lens, id, store);
  const b = store;

  console.log(a, b);
}
复制代码

对于可组合的示例,咱们将使用一个 over 的 auto-curried 版本:

import { curry } from 'ramda';

const over = curry(
  (lens, f, store) => set(lens, f(view(lens, store)), store)
);
复制代码

很容易看出,over 操做下的 lenses 依旧遵循仿函数可组合规则:

{ // over(lens, f) after over(lens g)
  // 和 over(lens, compose(f, g)) 是同样的
  const lens = aLens;

  const store = {
    a: 20
  };

  const g = n => n + 1;
  const f = n => n * 2;

  const a = compose(
    over(lens, f),
    over(lens, g)
  );

  const b = over(lens, compose(f, g));

  console.log(
    a(store), // {a: 42}
    b(store)  // {a: 42}
  );
}
复制代码

咱们目前只基本了解了 lenses 的的皮毛,可是对于你继续开始学习已经足够了。若是想获取更多细节,Edward Kmett 在这个话题讨论了不少,不少人也写了许多深度的探索。


Eric Elliott“编写 JavaScript 应用”(O’Reilly)以及“跟着 Eric Elliott 学 Javascript” 两书的做者。他为许多公司和组织做过贡献,例如 Adobe SystemsZumba FitnessThe Wall Street JournalESPNBBC 等,也是不少机构的顶级艺术家,包括但不限于 UsherFrank Ocean 以及 Metallica

大多数时间,他都在 San Francisco Bay Area,同这世上最美丽的女子在一块儿。

感谢 JS_Cheerleader

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索