哪些语言支持递归函数文字/匿名函数?

时间:2020-03-06 14:57:45  来源:igfitidea点击:

如今,似乎有很多主流语言都支持函数字面量。它们也称为匿名函数,但是我不在乎它们是否有名称。重要的是函数字面量是一个表达式,它产生的函数在其他地方尚未定义,因此例如在C中,&printf不计算在内。

编辑添加:如果我们具有真正的函数字面量表达式" <exp>",则应该能够将其传递给函数f(<exp>)或者立即将其应用于参数,即<exp>(5)`。

我很好奇哪种语言可以让我们编写递归的函数文字。维基百科的"匿名递归"文章没有提供任何编程示例。

让我们以递归阶乘函数为例。

这是我所知道的:

  • JavaScript / ECMAScript可以通过callee实现:
function(n){if (n<2) {return 1;} else {return n * arguments.callee(n-1);}}
  • 在带有letrec的语言中很容易,例如Haskell(称之为let):
let fac x = if x<2 then 1 else fac (x-1) * x in fac

Lisp和Scheme中有等效项。注意,fac的绑定是表达式的局部变量,因此整个表达式实际上是一个匿名函数。

还有其他吗?

解决方案

有"让REC"

嗯,除了我们已经提到的Common Lisp(labels)和Scheme(letrec)之外,JavaScript还允许我们命名匿名函数:

var foo = {"bar": function baz() {return baz() + 1;}};

这比使用callee更方便。 (这与顶层的"功能"不同;后者会导致名称也出现在全局范围内,而在前一种情况下,名称仅出现在功能本身的范围内。)

大多数语言都通过使用Y组合器来支持它。这是Python中的示例(来自菜谱):

# Define Y combinator...come on Gudio, put it in functools!
Y = lambda g: (lambda f: g(lambda arg: f(f)(arg))) (lambda f: g(lambda arg: f(f)(arg)))

# Define anonymous recursive factorial function
fac = Y(lambda f: lambda n: (1 if n<2 else n*f(n-1)))
assert fac(7) == 5040

我们可以在Perl中做到:

my $factorial = do {
  my $fac;
  $fac = sub {
    my $n = shift;
    if ($n < 2) { 1 } else { $n * $fac->($n-1) }
  };
};

print $factorial->(4);

绝对没有必要使用do块;我将其包括在内以强调结果是一个真正的匿名函数。

在C中,我们需要声明一个变量来保存委托,并为它分配null以确保已明确分配它,然后可以在分配给它的lambda表达式中调用它:

Func<int, int> fac = null;
fac = n => n < 2 ? 1 : n * fac(n-1);
Console.WriteLine(fac(7));

我想我听说有传言说Cteam正在考虑更改定值分配规则,以使不需要单独的声明/初始化,但是我不会发誓。

对于每种语言/运行时环境,一个重要的问题是它们是否支持尾部调用。据我所知,在C#中,MS编译器未使用" tail。" IL操作码,但在​​某些情况下JIT仍会对其进行优化。显然,这很容易使工作程序与堆栈溢出之间有所区别。 (最好对此有更多的控制,和/或者保证何时发生。否则,一台计算机上运行的程序可能会以难以理解的方式在另一台计算机上失败。)

编辑:正如FryHard所指出的,这只是伪递归。很简单,可以完成工作,但是Y组合器是一种更纯净的方法。我上面发布的代码还有另外一个警告:如果更改fac的值,则任何尝试使用旧值的尝试都会开始失败,因为lambda表达式本身已经捕获了fac变量。 (当然,为了完全正常工作,必须这样做……)

我认为这可能并不是我们想要的,但是在Lisp中,"标签"可用于动态声明可以递归调用的函数。

(labels ((factorial (x) ;define name and params
    ; body of function addrec
    (if (= x 1)
      (return 1)
      (+ (factorial (- x 1))))) ;should not close out labels
  ;call factorial inside labels function
  (factorial 5)) ;this would return 15 from labels

C#

阅读Wes Dyer的博客,我们会发现@Jon Skeet的答案并不完全正确。我不是语言天才,但是递归匿名函数和" fib函数实际上只是调用了本地变量fib引用的委托"之间的引用是有区别的。

实际的Canswer看起来像这样:

delegate Func<A, R> Recursive<A, R>(Recursive<A, R> r);

static Func<A, R> Y<A, R>(Func<Func<A, R>, Func<A, R>> f)
{
    Recursive<A, R> rec = r => a => f(r(r))(a);
    return rec(rec);
}

static void Main(string[] args)
{
    Func<int,int> fib = Y<int,int>(f => n => n > 1 ? f(n - 1) + f(n - 2) : n);
    Func<int, int> fact = Y<int, int>(f => n => n > 1 ? n * f(n - 1) : 1);
    Console.WriteLine(fib(6));                          // displays 8
    Console.WriteLine(fact(6));
    Console.ReadLine();
}

Delphi在2009版中包含匿名功能。

来自http://blogs.codegear.com/davidi/2008/07/23/38915/的示例

type
  // method reference
  TProc = reference to procedure(x: Integer);               

procedure Call(const proc: TProc);
begin
  proc(42);
end;

使用:

var
  proc: TProc;
begin
  // anonymous method
  proc := procedure(a: Integer)
  begin
    Writeln(a);
  end;               

  Call(proc);
  readln
end.

我们在这里混用了一些术语,函数文字不必是匿名的。

在javascript中,区别取决于函数是作为语句还是表达式编写。在这个问题的答案中有一些关于区别的讨论。

假设我们要将示例传递给函数:

foo(function(n){if (n<2) {return 1;} else {return n * arguments.callee(n-1);}});

也可以这样写:

foo(function fac(n){if (n<2) {return 1;} else {return n * fac(n-1);}});

在两种情况下都是函数文字。但是请注意,在第二个示例中,该名称未添加到周围的作用域中,这可能会造成混淆。但这并未得到广泛使用,因为某些javascript实现不支持此功能或者存在错误的实现。我也读过它比较慢。

匿名递归又有所不同,就是当函数递归而不引用自身时,已经提到了Y组合器。在大多数语言中,没有必要使用更好的方法。这是javascript实现的链接。

在Perl 6中:

my $f = -> $n { if ($n <= 1) {1} else {$n * &?BLOCK($n - 1)} }
$f(42);  # ==> 1405006117752879898543142606244511569936384000000000

因为我很好奇,所以我实际上试图提出一种在MATLAB中执行此操作的方法。可以做到,但看起来有点像鲁伯·戈德堡风格:

>> fact = @(val,branchFcns) val*branchFcns{(val <= 1)+1}(val-1,branchFcns);
>> returnOne = @(val,branchFcns) 1;
>> branchFcns = {fact returnOne};
>> fact(4,branchFcns)

ans =

    24

>> fact(5,branchFcns)

ans =

   120

我们可以在Matlab中使用匿名函数执行此操作,该函数使用dbstack()自省功能获取自身的函数文字,然后对其进行评估。 (我承认这是骗人的,因为dbstack应该被认为是语言外的,但是在所有Matlab中都可用。)

f = @(x) ~x || feval(str2func(getfield(dbstack, 'name')), x-1)

这是一个从x倒数然后返回1的匿名函数。它不是很有用,因为Matlab缺少?:运算符,并且不允许匿名函数内部使用if-blocks,因此很难构造基本案例/递归步骤形式。

我们可以通过调用f(-1)证明它是递归的;它将倒数到无穷大,并最终引发最大递归错误。

>> f(-1)
??? Maximum recursion limit of 500 reached. Use set(0,'RecursionLimit',N)
to change the limit.  Be aware that exceeding your available stack space can
crash MATLAB and/or your computer.

我们可以通过直接将匿名函数传递给feval来直接调用匿名函数,而无需将其绑定到任何变量。

>> feval(@(x) ~x || feval(str2func(getfield(dbstack, 'name')), x-1), -1)
??? Maximum recursion limit of 500 reached. Use set(0,'RecursionLimit',N)
to change the limit.  Be aware that exceeding your available stack space can
crash MATLAB and/or your computer.

Error in ==> create@(x)~x||feval(str2func(getfield(dbstack,'name')),x-1)

为了使其中的内容有用,我们可以创建一个单独的函数来实现递归步骤逻辑,并使用" if"来保护递归情况免于评估。

function out = basecase_or_feval(cond, baseval, fcn, args, accumfcn)
%BASECASE_OR_FEVAL Return base case value, or evaluate next step
if cond
    out = baseval;
else
    out = feval(accumfcn, feval(fcn, args{:}));
end

鉴于此,这里是阶乘。

recursive_factorial = @(x) basecase_or_feval(x < 2,...
    1,...
    str2func(getfield(dbstack, 'name')),...
    {x-1},...
    @(z)x*z);

我们可以不绑定而调用它。

>> feval( @(x) basecase_or_feval(x < 2, 1, str2func(getfield(dbstack, 'name')), {x-1}, @(z)x*z), 5)
ans =
   120

匿名函数存在于带有lambda的C ++ 0x中,并且它们可能是递归的,尽管我不确定匿名。

auto kek = [](){kek();}

"看来我们对匿名函数的想法是错误的,这不仅与运行时创建有关,还与范围有关。考虑以下Scheme宏:

(define-syntax lambdarec
  (syntax-rules ()
    ((lambdarec (tag . params) . body)
     ((lambda ()
       (define (tag . params) . body)
        tag)))))

这样:

(lambdarec (f n) (if (<= n 0) 1 (* n (f (- n 1)))))

评估为真正的匿名递归阶乘函数,例如,可以像以下方式使用:

(let ;no letrec used
    ((factorial (lambdarec (f n) (if (<= n 0) 1 (* n (f (- n 1)))))))
  (factorial 4)) ; ===> 24

但是,使它匿名的真正原因是,如果我这样做:

((lambdarec (f n) (if (<= n 0) 1 (* n (f (- n 1))))) 4)

此函数随后从内存中清除并且没有作用域,因此在此之后:

(f 4)

将表示错误,或者将绑定到之前绑定到的任何f。

在Haskell中,实现此目标的一种特殊方法是:

\n -> let fac x = if x<2 then 1 else fac (x-1) * x
    in fac n

再次不同的是,该函数没有作用域,如果我不使用它,Haskell是惰性的,其效果与空的代码行相同,它的确是字面意义上的,因为它与C代码具有相同的效果:

3;

文字数字。即使之后立即使用它,它也会消失。这就是文字函数的含义,而不是运行时本身的创建。