如何在ES6中递归地编写箭头函数?

50
在ES6中,箭头函数没有arguments属性,因此arguments.callee将无法工作,即使只使用匿名函数,也不会在严格模式下工作。
箭头函数不能被命名,因此不能使用命名函数表达式的技巧。
那么...如何编写递归箭头函数呢?也就是说,箭头函数根据某些条件进行递归调用等等。

3
通常情况下,您可以使用普通函数,但箭头函数可能不是递归调用的合适工具。 - dfsq
如果使用三元运算符,阶乘函数可以写成一条语句。单语句匿名函数当然是箭头函数的一个使用案例。 - Siddharth
1
为什么不将函数分配给一个变量,该变量又在函数体的作用域内? - philipp
那不太健壮。如果将函数分配给不同的变量并且原始函数被重新分配给新值,您的代码会出错。通常可以使用命名的函数表达式来解决此问题,但箭头函数不能在其自己的作用域中命名。 - Siddharth
如果使用const关键字,那么原变量就不能再被重新赋值。看起来非常健壮,而且现在已被广泛使用。 - Dtipson
13个回答

41

写一个不命名的递归函数是计算机科学本身就存在的问题(实际上更古老,因为λ-演算先于计算机科学),因为在λ-演算中所有函数都是匿名的,但你仍然需要递归。

解决方案是使用固定点组合器,通常是Y组合器。它看起来像这样:

(y => 
  y(
    givenFact => 
      n => 
        n < 2 ? 1 : n * givenFact(n-1)
  )(5)
)(le => 
  (f => 
    f(f)
  )(f => 
    le(x => (f(f))(x))
  )
);

这将递归计算5的阶乘。

注意:该代码基于 用JavaScript解释Y Combinator。所有功劳应归原作者所有。我只是���其上进行了“和谐化”(用ES / Harmony的新功能重构旧代码而已)。


32
说实话,如果有人使用 Y 组合子来代替给函数命名的话...(这是对一个前提非常糟糕的问题的好回答;-)) - John Dvorak
3
那个回答将函数分配给一个变量,而问答者不允许这样做。我知道这很愚蠢,你也知道这很愚蠢,每个人都知道这很愚蠢,但这就是要求。 - Jörg W Mittag
1
我更喜欢“harmonize”而不是“harmonicalize”,原因与我更喜欢“canonize”而不是“canonicalize”相同。 - Alex Shroyer
2
那看起来很可怕。 - sdfsdf
1
答案很有趣,但代码本身可能会让你的队友非常困惑。给它命名会是一个更好的解决方法。 - yuan
显示剩余6条评论

23

看起来你可以将箭头函数分配给一个变量,并使用它来递归调用函数。

var complex = (a, b) => {
    if (a > b) {
        return a;
    } else {
        complex(a, b);
    }
};

1
complex(2,3) 会返回 InternalError: too much recursion - Ortomala Lokni
1
是的,这只是一个简单的函数,用于显示递归调用。 - user405398
1
这会将函数和名称绑定在一起。不是个好主意。 - Siddharth
7
为什么他非要把它变成一个可工作的阶乘函数呢? - Jonathan Lerner
@JonathanLerner 因为这是递归函数的典型示例,而这里的问题是:如何编写递归箭头函数? - Ortomala Lokni
13
太荒谬了。任何一个递归函数的例子都是这个问题的有效答案。提问者不喜欢给函数指定名称,因此可能不会接受这个答案,但你对TamilVendhan的抱怨是荒谬的... - Jonathan Lerner

17

Claus Reinke在esdiscuss.org网站的讨论中回答了你的问题。

在ES6中,你必须定义他所称的递归组合器。

 let rec = (f)=> (..args)=> f( (..args)=>rec(f)(..args), ..args )

如果您想调用递归箭头函数,您需要使用箭头函数作为参数调用递归组合器,箭头函数的第一个参数是一个递归函数,其余参数是参数。递归函数的名称并不重要,因为它不会在递归组合器之外使用。然后可以调用匿名箭头函数。这里我们计算6的阶乘。

 rec( (f,n) => (n>1 ? n*f(n-1) : n) )(6)
如果您想在Firefox中测试它,您需要使用递归组合器的ES5翻译版本:
function rec(f){ 
    return function(){
        return f.apply(this,[
                               function(){
                                  return rec(f).apply(this,arguments);
                                }
                            ].concat(Array.prototype.slice.call(arguments))
                      );
    }
}

这看起来非常有趣。我认为这解决了它。 - Siddharth
只是想澄清(.. vs ...):let rec = (f)=> (...args)=> f( (...args)=>rec(f)(...args), ...args ); rec( (f,n) => (n>1 ? n*f(n-1) : n) )(6); - aviit

11

简而言之:

const rec = f => f((...xs) => rec(f)(...xs));

这里有很多答案都在变化一个正确的Y -- 但那有点冗余...问题在于通常解释Y的方式是“如果没有递归怎么办”,所以Y本身不能引用它自己。但由于这里的目标是一个实用的组合器,没有必要这样做。有这个答案使用它自己来定义rec,但它很复杂,而且有点丑陋,因为它添加了一个参数而不是柯里化。

简单地递归定义Y是:

const rec = f => f(rec(f));

但是由于JS不是惰性的,以上代码添加了必要的包装。


2
这才是真正的答案。我们不需要直接跳到完整的Y组合器实现,因为我们不在一个没有递归的语言中。 - slazarus

10

使用变量来分配函数,例如:

const fac = (n) => n>0 ? n*fac(n-1) : 1;

