各语言Y组合子大比拼

最近有点无聊,突然想试试在各种语言里面实现Y组合子。不过写完之后,没想到结果完全出乎我的意料。嘛,让我们来看看不同语言里的Y组合子。

首先祭上Y组合子的定义:

Y = \lambda f. (\lambda x. f(x \, x)) \, (\lambda x. f(x \, x))

Python魔法

和众多流行的弱类型语言一样,Python支持lambda表达式但不支持延迟求解和柯里化,所以Python的写法应该也是比较有代表性的。

Y = lambda f: (lambda x: f(x(x)))((lambda x: f(lambda *args: x(x)(*args))))

可以看到,虽然有lambda加持,然而由于lambda这个单词真的好长,所以整个式子就变成了这样。另外,由于递归函数实际参数是传至右式的,所以左式并不需要传args。

JavaScript魔法

大部分如lua、php和Python并没有太大的区别。不过,时至ES6,js已经有了自己的lambda表达式——箭头函数。

Y = f => (x => f(x(x))) ((x => f(...a => x(x)(...a))))

由于无法脱离“函数调用要加括号”的苦海,于是纵使有简单的lambda写法,JS里成山的括号依旧令人无法直视。

CoffeeScript黑魔法

熟悉我的人一定知道,我个人是cs的脑残粉。cs的简洁与灵活和js(尤其是es5)真是天壤之别,函数调用可以省略括号也提供了极大的便利。

Y = (f) -> ((g) -> f (g g)) ((g) -> f (...x) -> (g g) ...x)

除了不支持柯里化导致不可避免的需要x,其余可以说是很完美了。

Haskell

那支持柯里化的语言是不是就无敌了?少年,天真了。

import Unsafe.Coerce

y :: (a -> a) -> a
y = \f -> (\x -> f (unsafeCoerce x x)) (\x -> f (unsafeCoerce x x))

由于Haskell有严格的类型检查,于是Y组合子这种“无限递归类型”的函数是无法通过类型检查的。不过,难道就没有不通过unsafeCoerce的方法嘛?想了一晚上,脑阔疼。不过作为SOO(StackOverflow Oriented)程序员我还是成功的找到了答案(原答案地址:https://stackoverflow.com/a/5885270)。

newtype Mu a = Mu (Mu a -> a)
y f = (\h -> h $ Mu h) (\x -> f . (\(Mu g) -> g) x $ x)

让我们来分析下这段代码。首先先用β归约:

y f = (\x -> f . (\(Mu g) -> g) x $ x) $ Mu (\x -> f . (\(Mu g) -> g) x $ x)

再次β归约:

y f = f . (\(Mu g) -> g) Mu (\x -> f . (\(Mu g) -> g) x $ x) $ Mu (\x -> f . (\(Mu g) -> g) x $ x))
    = f . (\x -> f . (\(Mu g) -> g) x $ x) $ Mu (\x -> f . (\(Mu g) -> g) x $ x))
    = f . y f

可以看出,这和Y组合子是等价的。而引入的Mu也同样起到了防止递归类型检查的效果。

Lisp(Scheme)

