在javascript函数中使用函数式Haskell-like Accumulator

Use of functional Haskell-like Accumulator in javascript functions

本文关键字:函数 Haskell-like Accumulator javascript      更新时间:2023-09-26

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

问题:

  1. 在javascript中是否有类似的结构?或者它甚至在效率方面是有意义的,因为javascript不是as功能像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)
    

在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循环。

这是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。你可以用你的就像我上面展示的那样。还要注意lodash是提供用于操作的一致API的实用程序库以功能的方式收集。

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

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

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

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

Bergi对JS的翻译很好,尽管我认为至少在浏览器端JS中更习惯,将helper函数嵌入到局部闭包中,这更接近于Sibi的答案。

Martin Drautzburg指出的性能问题的原因是,尽管指定了尾部调用优化,但它几乎没有在任何地方实现。一个例外是Babel对直接递归的支持,因此Babel编译的版本应该获得预期的性能优势。

所以,如果你想这样做是因为它的优雅,因为你相信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

默认函数参数可以帮助替换这种语言中一些简单形式的模式匹配,因为它没有真正的模式匹配。这仍然依赖于TCO,但使代码更简洁。