Y组合子

Y组合子的用处

作者:王霄池
链接:https://www.zhihu.com/question/21099081/answer/18830200
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Y组合子的用处是使得 lambda 表达式不需要名字

如你所说,阶乘函数可以这样定义:

let F = lambda n. n==0 ? 1 : n*(F n-1)

当我们需要调用的时候,我们只需要这样写就可以了:

F 4

但你有没有想过,如果我们没有 let 这个关键字怎么办?没有 let,就不能对一个 lambda 表达式命名。实际上,在 lambda 演算里确实没有 let,换句话说,let 只是个语法糖,让我们写起来更加舒适而已。没有 let,并没有对lambda表达式造成什么实质性的伤害。

数学家们推崇:

如无必要,勿增实体

是的。所以,你不能对任何lambda表达式命名。这就像你中了一个沉默魔法一样。

我们先来看看如果没有递归,无名 lambda 表达式是如何使用的。
我们来写一个求平方的lambda:

lambda x. x * x

这个lambda是无名的。如果要调用,我们只能这么调用:

( lambda x. x * x ) 3

结果自然是返回 9 了。

看来没有名字,lambda 世界还是可以正常运转的。且慢,我们不要忘记递归。递归函数,似乎真的是个问题——如果没有名字,自身如何调用自身?其实也不是啥大问题。不过,要解决这个问题,我们先假设我们可以使用名字。别担心,这只是前进途中的曲折。最后,我们会去掉名字的,大家先不要着急。

我们以阶乘函数为例,先看看我们现阶段的成果:

let F = lambda n. n==0 ? 1 : n*(F n-1)

首先我们先设法消除掉 lambda 函数体中的函数名称(对不起,一激动就用上了函数这个说法,如果你不知道什么叫函数,那么你就可以理解为函数就是 lambda,二者是等同的)。方法就是将函数作为参数传进去。

let F = lambda f. lambda n. n==0 ? 1 : n*((f f) (n-1))

这个函数的接受一个参数,返回一个函数,这个返回的函数才是真正做计算的阶乘函数。
调用此函数的方法如下:

F F 4

将会返回24。

接下来的一步将是至关重要的。我们现在就抛弃let关键字。我们将F的名字换成F的定义,于是调用阶乘函数的的方式将变成如下的样子:

( lambda f. lambda n. n==0 ? 1 : n*((f f) (n-1)) ) ( lambda f. lambda n. n==0 ? 1 : n*((f f) (n-1)) ) 4

看到了没,这里的所有 lambda 都没有名字,不过,这丝毫没有影响 lambda 表达式的威力。

如果你看到这里,就会发现,我们可以用类似的方法定义所有的递归函数,而用不着Y组合子。是的。你是对的。上面这种方法叫做穷人的Y组合子。但Y组合子的作用就是提供了一个通用的方法来定义递归函数。

让我们来看一下Y的定义:

lambda f. (lambda x. (f(x x)) lambda x. (f(x x)))

要讲清楚Y的来龙去脉,可是非常难(大家可以去看我的博文重新发明 Y 组合子 Python 版)。事实上,连发现它的哈斯卡大神也感慨不已,觉得自己捡了个大便宜,还因此将Y纹在了自己的胳膊上。我现在就只讲Y的用处了。

我们用Y来定义一下递归函数

let F = Y ( lambda f. lambda n. n==0 ? 1 : n*(f n-1) )

大家有没有发现,定义变得比以前的特定方法更加优美了。在之前的特定方法中 f 需要应用于自身,但现在,f 是由 Y 提供的,是一个纯阶乘函数。

不只是定义更加优雅,连调用也像有名字的lambda一样优美了。我们现在就优雅的调用阶乘函数:

F 4

而去掉F的名字,我们有:

( Y ( lambda f. lambda n. n==0 ? 1 : n*(f n-1) ) ) 4

再去掉Y的名字:

( ( lambda f. (lambda x. (f(x x)) lambda x. (f(x x))) )
( lambda f. lambda n. n==0 ? 1 : n*(f n-1) )
) 4

看,这里没有任何名字,但我们定义并且调用了阶乘函数。再次强调一下,阶乘函数是个递归函数哦。

任何一阶递归函数都可以用Y来定义,就像我们定义阶乘函数那样:

Y (lambda f. < 真正的函数体,在内部用f指代自身 >)

多说一句,可以在 JavaScript 中实现Y算子,如果用上 CoffeScript 提供的语法糖,将非常优雅(这里我原写错了,感谢

):

