函数式引用 lenses

猪小花1号2018-08-29 11:15

作者:李权威

对于精准的数据操作,从来没有一种方法像lens这样易用。例如对于immutable的数据修改。

const data = {
           some: {
               of: {
                   property: 1
               }
           }
 };
在Redux中我们如果修改data.some.of.property的值需要写成下面的形式
const newData = {
    ...data,
    some: {
        ...data.some,
        of: {
            ...data.some.of,
            property: newValue
        }
    }
};
当然在设计数据模形时一般不会设计成如此的结构,但对于举例子来说足够了。从上面可以看到修改一个深层次属性是多么的麻烦,而且这样的代码也是建立在es6提供的解构操作符下写出的。
像这种我们的关注点聚焦在某个属性上,而不关心其他数据的行为我们称之为透镜操作(lens),例如使用lens可以写成这样。
const R = require('ramda');

const data = {
    some: {
        of: {
            otherProperty: 2,
            property: 1
        }
    }
};

const propertyLens = R.lensPath(['some', 'of', 'property']);

console.log(R.set(propertyLens, 3, data))
输出
{ some: { of: { otherProperty: 2, property: 3 } } }
此时我们拿到的propertyLens就是一个some.of.property的透镜,也称之为函数式引用。就像普通的数据引用一样支持原地操作。我们对普通引用类型的操作:读值,写值,操作值对应在函数式引用中的操作为: view, set, over.
- 构建一个lens
const propertyLens =  R.lensPath(['some', 'of', 'property']);

const path = ['some', 'of', 'otherProperty'];
const getter = R.path(path);
const setter = R.assocPath(path);
const otherPropertyLens = R.lens(getter, setter);
可以看到lens实际上是一个getter,setter的封装。当我们使用`view(otherPropertyLens, data)`时实际上触发的是 getter(data). 写值同理.
- 读取操作(R)
读取操作比较简单,拿到该引用指向的值做为返回值
R.view(otherProperty, data);  // return 3
- 写值操作(W)
写入也很简单,直接赋值给该引用. 不同的是返回值是修改后的data的新对象.
R.set(otherProperty, newValue, data);
例如
const R = require('ramda');

const data = {
    a: 1,
    b: 2
};

const aLens = R.lensProp('a');

const newData = R.set(aLens, 3, data);

console.log(data, newData);
输出
 { a: 1, b: 2 } { a: 3, b: 2 }
- 变换操作(RW)
传统引用支持的原地变换(transform)操作,例如 ++i 就是先读取当前值,增加1后写入 对于lens也是同样支持.
const transformer = addOne;
R.over(otherProperty, transformer, data);
这里传入的transformer接收一个原来的旧值,返回一个新值进行写入. R.over的返回值类似R.set, 一个写入后的data对象。
- 际应用
lens因为是FP中的一个基础数据类型,所以对于React,Redux有着天然的优势. 这里举出一个利用lens进行简洁的Reducer操作。对于存在大量的CRUD情况下可以写出非常清晰的逻辑.
(store/userReducer.js)
import Actions from './actions';
import R from 'ramda';

const UserListLens = R.propLens('list');

const initialState = {
    list: [],
};
 
function userReducer(state, action) {
    switch(action.type) {
      case Actions.SET_USER:
          return R.set(UserListLens, action.value, state);

      case Actions.ADD_USER:
          return R.over(UserListLens, R.append(action.value), state);

      case Actions.DELETE_USER:
          return R.over(UserListLens, R.without(action.value), state);

      default:
        return state;
    }
}
js正在享受着来自传统编程世界的遗产,lens也是其中一个,支持的工具库也有很多,Ramda提供了基本的支持。但lodash/underscore因为理念不同所以无法支持.
另外针对vue这种不提倡immutable的框架,我写了一个针对vue的lens:  https://github.com/Qquanwei/vuex-lens
inspired by: https://medium.com/netscape/seeing-through-redux-with-lenses-112ecb250f4



网易云新用户大礼包:https://www.163yun.com/gift

本文来自网易实践者社区,经作者李权威授权发布。