【问题标题】:Use of functional Haskell-like Accumulator in javascript functions在 javascript 函数中使用类似 Haskell 的函数式累加器
【发布时间】:2015-09-21 09:45:55
【问题描述】:

我目前正在研究 Haskell,我对它的一些特性很着迷,例如使用累加器的末端递归函数。

问题:

  1. javascript 中是否有类似的构造?或者它甚至 关于效率是有道理的,因为 javascript 不像 像 Haskell 一样运行?
  2. 有没有像 ramda, lodash, ...这样的库支持这种方式 编程
  3. 如果是这样,你会如何用javascript写这个:

     power_acc :: Double -> Int -> Double
     power_acc x y = power_acc_h x y 1
    
     power_acc_h :: Double -> Int -> Double -> Double
     power_acc_h x 0 acc = acc
     power_acc_h x y acc = power_acc_h x (y-1) (acc*x)
    

【问题讨论】:

  • 我的 2 美分:尽管我很喜欢函数式编程,但我发现习惯用法 accumulator+recursion 是有状态 for 循环的一种有点不雅的编码。在命令式语言中,我更喜欢循环而不是 FP 习语。当然,有时这样的循环实际上是折叠,就像在发布的示例中一样,在这种情况下,我发现 FP 高阶折叠比 fors 或累加器更优雅。当然,所有这些都只是个人喜好问题。

标签: javascript haskell underscore.js lodash ramda.js


【解决方案1】:

javascript 中是否有类似的构造?

是的,你可以直接把它翻译成 JS:

function power_acc(x, y) { // Double -> Int -> Double
    y = y>>>0; // cast to positive int (avoiding nontermination)
    return power_acc_h(x, y, 1);
}
function power_acc_h(x, y, acc) { // Double -> Int -> Double -> Double
    return y == 0
      ? acc
      : power_acc_h(x, y-1, acc*x);
}

或者,由于 javascript 的功能不如 Haskell,因此在效率方面是否有意义?

使用 ES6,JS 完全支持尾递归,您将获得与循环相同的效率(甚至可能比 haskell 更好,因为您不会创建惰性乘法)。

有没有像 ramda, lodash, ...这样的库支持这种编程方式

不需要库。虽然我确信有一些库可以简化类型检查或为模式匹配提供更好的符号。

例如,您将如何用 javascript 编写此代码?

您将使用while 循环。 haskell 中所有的累加函数都是这样写的,因为它们可以直接优化成一个循环,这就是你应该在 JS 中使用这个构造的表示法(因为大多数程序员都熟悉它):

function power_acc(x, y) { // Double -> Int -> Double
    y = y>>>0; // cast to positive int (avoiding nontermination)
    var acc = 1;
    while (y != 0) {
        acc *= x;
        y -= 1;
    }
    return acc;
}

改变局部变量没有坏处,你的函数仍然是纯的。如果您正在寻找更短的符号,请使用 for 循环。

【讨论】:

  • “使用 ES6,JS 完全支持尾递归”……不过,真的是not implemented yet
  • @ScottSauyet:是的(除非是 babel)。使用惯用循环的另一个原因:-)
【解决方案2】:

这是用 javascript 直接翻译 Haskell 代码:

function power_acc(x, y) {
    return aux(x,y,1);
    function aux(x, y, acc) {
        if (y == 0)
            return acc;
        else
            return aux(x, y-1, acc*x);
    }
}

有没有像 ramda, lodash, ...这样的库支持这个 编程方式?

你不需要 lodash 或 ramda。你可以用你的 就像我上面展示的那样,纯 javascript。另请注意,lodash 是 一个实用程序库,提供一致的 API 用于操作 以实用的方式收藏。在这些情况下它对您没有帮助。

【讨论】:

    【解决方案3】:

    除了 Sibi 的回答,我想指出 javascript(至少是 nodejs)实际上分配了堆栈空间。它工作得很好而且很快,指数大约为 13,000,然后你会得到RangeError: Maximum call stack size exceeded。要进行此实验,您需要将基数设置为接近 1 的数字(例如 1.0001),否则您将获得 Infinity。

    Haskell 不会遇到这个问题。 1000 倍大的指数(即 13,000,000)仍然不会导致任何空间问题,尽管它确实需要几秒钟才能运行。这是因为递归是尾调用,而这些在 haskell 中是在常量空间中运行的。

    因此,Sibi 的回答在某种程度上模仿了 haskell 的表现力,但它仍然表现出不同的运行时行为。我认为您对此无能为力。

    【讨论】:

      【解决方案4】:

      我同意所有的答案,即图书馆既不是必需的,也不是特别有用。 (顺便说一句,我是 Ramda 的作者之一。)

      Bergi 的 JS 翻译很好,虽然我认为至少在浏览器端的 JS 中更惯用,将辅助函数嵌入到本地闭包中,这更接近 Sibi 的答案。

      Martin Drautzburg 指出的性能问题的原因是,虽然尾部调用优化is specified,但它在任何地方都是barely implemented。一个例外是 Babel 对直接递归的支持,因此 Babel-transpiled 版本应该获得预期的性能优势。

      因此,如果您因为优雅而想要这样做,并且因为您相信 TCO 很快就会出现,并且如果您不担心当前可能出现的性能问题,那么这些响应很有用,我什至会抛出混合了另一种 ES6 技术:

      // Double -> Int -> Double -> Double
      function powerAcc(x, y, acc = 1) {
          return y == 0 ? acc : powerAcc(x, y - 1, acc * x);
      }
      
      powerAcc(2, 5); //=> 32
      

      Default function parameters 帮助替换该语言中一些简单形式的模式匹配,它没有真正的模式匹配。这仍然依赖于 TCO,但代码更简洁。它还应该在 Babel 中高效运行。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2018-11-21
        • 1970-01-01
        • 1970-01-01
        • 2023-03-23
        • 2019-11-13
        • 2021-10-10
        • 1970-01-01
        • 2019-02-06
        相关资源
        最近更新 更多