Y = (g) ->
h = (f) ->
g(n) -> f(f) n
h h

Y算子真是人见人爱。但除了证明lambda只需要alpha/beta/eta三条规则而不需要命名之外,它主要用自身的优美供大家感叹。在真实的世界中,不论是数学家,还是函数式编程的 coder,都需要给事物命名。

====
更新:函数不动点在编程中的应用 http://www.lfcs.inf.ed.ac.uk/reports/97/ECS-LFCS-97-375/ECS-LFCS-97-375.pdf

重新发明 Y 组合子 JavaScript(ES6) 版

来源 http://picasso250.github.io/2015/03/31/reinvent-y.html

2015-03-31

关于Y组合子的来龙去脉,我读过几篇介绍的文章,相比之下,还是王垠大神的著作 最易懂。但他原来所有的语言是scheme,我写一个 JS 版的,来帮助大家理解吧。

我们首先来看看如何实现递归。

lambda演算的语法非常简洁,一言以蔽之:

x | t t | λx.t

其中xx表示变量,t tt t 表示调用, λx.tλx.t 表示函数定义。

首先我们来定义一个阶乘函数,然后调用它。

fact = n => n == 1 ? 1 : n * fact(n-1)
fact(5)

lambda演算中不可以这么简单的定义阶乘函数,是因为它没有 = 赋值符号 。

现在我们看到在lambda定义中,存在fact的名字,如果我们想要无名的调用它,是不行的。如下

(n => n == 1 ? 1 : n * fact(n-1))(5) # there is still `fact` name

我们想要将名字消去,如何消去一个函数的名字呢?

首先,没有名字是无法定义一个递归函数的。

那么,我们不禁要问了,哪里可以对事物命名呢?

对了,将之变为参数,因为参数是可以随意命名的。

fact = (f, n) => n == 1 ? 1 : n * f(f, n-1)
fact(fact, 5)

嗯,很好,看起来不错。不过,要记住在 lambda 演算里面,函数只能有一个参数,所以我们稍微做一下变化。

fact = f => n => n == 1 ? 1 : n * f(f)(n-1)
fact(fact)(5)

你可能会说我在做无用功,别过早下结论,我们只需要将 fact 代入,就得到了完美的匿名函数调用。

(f => n => n == 1 ? 1 : n * f(f)(n-1)) (f => n => n == 1 ? 1 : n * f(f)(n-1)) (5)

看,我们成功了,这一坨代码,是完全可以运行的哦。这个叫做 穷人的Y组合子。可以用,但是不通用,你要针对每个具体函数改造。

于是我们继续改造。我们将把通用的模式提取出来,这个过程叫做 抽象

首先我们看到了 f(f) 两次, fact(fact) 一次,这种pattern重复了3次,根据 DRY 原则,我们可以这么做

w = f => f(f)
w(fact) (5) # short version
w (f => n => n == 1 ? 1 : n * f(f)(n-1)) (5) # longer version

现在,我们就只有一个重复的模式了,那就是 f(f) 。但是因为它在函数内部(也就是在业务逻辑内部),我们要先把它解耦出来。也就是 factor out。

我们从 f => n => n == 1 ? 1 : n * f(f)(n-1) 开始

f =>
n => n == 1 ? 1 : n * f(f)(n-1)

我们令 g=f(f) ,然后 可以变成

f =>
(g => n => n == 1 ? 1 : n * g(n-1)) ( f(f) )

当然, f(f) 在call by value 时会导致栈溢出,所以我们就 ηη 化一下

f =>
(g => n => n == 1 ? 1 : n * g(n-1)) ( v => f(f)(v) )

我们看到了 g => n => n == 1 ? 1 : n * g(n-1) 这个就是我们梦寐以求的阶乘函数的定义啊。

我们将这个(阶乘函数的定义)提取出来(再一次的factor out),将之命名为 fact0(更接近本质的fact)。上面的可以改写成。

( fact0 => f =>
fact0 ( v => f(f)(v) )
) ( g => n => n == 1 ? 1 : n * g(n-1) )

不要忘记最初的w,那么如下:

w(
(fact0 => f => fact0 ( v => f(f)(v) ))
(g => n => n == 1 ? 1 : n * g(n-1))
)(5)

很自然我们会再一次把阶乘函数的定义factor out出来,当然,fact0 => f => fact0 ( v=>f(f)(v) )中的fact0参数我们也会换成其他的名字,比如 s,而那个fact0的实参,那一大坨更加本质的定义我们也会抽象成一个参数,h

