Ramda.js 的无点函数组合

Point-free Function Composition with Ramda.js

我正在使用 Ramda.js 作为选择器函数,以访问 Redux 存储中的数据。我想要的是将我的选择器定义为不引用选择器所作用的 state 的函数,例如:

const getUserName = path(['user', 'name']);

const name = getUserName({
  user: {
    name: 'Some Name'
  }
});

这对于简单的选择器来说很容易,但对于组合的选择器来说有时会成为一个问题。

这是一个示例,其中一些 items 需要解析,由对象上的 id 引用:

const getItemById = id => state => path(['items', id], state);

const getConnectedItemIds = obj => path(['items'], obj);

const getItemsFromObj = obj => state => {
    const ids = getConnectedItemIds(obj);
    return ids.map(id => getItemById(id)(state));
};

第一个函数不用参考state就可以很容易地表达出来,而第二个函数不用obj,我相信这就是所谓的无点风格。但是没有state怎么写第三个函数呢?

我正在寻找如何使用 Ramda 重写第三个函数,还有关于此的规则和程序,例如(不知道它是否正确):

所有组合函数都需要将 state 作为它们的最后一个参数才能在最终组合中提取它。

我认为你仍然可以用 point-free 的方式来写它, 可读性,然而,得到一点妥协.. 希望对您有所帮助:)

/**
 * @param {string} id
 * @param {Object<string, *>} state
**/
const getItemById = R.useWith(R.prop, [
  R.identity,
  R.prop('items'),
]);

const getItemsFromObj = R.useWith(R.flip(R.map), [
  R.pipe(R.prop('items'), R.map(getItemById)),
  R.applyTo,
]);


const items = {
  1: { id: 1, title: 'Hello World' },
  2: { id: 2, title: 'Hello Galaxy' },
  3: { id: 3, title: 'Hello Universe' },
};

const state = { items };

// this should only take 'world' and 'universe';
console.log(
  'result', 
  getItemsFromObj({ items: [1, 3] }, state),
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js" integrity="sha256-xB25ljGZ7K2VXnq087unEnoVhvTosWWtqXB4tAtZmHU=" crossorigin="anonymous"></script>


备注:

  1. Ramda 函数都是柯里化的,所以你 不需要 需要在尾部位置声明参数:obj => path(['items'], obj); 等于 path(['items']);
  2. 成为point-free有助于编写小而集中的功能,但它应该与组合可读性保持平衡

我喜欢pointfree风格。我认为这会迫使您对函数的设计三思而后行,这总是一件好事。然而,它 永远不会 成为目标。仅在有意义时使用它。

这个功能已经够用了:

const getItemById = id => state => path(['items', id], state);

我唯一的建议是使用 curry:

const getItemById = curry((id, state) => path(['items', id], state));

虽然你可以将它转换成 pointfree 风格,但我认为它实际上不值得:

const getItemById = useWith(path, [pair('items'), identity]);

让我们回到你的问题。

我们有这两个功能:

const getItemById = curry((id, state) => path(['items', id], state));
const getConnectedItemIds = propOr([], 'items');

你可以像这样设计一个 getItemsFromObj pointfree 风格:

const getItemsFromObj = useWith(flip(map), [getConnectedItemIds, flip(getItemById)]);

或者你也可以简单地做:

const getItemsFromObj = curry((obj, state) => {
  const ids = getConnectedItemIds(obj);
  const idFn = flip(getItemById)(state);
  return map(idFn, ids);
});

我会推荐哪个版本?我不知道;这里有几个想法可供考虑:

  1. 你觉得自然吗?
  2. 您的团队对 FP 的亲和力如何?你能训练他们吗?如果他们刚刚开始,请放轻松
  3. 六个月后,您觉得使用哪个版本会更自如?

不过我建议您熟悉 Hindley-Milner 类型系统。这绝对是投资的时间。

不要让任何人告诉你,如果你没有使用 pointfree 风格,你就没有正确地进行函数式编程。

可以让它既短又无点,但我不确定它是否比使用变量的函数更具可读性。

const getItemsFromObj = R.useWith(R.props, [R.prop('items'), R.prop('items')])

我不会在这里使用您的 getItemById,因为这种情况非常适合 R.props。如果你真的想使用你原来的两个功能,你可以这样写。

const getItemsFromObj = R.useWith(
  R.flip(R.map), [getConnectedItemIds, R.flip(R.uncurryN(2, getItemById))]
)

需要 flip 和 uncurry 来将 getItemById 函数参数从 id -> state -> value 反转为 state -> id -> value

这里已经有很多好的建议。可能最重要的是建议仅在提高可读性时才使用 point-free,而不是单独作为目标。

我的回答确实使用了 point-free 版本的主要功能和你的一个助手,但跳过另一个版本,我认为可读性会受到影响。

const getItemById = id => path(['items', id]);

const getConnectedItemIds = prop ('items');

const getItemsFromObj = pipe (
  getConnectedItemIds, 
  map (getItemById), 
  juxt
)

const obj = {foo: 42, items: ['8', '17']}
const state = {bar: 1, items: {'6': 'a', '8': 'b', '14': 'c', '17': 'd', '25': 'e'}}

console .log (
  getItemsFromObj (obj) (state)
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {juxt, map, path, pipe, prop} = R                      </script>

这里重要的函数是juxt,它将一组函数应用于相同的值,返回一组结果。

这里 getItemById 是从原来的基础上简化而来的,删除了 state 点,但是使这个 point-free 只能以牺牲可读性为代价来完成,就我而言告诉。其他人对此有建议,所有这些都很好,但正如他们指出的那样,none 与上面的一样可读。我的版本是这样的:

const getItemById = compose (path, flip (append) (['items']));
//  or              pipe (flip (append) (['items']), path);

我不认为它比其他答案中的建议更好或更差,但其中 none 与

一样易于阅读
const getItemById = id => path(['items', id]);

最后,如果那些辅助函数没有在其他地方使用,我认为内联它们实际上可以提高可读性:

const getItemsFromObj = pipe (
  prop ('items'), 
  map (id => path(['items', id])), 
  juxt
)

注意,虽然我没有在这里使用它,但我真的很喜欢customcommander对propOr([], 'items')getConnectedItemIds的建议。它彻底消除了一个潜在的故障点。