【问题标题】:Point-free Function Composition with Ramda.js使用 Ramda.js 的无点函数组合
【发布时间】:2019-11-28 06:19:30
【问题描述】:

我将 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 作为最后一个参数,才能在最终组合中将其拉出。

【问题讨论】:

    标签: javascript redux functional-programming ramda.js


    【解决方案1】:

    我认为您仍然可以以无点的方式编写它, 然而,可读性有点受损.. 希望对你有帮助:)

    /**
     * @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),
    );
    &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js" integrity="sha256-xB25ljGZ7K2VXnq087unEnoVhvTosWWtqXB4tAtZmHU=" crossorigin="anonymous"&gt;&lt;/script&gt;

    注意事项:

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

    【讨论】:

      【解决方案2】:

      我确实喜欢无点风格。我认为它迫使你对函数的设计三思而后行,这总是一件好事。然而,它应该永远成为一个目标。仅在有意义时才使用它。

      这个功能已经够用了:

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

      我唯一的建议是使用curry

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

      虽然你可以将其转换为无点样式,但我认为它实际上不值得:

      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 类型系统。时间绝对值得投资。

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

      【讨论】:

        【解决方案3】:

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

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

        getItemById 函数参数从 id -&gt; state -&gt; value 反转为 state -&gt; id -&gt; value 需要翻转和 uncurry

        【讨论】:

          【解决方案4】:

          这里已经有很多很好的建议。可能最重要的是建议仅在提高可读性时才使用无点,而不是将其作为目标。

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

          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 点来简化的,但据我所知,只有以可读性为代价才能做到这一点。其他人对此提出了建议,所有这些都很好,但正如他们所指出的,没有一个像上面那样可读。我的版本是这样的:

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

          我不认为它比其他答案中的建议更好或更差,但没有一个比其他答案更容易阅读

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

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

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

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

          【讨论】:

          • 我喜欢这个,比我使用applyTo的干净得多
          • @Hitmands:虽然我对此很满意,但我不喜欢juxt 这个名字有多么不直观。我们从 Clojure 借来了它,我猜这个名字来自 juxtapose,但它不是人们可以用这个名字搜索的东西。再说一次,我没有好的选择......
          • 嗯,我个人认为并列的概念很好地适用于这个函数的目标......我知道第一次接近时可能会有多么令人惊讶,但这就是它的作用......
          • @Hitmands:我认为将其命名为juxtapose 会更好,但为时已晚。
          • 可以添加juxt作为juxtapose的别名
          猜你喜欢
          • 1970-01-01
          • 2018-04-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-09-13
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多