(h =>
w( (s => f => s ( v => f(f)(v) )) (h))
)
(g => n => n == 1 ? 1 : n * g(n-1)) (5)

好,大功告成,上面的那个括号里面的就是Y了。我们将之单独拿出来看。

(h =>
w(
(s => f => s ( v => f(f)(v) )) (h)
)
)

最中间一行的 h 可以apply一下,也就是化简:

(h =>
w(
(f => h ( v => f(f)(v) ))
)
)

当然, w这个名字也可以去除

(h =>
(f => h ( v => f(f)(v) ))
(f => h ( v => f(f)(v) ))
)

这就是最后的结果了。

名调用中,可以这么写:

λf.(λu.u u)(λx.f(x x))λf.(λu.u u)(λx.f(x x))

或者使用更经典的形式

λf.(λx.f(x x))(λx.f(x x))

浅谈Y组合子

来源 http://jjyy.guru/y-combinator

这篇文章希望能够通俗地讲清楚Y组合子,如果对lambda演算感兴趣的同学可以看看最后的相关资料

在lambda中,如果我们想要递归,以斐波那契数列为例,可以这样:

let power = lambda n. IF_Else n==0 1 n*power(n-1)

然而,在“纯”lambda演算中,是没有let关键字的,但我们可以暂时忘记这件事。我们需要换个方法进行递归,如果直接的递归不可行,那么我们可以尝试间接的。很容易能想到通过参数把自己传给自己:

let P = lambda self n. If_Else n==0 1 n*self(self, n-1)
P(P, 3)

如果每次递归都要这么写,就显得很不优雅。我们要想一个办法,能够通用的把自己传给自己。就像上面一样。我们试着构造一下,把斐波那契数列的逻辑替换为任意函数:

let gen = lambda self. AnyFunction(self(self))
gen(gen)

尝试写出斐波那契数列的AnyFunction实现:

let AnyFunction = lambda self n. If_Else n==0 1 n*self(n-1)

经过展开之后,发现任何函数只要在AnyFunction那个位置,经过上面的代码之后,都能够实现递归。

其中gen(gen)展开如下:

gen(gen) => AnyFunction(gen(gen))

可能你会疑问,gen(gen)为什么能够表达自己呢?因为gen(gen)展开为AnyFunction(gen(gen)),它能够返回AnyFunction自身,这就得到自己了。并且这时会把这个gen(gen)再传给AnyFunction。而gen(gen)不求值时是不展开的,因此gen(gen)没有被调用时,没有任何作用,但是一旦AnyFunction内部调用了传进来的gen(gen),那么就进行求值再次得到“自己”。通俗来讲,与其说gen(gen)是自身,还不如说这是一个把能够得到自己,并且把gen(gen)再次传入的函数。

在理解这个机制之后,通用的递归函数已经到手。封装一下就轻而易举了,这就是传说中的Y组合子:

let Y = lambda f.
let gen = lambda self. f(self(self))
gen(gen)

再把let去掉可得到Y的定义:

lambda f. (lambda x. (f(x x)) lambda x. (f(x x)))

接下来可以试着使用一下:

( ( lambda f. (lambda x. (f(x x)) lambda x. (f(x x))) )
( lambda f. lambda n. n==0 ? 1 : n*(f n-1) )
) 4

看,完美!证明了lambda只需要alpha/beta/eta三条规则而不需要命名。


相关资料,从易到难排序

其他相关资料

================== End