如果您真的需要匿名,可以使用Y组合子,像这样:

const Y = (f) => ((x)=>f((v)=>x(x)(v)))((x)=>f((v)=>x(x)(v)))
… Y((fac)=>(n)=> n>0 ? n*fac(n-1) : 1) …

(丑陋,不是吗?)


5
非常丑陋,但概念上非常美丽。 - Siddharth

6
我发现提供的解决方案非常复杂,老实说我一个也没懂,所以我自己想出了一个更简单的解决方案(我相信它已经被人知道了,但这是我的思考过程):
你正在编写一个阶乘函数。
x => x < 2 ? x : x * (???)

(???) 是函数应该调用自身的位置,但由于您无法命名它,显而易见的解决方案是将其作为参数传递给自身。

f => x => x < 2 ? x : x * f(x-1)

然而这样做行不通。因为当我们调用 f(x-1) 时,我们正在调用该函数本身,并且我们刚刚将其参数定义为1)f:再次是函数本身,以及2)x :该值。嗯,我们确实有函数本身,记得吗?所以首先传递它:

f => x => x < 2 ? x : x * f(f)(x-1)
                            ^ the new bit

就是这样。我们刚刚创建了一个函数,将自身作为第一个参数,生成阶乘函数!只需将其传递给自身即可:

(f => x => x < 2 ? x : x * f(f)(x-1))(f => x => x < 2 ? x : x * f(f)(x-1))(5)
>120

你可以编写另一个函数,将其参数传递给自身,而不是重复编写该函数:

y => y(y)

并将您的阶乘制作函数传递给它:

(y => y(y))(f => x => x < 2 ? x : x * f(f)(x-1))(5)
>120

嘭。这里有一个简单的公式:

(y => y(y))(f => x => endCondition(x) ? default(x) : operation(x)(f(f)(nextStep(x))))

对于一个从0加到x的基本函数,endCondition是需要停止递归的条件,因此x => x == 0default是在满足endCondition后给出的最后一个值,因此为x => xoperation仅是每次递归中所执行的操作,例如在阶乘中进行乘法,在斐波那契数列中进行加法:x1 => x2 => x1 + x2。最后,nextStep是要传递给函数的下一个值,通常是当前值减一:x => x - 1。应用:

(y => y(y))(f => x => x == 0 ? x : x + f(f)(x - 1))(5)
>15

1
这被称为“U组合子” const U = f => f(f) - user6445533
1
这个更易读。 - khaiyom

5

一个适用于任意数量参数的递归函数定义的通用组合器(不使用变量自身)是:

const rec = (le => ((f => f(f))(f => (le((...x) => f(f)(...x))))));

例如可以用这个来定义阶乘:

const factorial = rec( fact => (n => n < 2 ? 1 : n * fact(n - 1)) );
//factorial(5): 120

或字符串反转:
const reverse = rec(
  rev => (
    (w, start) => typeof(start) === "string" 
                ? (!w ? start : rev(w.substring(1), w[0] + start)) 
                : rev(w, '')
  )
);
//reverse("olleh"): "hello"

或者按顺序遍历树:

const inorder = rec(go => ((node, visit) => !!(node && [go(node.left, visit), visit(node), go(node.right, visit)])));

//inorder({left:{value:3},value:4,right:{value:5}}, function(n) {console.log(n.value)})
// calls console.log(3)
// calls console.log(4)
// calls console.log(5)
// returns true

3

由于 arguments.callee 已被废弃 / 在严格模式下不起作用,而像 var func = () => {} 这样的做法也不好,因此像这个答案描述的那样使用 hack 可能是您唯一的选择:

javascript: recursive anonymous function?


该属性已被弃用,不建议使用。在严格模式下也无法正常工作。 - Siddharth
1
如果我没记错的话,ES6模块是隐式严格模式,没有选择退出的选项。 - Fabrício Matté
将 "use strict"; 写入我的控制台,然后重新运行代码并没有抛出任何错误。编辑:这条评论是错误的,你是正确的。 - Jonathan Lerner
1
@Siddharth,由于不允许使用arguments.callee,并且您不想像func = () => {}那样做些事情,因此可能没有任何方法,尽管您可以像我链接到的其他SO答案中使用hack。 - Jonathan Lerner
"var func = () => {}" - 等等,为什么? - John Dvorak
@JanDvorak请阅读提问者对问题的评论;将函数分配给名称是不可取的。 - Jonathan Lerner

3
var rec = () => {rec()};
rec();

那行得通吗?

请阅读我在问题上的最后一条评论。它与其分配的变量紧密耦合。重新分配给另一个变量并将rec重新分配给其他任何内容,它就会出现错误。 - Siddharth
1
嗯,我猜箭头函数不能做你想要的事情,所以你需要使用普通函数。 - philipp
将其包装在一个闭包中。 - sleblanc
但这需要编写“函数”,而且也不能缩短所需的代码。当然,可以围绕圆圈走,而直线是可能的,但是否有意义是另一个问题。 - philipp

2
这是此答案的版本,链接为https://dev59.com/dG865IYBdhLWcg3wTctY#3903334,使用箭头函数。
你可以使用 UY 组合子。其中 Y 组合子是最简单的。 U 组合子,你需要不断传递函数: const U = f => f(f) U(selfFn => arg => selfFn(selfFn)('to infinity and beyond')) Y 组合子,你不需要不断传递函数: const Y = gen => U(f => gen((...args) => f(f)(...args))) Y(selfFn => arg => selfFn('to infinity and beyond'))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接