Haskell那边栽跟头主要是因为类型检查,那Lisp呢?(出处

(define (Y f) (lambda (f)
      ((lambda (x)
         (f (lambda (arg) ((x x) arg))))
       (lambda (x)
         (f (lambda (arg) ((x x) arg)))))))

Scheme还是很纯粹很好看的。

Java

Java作为一个有类型检查,lambda是语法糖的语言,想写Y组合子必然是痛苦的。我从Gists上找了一个:

package test;

import java.math.BigInteger;
import java.util.function.Function;

public class YCombinatorFactorial<T> {
    private interface Improver<T> {
        Function<T,T> apply( Improver<T> f ) ;
    }

    private Function<T,T> Y( final Function<Function<T,T>,Function<T,T>> r ) {
        return ((Improver<T>)f -> f.apply( f )).apply( f -> r.apply( x -> f.apply( f ).apply( x ) ) ) ;
    }

    public static void main( String[] args ) {
        YCombinatorFactorial<BigInteger> yf = new YCombinatorFactorial<BigInteger>() ;
        BigInteger result = yf.Y(
                f -> n -> n.equals( BigInteger.ZERO ) ?
                        BigInteger.ONE :
                        n.multiply( f.apply(n.subtract( BigInteger.ONE ) ) ) ).apply( BigInteger.valueOf( 100 ) ) ;
        System.out.println( result );
    }
}

不过老实说,引入了lambda之后确实简洁了不少。这里简单解释一下。首先还是做β变换:

((Improver<T>)f -> f.apply( f )).apply( f -> r.apply( x -> f.apply( f ).apply( x ) ) );
(f -> r.apply( x -> f.apply( f ).apply( x ) )).apply( f -> r.apply( x -> f.apply( f ).apply( x ) ) );

其实也不用过多解释了,这样的形式已经和Y组合子的定义一致了。

WolframScript真魔法

由于纯函数支持类scala的占位符_,所以ws写出来真是又简洁又看不懂。(代码来自互联网)

yCombinator@f_ := #@# &[Function[n, f[#@#]@n] &];

总结

果然还是喜欢CoffeeScript啊……

分享到

KAAAsS

喜欢二次元的程序员,喜欢发发教程,或者偶尔开坑。(←然而并不打算填)

相关日志

  1. 没有图片
  2. 没有图片
  3. 没有图片
  4. 没有图片
  5. 没有图片

评论

  1. (null) 2020.08.30 12:50下午

    python 其实部分支持惰性求值……比如 map 函数。

    另外有一个疑惑……为什么按照原版的 Y 组合子写出来的函数会在定义 fac 函数的时候报错
    “`python
    fac1 = lambda f: lambda x: 1 if x==0 else x*f(x-1)
    w = lambda f: f(f)
    Y = lambda f: w(lambda x: f(w(x)))
    fac = Y(fac1)
    “`
    但是本文中的就不会
    `Y = lambda f: (lambda x: f(x(x)))((lambda x: f(lambda *args: x(x)(*args))))`

    • KAAAsS 2020.08.30 1:58下午

      Python map的是generator,在流式处理上确实能发挥一定程度的惰性,并不是真正意义上的语言层面惰性求值(参考Haskell的WHNF)。
      关于报错,由于Python没有惰性求值,因此需要显式给出参数:w = lambda f: lambda *args: f(f)(*args)。

  2. (null) 2020.09.05 3:11下午

    w = lambda f: f(f)
    fac1 = lambda f: lambda x: 1 if not x else x*f(x-1)

    Y = lambda f: w(lambda x: f(lambda *args: w(x)(*args)))
    fac = Y(fac1)
    = w(lambda x: fac1(lambda *args: w(x)(*args)))
    = (lambda x: fac1(lambda *args: w(x)(*args)))(lambda x: fac1(lambda *args: w(x)(*args)))
    = fac1(lambda *args: w(lambda x: fac1(lambda *args: w(x)(*args)))(*args))
    = lambda x: 1 if not x else x*(lambda *args: w(lambda x: fac1(lambda *args: w(x)(*args)))(*args))(x-1)
    = lambda x: 1 if not x else x*w(lambda x: fac1(lambda *args: w(x)(*args)))(x-1)

    Y = lambda f: w(lambda x: f(w(x)))
    fac = Y(fac1)
    = w(lambda x: fac1(w(x)))
    = (lambda x: fac1(w(x)))(lambda x: fac1(w(x)))
    = fac1(w(lambda x: fac1(w(x))))
    = lambda x: 1 if not x else x*w(lambda x: fac1(w(x)))(x-1)

    上面是我的手算过程。算到最后一个等号的表现与第一个等号基本一致,所以我觉得我大概没算错。

    问题是,上式和下式看起来样子都差不多……都包含了完整的自身引用,但是为什么前面的能跑通,而后面的不行?

    求赐教。

    • KAAAsS 2020.09.05 6:59下午

      你的推导没错。但是对于能继续求值的情形,python 会继续进行求值。因此这个求值过程将会不停止的执行下去。这种情况在提供参数的情况下也是一样的,比如对 Y(fac1)(n),python照样会持续求 Y(fac1)。

      友链交换完全欢迎,不过还请确保网站能正常访问并且不会长时间不更新(比如1年不变)~

  3. (null) 2020.09.05 3:13下午

    另外大佬可以交换友链吗

  4. (null) 2020.09.05 10:39下午

    上下两式继续求值都是继续展开 w……这样看起来上下两式也没什么不同……那为什么上式能跑通而下式不可以呢?

    博客不至于不更新,只是敝站才建好不久,文章还不多……并且 css 还等着我大改。(具体内容我放留言板里吧)

    邮件通知评论之类的功能你用的是什么实现?

    • KAAAsS 2020.09.05 10:57下午

      python在调用时会保证参数全部求值。上式 w(x)(*args) 不可求值,其余lambda式都有外层变量无法求值,因此停止计算。
      后式中 w(lambda x: fac1(w(x))) 部分不包含外层变量,因此会反复对w与lambda进行求值。

      邮件评论使用的是插件Comment Email Reply。但是原插件老旧,我已经多次修改其源码,建议使用其他插件。

在此评论中不能使用 HTML 标签。