Y组合子的更多相关文章

  1. Lambda演算 - 简述Y组合子的作用

    Y组合子:\f.(\x.f(xx))(\x.f(xx)),接受一个函数,返回一个高阶函数 Y组合子用于生成匿名递归函数. 什么叫匿名递归函数,考虑以下C语言递归函数 int sum(int n) { ...

  2. 大到可以小说的Y组合子(一)

    问:上回乱扯淡了一通,这回该讲正题了吧. 答:OK. 先来列举一些我参考过,并从中受到启发的文章. (1.)老赵的一篇文章:使用Lambda表达式编写递归函数 (2.)装配脑袋的两篇文章:VS2008 ...

  3. 大到可以小说的Y组合子(三)

    答:关于Fix的问题你fix了吗? 问:慢着,让我想想,上次留下个什么问题来着?是说我们有了一个求不动点的函数Fix,但Fix却是显式递归的,是吧? 答:有劳你还记的这个问题. 问:Fix的参与背离了 ...

  4. 大到可以小说的Y组合子(二)

    问:上一回,你在最后曾提到"抽象性不足",这话怎么说? 答:试想,如果现在需要实现一个其它的递归(比如:Fibonacci),就必须把之前的模式从头套一遍,然后通过fib_make ...

  5. 大到可以小说的Y组合子(零)

    问:啊!我想要一个匿名的递归… 答:Y(音同Why)… … … 问:作为一位命令式语言的使用者,为什么会突然折腾起Y组合子呢? 答:的确,这事儿要从很久以前的几次搁浅开始说起…上学的时候,从来没有接触 ...

  6. Racket中使用Y组合子

    关于Y组合子,网上已经介绍很多了,其作用主要是解决匿名lambda的递归调用自己. 首先我们来看直观的递归lambda定义, 假设要定义阶乘的lambda表达,C#中需要这么定义 Func<in ...

  7. 简单易懂的程序语言入门小册子(4):基于文本替换的解释器,递归,如何构造递归函数,Y组合子

    递归.哦,递归. 递归在计算机科学中的重要性不言而喻. 递归就像女人,即令人烦恼,又无法抛弃. 先上个例子,这个例子里的函数double输入一个非负整数$n$,输出$2n$. \[ {double} ...

  8. 用不动点组合子解递归(python实现)

    不动点组合子 Y = λf. (λx. f (x x)) (λx. f (x x)) θ = (λx. λy. (y(x x y))) (λx.λy.(y(x x y))) Y f = f (Y f) ...

  9. [学习] 从 函数式编程 到 lambda演算 到 函数的本质 到 组合子逻辑

    函数式编程 阮一峰 <函数式编程初探>,阮一峰是<黑客与画家>的译者. wiki <函数编程语言> 一本好书,<计算机程序的构造与解释>有讲到schem ...

随机推荐

  1. Android之网络摄像头

    实现的功能就是两个手机在一个局域网内可以互相观看对方的摄像头图像,当然如果都是连接公网那么就能远程互看了,,,,和视频聊天差不多,,不过没有声音,,,,,,,, 源码是在网上找的(具体地址忘了,如有侵 ...

  2. Nginx 服务器的安装部署(CentOS系统)

    1.准备安装环境yum -y install gcc gcc-c++ automake pcre pcre-devel zlib zlib-devel open openssl-develgcc编译器 ...

  3. 大数据入门第二十一天——scala入门(一)并发编程Actor

    注:我们现在学的Scala Actor是scala 2.10.x版本及以前版本的Actor. Scala在2.11.x版本中将Akka加入其中,作为其默认的Actor,老版本的Actor已经废弃 一. ...

  4. Spark(Python) 从内存中建立 RDD 的例子

    Spark(Python) 从内存中建立 RDD 的例子: myData = ["Alice","Carlos","Frank"," ...

  5. 编程语法分析之“优先级”和“结合律”

    上节<编程语法分析之从表达式说起>中说到表达式,他的主要作用就是返回一个值!那这个值具体是多少,就要看表达式的整个运算过程.要理解表达式的运算过程就必须了解"优先级"和 ...

  6. 【php增删改查实例】第六节 - 部门管理模块(开始)

    sql建表语句详见:https://www.jianshu.com/p/c88077ed9073 1.新建html模板 新建一个空白的txt文档,然后把后缀名改为.html 用任意一个编辑器打开,比如 ...

  7. STM32一键下载电路设计原理

    先放原理图(补充:图中的BOOT0通过10K的电阻接到地),再解释为什么这么设计: STM32启动方式:BOOT0和 BOOT1用于设置 STM32的启动方式 ,见下表: BOOT0=1,BOOT1= ...

  8. Django实现websocket完成实时通讯、聊天室、在线客服等

    一 什么是Websocket WebSocket是一种在单个TCP连接上进行全双工通信的协议 WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据.在WebS ...

  9. 第一次软件工程作业(One who wants to wear the crown, Bears the crown.)

    回顾你过去将近3年的学习经历 1.当初报考的时候,是真正的喜欢计算机这个专业吗? 报考时对于计算机专业只能说不讨厌,也可以认为对其没有任何的感觉. 有一个比我自己还注意我未来的老妈,我的报考只能通过一 ...

  10. k8s-rabbitmq-(一)集群部署

    K8S版本:1.10.1 rabbitmq版本:3.6.14 从来没用过这个软件,所以对里面很多术语看不太懂.最后通过https://www.kubernetes.org.cn/2629.html 大 ...