zoukankan      html  css  js  c++  java
  • Y组合子

    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,都需要给事物命名。

    ====
    更新:函数不动点在编程中的应用

    重新发明 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

  • 相关阅读:
    670. Maximum Swap
    653. Two Sum IV
    639. Decode Ways II
    636. Exclusive Time of Functions
    621. Task Scheduler
    572. Subtree of Another Tree
    554. Brick Wall
    543. Diameter of Binary Tree
    535. Encode and Decode TinyURL
    博客园自定义背景图片
  • 原文地址:https://www.cnblogs.com/lsgxeva/p/10137611.html
Copyright © 2011-2022 走看看