【问题标题】:Ramda chain usageRamda 链的使用
【发布时间】:2018-05-20 20:20:01
【问题描述】:

来自documentation

var duplicate = n => [n, n];
R.chain(duplicate, [1, 2, 3]); //=> [1, 1, 2, 2, 3, 3]
R.chain(R.append, R.head)([1, 2, 3]); //=> [1, 2, 3, 1]

第一个示例非常简单,它将duplicate() 应用于数组中的每个元素并将结果连接起来。但我无法理解第二个例子。它究竟是如何将 R.append + R.head 映射到数组上的?有人可以为第二个示例提供分步说明吗?

我熟悉 compose 和 currying。

谢谢

【问题讨论】:

    标签: ramda.js


    【解决方案1】:

    第二个示例展示了 R.chain 如何用于数组以外的其他事物,例如函数(或任何实现 Fantasy Land chain 规范的事物)。

    如果您熟悉映射然后连接数组的概念,您可以将一个函数映射到另一个函数上视为普通函数组合。连接部分需要进一步解释。

    R.chain 声明其签名为:

    Chain m => (a → m b) → m a → m b
    

    对于数组,我们可以将m[] 交换得到:

    (a → [b]) → [a] → [b]
    

    对于接收一些参数r的函数,它变成:

    (a → r → b) → (r → a) → (r → b)
    

    因此,仅知道这些类型可用,生成最终r → b 函数的唯一方法是执行以下操作:

    • 将接收到的参数r 传递给第二个函数以生成a
    • 将新的a 和原始的r 应用到第一个函数以生成结果b

    或者在代码中:

    // specialised to functions
    const chain = (firstFn, secondFn) =>
      x => firstFn(secondFn(x), x)
    

    从示例中交换函数,您可以看到它变成:

    x => R.append(R.head(x), x)
    

    如果您熟悉R.converge,那么这很有效:

    R.converge(firstFn, [secondFn, R.identity])
    

    【讨论】:

    • 非常感谢您的解释,现在对我来说更有意义了。
    • 我见过的最好的解释!
    • 说实话我还是不明白。是不是 R.chain 函数对数组做一件事,对函数做另一件事?它们是两个功能合二为一吗?形式: const chain = (firstFn, secondFn) => x => firstFn(secondFn(x), x) 也可以对数组情况有意义吗?在那种情况下,secondFn 是哪个函数?
    • @taz0k R.chain 的行为会根据第二个参数的类型而有所不同。对于数组,它的行为类似于chain = (fn, list) => R.unnest(R.map(fn, list))
    • 感谢您的解释。
    【解决方案2】:

    首先查看函数的抽象版本R.chain 并区分被视为单子的函数m: r -> a 和被视为Kleisli 箭头的函数可能更容易,如this answer 中所述。

    那么R.chain定义为:

    // (a -> r -> b, r -> a) -> r -> b
    R.chain = (f, m) => x => f(m(x))(x)
    

    这很有用,当x 是某种配置参数时,fm 都一样。那么a = m(x)m为那个参数返回的值,g = f(_)(x)f为同一个参数返回的函数。将x 视为某种环境,同时进入mf。那么上面的定义可以分解为:

    R.chain = (f, m) => x => {
        const a = m(x)
            , g = a => f(a)(x)
        return g(a)
    }
    

    相比之下,函数的R.map 对应于f 独立于该参数x 的情况:

    // (a -> b, r -> a) -> r -> b
    R.map = (f, m) => x => f(m(x))
    

    当然,这是从外部看的通常的函数组合。

    定义chain(在Haskell 中又名bind)的另一种概念方法是应用map(又名fmap),然后是flatten(又名join)。

    R.chain = (f, m) => {
        // m1: r -> r -> b
        const m1 = R.map(f, m)
        // flattening to fm1: r -> b
        const fm1 = x => m1(x)(x)
        return fm1
    }
    

    现在对于m = x => R.head(x)f = a => x => R.append(a)(x)R.chain(f, m) 相当于将参数x 放入fm 并组合结果:

    x => R.append(R.head(x))(x)
    

    这给出了预期的结果。

    警告。注意这里的R.append 函数必须是柯里化的,因为它代表Kleisli 箭头a -> r -> b。顺便说一句,Ramda 提供了与 uncurried 相同的命名函数,但它是这里使用的 curried 函数。要看到这一点,让我们获取自定义的 uncurried R.append

    const appendCustom = (a, b) => R.append(a, b)
    

    然后注意 Ramda 的 REPL 是如何抛出错误并给出意外结果的:

    // 定义我们自己的 uncurried append const appendCustom = (a, b) => R.append(a, b) R.chain(appendCustom, R.head)([1, 2]); // => t(...) 不是函数

    http://ramdajs.com/repl/?v=0.25.0#?%2F%2F%20Define%20our%20own%20uncurried%20append%0Aconst%20appendCustom%20%3D%20%28a%2C%20b%29%20%3D%3E%20R.append%28a%2C%20b%29%0AR.chain%28appendCustom%2C%20R.head%29%28%5B1%2C%202%5D%29%3B%0A

    这里真正发生了什么,appendCustom 以其柯里化形式执行:appendCustom(a)(b),第二次调用被委托给某个返回错误的内部函数。

    【讨论】:

    • 在 SO 上使用 sn-p 而不是指向 REPL 的链接会更好。在介绍了 sn-ps 之后,我花了一些时间回答问题才意识到这一点,但它对读者肯定更有帮助。
    【解决方案3】:

    希望对你有帮助

    let R = require('ramda')
    
    // using vanillajs
    let append = (arr1) => (arr2) => arr2.concat(arr1)
    let double = (arr1) => arr1.map( x => 2*x )
    
    let chain = (f, g) => arr => {
       let yarr = g(arr)
       return f(yarr)(arr)
    }
    
    console.log(chain(
       append,
       double
    )([10, 15, 20]))
    
    //using Ramda
    console.log(R.chain(append, double)([10, 15, 20]))
    

    【讨论】:

    • append 不是concat,参见ramdajs.com/docs/#append 的最后一个示例:R.append(['tests'], ['write', 'more']); //=> ['write', 'more', ['tests']]
    【解决方案4】:

    chain(大约)定义为(对于函数):(fn, monad) => x => fn(monad(x))(x)

    因此,我们可以像这样转换R.chain(R.append, R.head)([1, 2, 3]);

    R.chain(R.append, R.head)([1, 2, 3]);
    
    R.append(R.head([1, 2, 3]), [1, 2, 3]); // This is the important step
    
    R.append(1, [1, 2, 3]);
    
    [1, 2, 3, 1];
    

    The actual source:

    var chain = _curry2(_dispatchable(['fantasy-land/chain', 'chain'], _xchain, function chain(fn, monad) {
      if (typeof monad === 'function') {
        return function(x) { return fn(monad(x))(x); };
      }
      return _makeFlat(false)(map(fn, monad));
    }));
    

    其中重要的部分是:

    function chain(fn, monad) {
      if (typeof monad === 'function') {
        return function(x) { return fn(monad(x))(x); };
      }
      return _makeFlat(false)(map(fn, monad));
    }
    

    根据the comment in the code,“_makeFlat 是一个辅助函数,它根据传入的标志返回一个一级或完全递归函数。”

    _makeFlat(false) 似乎等同于unnest_makeFlat(true) 似乎等同于flatten

    【讨论】:

    • monad(x) 没有意义,除非它是一个函数
    • 是的,我在回答中澄清了。
    • 这个来自ramda的工作代码不是独立的,不知道所有使用的东西是不可能理解的,简单地复制没有解释是没有多大帮助的。
    猜你喜欢
    • 1970-01-01
    • 2018-03-13
    • 2017-10-07
    • 2018-12-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-02-14
    相关资源
    最近更新 更多