哪些语言支持*递归*函数文字/匿名函数?
Posted
技术标签:
【中文标题】哪些语言支持*递归*函数文字/匿名函数?【英文标题】:Which languages support *recursive* function literals / anonymous functions? 【发布时间】:2010-09-14 10:58:10 【问题描述】:现在似乎有不少主流语言支持function literals。他们也被称为anonymous functions,但我不在乎他们有没有名字。重要的是,函数字面量是一个表达式,它产生一个尚未在其他地方定义的函数,例如在 C 中,&printf
不计算在内。
编辑补充:如果你有一个真正的函数文字表达式<exp>
,你应该能够将它传递给一个函数f(<exp>)
或立即将它应用到一个参数,即。 <exp>(5)
。
我很好奇哪些语言可以让你编写递归的函数字面量。***的“anonymous recursion”文章没有给出任何编程示例。
我们以递归阶乘函数为例。
以下是我知道的:
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
的绑定是表达式的局部变量,因此整个表达式实际上是一个匿名函数。
还有其他的吗?
【问题讨论】:
匿名函数与动态函数不同,区别在于作用域,匿名函数或“函数字面量”是一个真正的对象,就像整数3一样。例如,5.3之前的php确实有动态函数,但不是匿名的。 (见下文) 【参考方案1】:大多数语言通过使用Y combinator 来支持它。这是一个 Python 示例(来自cookbook):
# 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
【讨论】:
"大多数语言" 不,任何静态类型的语言都不能写出真正的 Y 组合子。 @newacct,与您的说法相反,这里是 Scala 中 Y 组合器的实现:scala-blogs.org/2008/09/y-combinator-in-scala.html @avernet:我认为这不是一个“真正的”Y 组合子,因为 Y 组合子是一个非常特定形式的表达式,并且该示例需要一个外部类型,并且涉及创建一个对象表达式中的那种类型,它不在 Y 组合器中。 @newacct,这是一个有趣的论点,我对 Y 组合器的了解还不够,无法完全理解它。你所说的似乎与en.wikipedia.org/wiki/Fixed_point_combinator第二段中以“一个著名的定点组合器…”开头的几句话方向相同。【参考方案2】:C#
阅读Wes Dyer's 博客,您会发现@Jon Skeet 的回答并不完全正确。我不是语言天才,但递归匿名函数和“fib 函数实际上只是调用局部变量 fib 引用的委托”从博客中引用是有区别的。
实际的 C# 答案如下所示:
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();
【讨论】:
是的,Y 组合器肯定能解决问题,并且在某种更纯粹的意义上是递归 - 它比我的“伪递归”解决方案更难理解 :) 更难可能是轻描淡写:)【参考方案3】:你可以在 Perl 中做到这一点:
my $factorial = do
my $fac;
$fac = sub
my $n = shift;
if ($n < 2) 1 else $n * $fac->($n-1)
;
;
print $factorial->(4);
do
块不是绝对必要的;我包含它是为了强调结果是一个真正的匿名函数。
【讨论】:
【参考方案4】:嗯,除了您已经提到的 Common Lisp (labels
) 和 Scheme (letrec
) 之外,JavaScript 还允许您命名匿名函数:
var foo = "bar": function baz() return baz() + 1;;
这可能比using callee
更方便。 (这与顶层的function
不同;后者会导致名称也出现在全局范围内,而在前一种情况下,名称仅出现在函数本身的范围内。)
【讨论】:
【参考方案5】:在 Perl 6 中:
my $f = -> $n if ($n <= 1) 1 else $n * &?BLOCK($n - 1)
$f(42); # ==> 1405006117752879898543142606244511569936384000000000
【讨论】:
【参考方案6】:F# 有“let rec”
【讨论】:
【参考方案7】:您在这里混淆了一些术语,函数字面量不必是匿名的。
在 javascript 中,区别取决于函数是写成语句还是表达式。在this question的答案中有一些关于区别的讨论。
假设您将示例传递给函数:
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 Combinator。在大多数语言中,没有必要这样做,因为有更好的方法可用。这是a javascript implementation的链接。
【讨论】:
【参考方案8】:在 C# 中,您需要声明一个变量来保存委托,并将 null 分配给它以确保它已明确分配,然后您可以从分配给它的 lambda 表达式中调用它:
Func<int, int> fac = null;
fac = n => n < 2 ? 1 : n * fac(n-1);
Console.WriteLine(fac(7));
我认为我听说 C# 团队正在考虑更改明确分配的规则以使单独的声明/初始化变得不必要,但我不会发誓。
每种语言/运行时环境的一个重要问题是它们是否支持尾调用。在 C# 中,据我所知,MS 编译器不使用 tail.
IL 操作码,而是使用 JIT may optimise it anyway, in certain circumstances。显然,这很容易区分工作程序和堆栈溢出。 (最好能对此有更多的控制和/或保证它何时会发生。否则,在一台机器上运行的程序可能会以难以理解的方式在另一台机器上失败。)
编辑:正如FryHard 指出的,这只是伪递归。简单到足以完成工作,但 Y-combinator 是一种更纯粹的方法。我在上面发布的代码还有一个警告:如果您更改 fac
的值,任何尝试使用旧值的东西都会开始失败,因为 lambda 表达式已经捕获了 fac
变量本身。 (当然,它必须这样做才能正常工作......)
【讨论】:
您似乎走上了正确答案的道路,但并没有完全到达那里。创建 fac 的副本 (Func您可以在 Matlab 中使用匿名函数来执行此操作,该函数使用 dbstack() 自省来获取自身的函数文字,然后对其进行评估。 (我承认这是作弊,因为 dbstack 可能应该被认为是语言外的,但它在所有 Matlabs 中都可用。)
f = @(x) ~x || feval(str2func(getfield(dbstack, 'name')), x-1)
这是一个匿名函数,从 x 开始倒数,然后返回 1。它不是很有用,因为 Matlab 缺少 ?: 运算符并且不允许在匿名函数中使用 if 块,因此很难构造基本情况/递归步骤形式.
你可以通过调用 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
【讨论】:
我没有 Matlab 但你似乎错过了显而易见的:`@(x) ~x || x * feval(str2func(getfield(dbstack, 'name')), x-1) ` .无论如何+1 :) @Hugh Allen:不幸的是,Matlab 的 ||运算符将其操作数转换为逻辑 (0/1) 值,因此您建议的更简单的变体将始终评估为 1 或 0。因此获得有用的阶乘实现的扭曲。谢谢。 还有一个稍微不那么冗长的版本here。【参考方案10】:似乎 Mathematica 还允许您使用 #0
定义递归函数来表示函数本身,如下所示:
(expression[#0]) &
例如阶乘:
fac = Piecewise[1, #1 == 0, #1 * #0[#1 - 1], True] &;
这与 #i
表示第 i 个参数的表示法以及脚本是它自己的第 0 个参数的 shell 脚本约定保持一致。
【讨论】:
【参考方案11】:我认为这可能不是您想要的,但在 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
【讨论】:
【参考方案12】: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.
【讨论】:
【参考方案13】:因为我很好奇,我实际上尝试在 MATLAB 中想出一种方法来做到这一点。可以做到,但看起来有点像 Rube-Goldberg 式的:
>> 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,但这对我来说并不匿名。你能写一个函数表达式而不绑定名称“fact”、“returnOne”和“branchFcns”吗? 'fact' 和 'returnOne' 只是存储两个匿名函数句柄的变量。在函数外部,“branchFcns”只是一个传入的变量,可以调用任何东西。在函数中,输入“val”和“branchFcns”可以指定任何名称。【参考方案14】:匿名函数存在于 C++0x 和 lambda 中,它们可能是递归的,虽然我不确定匿名。
auto kek = []()kek();
【讨论】:
【参考方案15】:'看来你对匿名函数的理解是错误的,这不仅仅是关于运行时的创建,它也是关于范围的。考虑这个 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 是 Lazy 效果和空行代码一样,它是真正的字面意义,因为它与C代码:
3;
文字数字。即使我之后立即使用它,它也会消失。这就是字面量函数的意义所在,而不是在运行时创建本身。
【讨论】:
你所说的一切都不能让我相信我有什么问题。 “产生函数的表达式”并不意味着运行时创建,您所说的范围是显而易见的。我区分了匿名函数和函数文字,因为我不在乎在一些奇怪的语言中,函数文字是否引入了新的绑定,即。一个新的命名(非匿名)函数。 好吧,对不起,您只是用错了术语,函数字面量或匿名函数,顾名思义,只不过是一个无名函数。你的 JavaScript 函数是匿名的,你的 Haskell 函数是命名的。而产生函数确实的表达式意味着在运行时创建,表达式的值仅在运行时才知道。匿名函数只是从某个没有给定名称的函数创建表达式返回的函数对象,就像表达式可以返回一个可以直接使用而不是首先存储在变量中的数字对象一样。 "JavaScript 函数是匿名的...... Haskell 函数被命名"。你仍然错过了我关于不关心它是否匿名的观点。 “产生函数的表达式确实意味着在运行时创建”。我想您还没有听说过“常量表达式”或“优化编译器”。 1+1 的值通常在编译时已知。函数表达式稍微复杂一些,但在编译时评估表达式以生成“函数”(以机器代码等形式)是编译器所做的。 不,那么它不是一个表达式,这就是为什么在C中,函数实际上是由语句声明的,而不是由表达式声明的。理论上 1+1 的值在编译时是未知的,尽管一些编译器为了优化而运行它。命名函数可以递归的问题并不有趣,所有命名版本都可以引用自己,因此具有递归。世界上没有一种语言不允许在其命名函数、编译时或运行时递归,但有些语言不允许匿名(例如 PHP @Lajla:“世界上没有一种语言不允许在其命名函数中进行递归”:这是一个轻率的声明;)我至少可以说出两个。 (BASIC 和 FORTRAN 的早期版本)【参考方案16】:Clojure 可以做到这一点,因为 fn
采用了一个专门用于此目的的可选名称(该名称不会超出定义范围):
> (def fac (fn self [n] (if (< n 2) 1 (* n (self (dec n))))))
#'sandbox17083/fac
> (fac 5)
120
> self
java.lang.RuntimeException: Unable to resolve symbol: self in this context
如果恰好是尾递归,那么recur
是一种更有效的方法:
> (def fac (fn [n] (loop [count n result 1]
(if (zero? count)
result
(recur (dec count) (* result count))))))
【讨论】:
以上是关于哪些语言支持*递归*函数文字/匿名函数?的主要内容,如果未能解决你的问题,请参考以下文章