什么?函数是一等公民!

来源:互联网 发布:如何开通淘宝达人账号 编辑:程序博客网 时间:2024/05/01 19:54

最近在看λ演算,看到Church 数的时候很迷惑,不就是一堆函数吗,Church为什么要搞个什么λ记法。后来发现,λ记法固然有它的优势(在替换时便于分清替换的是哪个部分,函数还是自变量),但不用它似乎更直观一些。

Church 创造了一个只有单参数的匿名函数的演算系统。首要的问题,数怎么表示呢?Church 想到了用函数的幂次。

Church Numerals

还得解决三个问题:

一、x是什么?没关系,只要大家都用同一个x就行。

二、Church 的这个系统没有函数名啊?!没关系,我们把f 当作参数传进去就行。

Church Numerals

三、如何转换成单参数?构造一系列中间函数,每次固定一个参数不就行了,比如,z = f(x, y) 是一个(X×Y)Z 的函数,先固定x,返回一个YZ 的函数fix_x(y) = f(x, y),那么f(x, y) = (fix_x(y))(x)(不知这么理解对不对,虽然思想很简单,但形式化却不容易)。这就是传说中的Currying 化。

数是可以随便定义啦,但得符合数的基本性质啊,比如:

  • 四则运算

从加法开始吧,加法函数应该满足:

    plus(Cm(f, x), Cn(f, x)) = f(m+n)(x)

    想到函数复合的性质f(m+n)(x) = fm(fn(x)),即把fm(x)中的xfn(x)替换。

    plus

    例,

    2+3

    乘法,还是利用函数复合的性质f(m×n)(x) = (fm)n(x),即把fm(x)中的ffn(x)替换。

      从这就可以看出Church λ记法的好处了:把函数和自变量分开表示,便于替换。

      mult

      例,

      2×3

       

      现在再回头看λ记法版的Church数就直观多了。一个λ表达式就是一个匿名函数,分为两部分,参数和函数体,参数前加λ,后跟点。

      对比

      加法和乘法也是对应的:

      加法也对应

      乘法也对应

      • 布尔运算

      布尔世界是个封闭的世界,这个世界里的任何运算得到的结果要么真,要么假。Church是这样定义真假的:

      true(x, y) x

      false(x, y) y

      你注意到没?true(x,y) 返回x,而false(x,y) 返回y,那么对任意Church布尔变量bb(x,y) 等价于if b then x else y

      当然最简单的运算是取非,

      not

      如果var(x,y)为真,返回假;反之,返回真。

      与或运算就不那么直观了,但利用“b(a,c) 等价于if b then a else c”这一性质也可以很快写出来。

      M与上N:如果M为真,整个表达式的真假就看N了;如果M为假,整个表达式必然为假(等价于M)。

      and

      例,and(true(x,y), true(x, y)) = true(true(x,y), false(x, y)) = true(x, y)

      M或上N:如果M为真,整个表达式必为真(换成M也一样);如果M为假,整个表达式的真假就看N了。

      or

      例,or(false(x,y), true(x, y)) = false(true(x,y), true(x, y)) = true(x, y)

      再来看看对应的λ记法,

      与或非

      • 递归

       

      如果仅仅是做一些加减乘除或者布尔运算,何必搞这么麻烦。λ演算最精彩的部分在于它能不用绑定函数名实现递归。

      以阶乘为例,

      阶乘

       

      我们知道λ演算中是没有函数名的,那么怎么实现递归呢?

      考虑一般的递归函数f = G(f),以阶乘为例,

       

      我们要做的就好比对一个隐式方程就显式解(比如x = sin(x))。

      也可看成求G的不动点f*,即f* = G(f*)。而f*应该与G有关,假设f* = T(G),所以我们要找的是这样一种变换:

      不动点

       

      对阶乘来说,这个显式解就是f(n) = n!,因为,

       

      显然不可能所有的递归函数都有显式解,但起码通过T(G)我们找到一种迭代的求解方式。

      下面我们先找出T(阶乘),再将它一般化。

      我们先尝试把函数体中的递归函数名分离出来,并作为参数传入函数体。

       

      aux-fact

       

      运行的时候这么调用,

       

      验证一下:

       

      这不扯鬼淡吗!别急,奇迹马上出现。

      Currying

      step 1

      然后设法把和阶乘有关的结构分离出来,

       

      step 2

      step 3

      step 4

      step 5

      其中,

      Y & G

      这里的T就是传说中的Y Combinator!验证一下,

      example

      咦,只是用到不动点的性质,并不需要其定义!很多文章认为这是在用Y实现Y。但我认为这不过是挂了羊头的递归版本。你把T(G)全部替换成factorial就知道了。事情的真相是这样的:

      factorial

      咦,原来是一样的!It's my fault.

      注意完成一次代人后并不急于继续代人,否则将陷入无穷递归,这就是所谓的惰性求值吧。

       

      解释一下f[]等价于f.call,不信?!

       

      (未完)

      另外,也揭示了很重要却容易被忽视的一点,写递归,终止条件很重要。

      P.S. 有人用JavaScript 写了个λ记法版的加法,蛮有意思。

      P.S.S.g9老大以前写过一个关于λ演算的系列,。就是这个系列让我知道了g9这位牛人。

      P.S.S.S. pluskid同学的the Y Combinator写得相当赞,能把理论直接用于实践的人不多啊!

      P.S.S.S.S 王垠推荐的Scheme版the Y Combinator明了地推导了Y组合子的前世今生。

      P.S.S.S.S.S 有人谈了Y Combinator在Ruby上的应用。