停机问题与Y Combinator(二)

Y Combinator

停机问题在上一篇文章已经了解过了,这篇文章让我们来了解一下Y组合子。其实说了解也并不恰当,只能说是我们用Scheme语言一步一步的推导出了Y组合子,至于了解,留给下一篇文章。上一篇文章中提过我是看《The Little Schemer》才写的这系列文章(实际上是因为组里人让技术分享,反正都要准备,不如记下来),但是实际上这篇文章中的推导思路和Dan的《The Little Schemer》中的思路并不一样。我个人认为我的思路更好理解一些,但是也是在Dan书的启发下才想出来的。在下一篇文章中还会有一种全新的思路的去推导Y组合子,会让人感觉豁然开朗。

来思考这样一个问题:如果没有define,也就是说我们没法给函数起字,我们怎么写一个递归程序?(你怎么这么多问题?你就是想装B)回顾之前的length函数:

(define length
  (lambda (l)
    (cond ((null? l) 0)
          (else (add1 (length (cdr l)))))))

既然没有名字了,那怎么着。那把用到length的地方都换成lambda表达式吧,这是个很直接的想法!但是这么写有问题啊,用lambda表达式替换length,新的lambda表达式里面还有length啊,这么写根本写不完啊!怎么办?那这样吧,新的lambda表达式里面把length替换成之前写的eternity(停机问题与Y Combinator(一))吧,这样我们的程序对于空的list或者只含有一个元素的list是可以返回正确的结果的。代码就是这个样子:

;;; length <= 1
(lambda (l)
  (cond
   ((null? l) 0)
   (else
    (add1
     ((lambda (l)
        (cond
         ((null? l) 0)
         (else
          (add1
           (eternity (cdr l))))))
      (cdr l))))))

所以长度小于等于2的?

;;; length <= 2
(lambda (l)
  (cond
   ((null? l) 0)
   (else
    (add1
     ((lambda (l)
        (cond
         ((null? l) 0)
         (else
          (add1
           ((lambda (l)
              (cond
               ((null? l) 0)
               (else
                (add1
                 (eternity (cdr l))))))
            (cdr l))))))
      (cdr l))))))

我们写了这么长的代码只支持两个元素,这咋办?要不写个支持一千个元素的,然后多于一千个就提示个错误?强行纯手工递归,66666!要是这么写这个函数是写不完的,不过我们发现这个东西有很多地方都是重复的,可不可以抽象一下?我们仔细看一下之前的两个函数,有一块代码好像出现了两次,根据DRY原则,我们应该抽出来啊。要不违反原则,那怎么行啊:)。开个玩笑,那些原则看看笑笑就过了,那些写X原则X模式的书怎么看都像传销手册。好,不扯别的,我们来看是哪块代码重复了。

(lambda (l)
  (cond
   ((null? l) 0)
   (else
    (add1
     (eternity (cdr l))))))

看看这块代码能干什么?好像是对于空的list,它能够正确计算空的list的长度。再看能过计算长度为1的代码,好像是把原来length的地方替换成长度为0的就可以了。长度为2的那个函数,就是把length替换成长度为1那个的函数,是这样吧,那我们加个参数?

所以能够正确计算长度为0的list的函数?

((lambda (length)
   (lambda (l)
     (cond
      ((null? l) 0)
      (else (add1 (length (cdr l)))))))
 eternity)

好,写一下最大长度为1和2的函数吧:

;;; 长度 <= 1
((lambda (length)
   (lambda (l)
     (cond
      ((null? l) 0)
      (else (add1 (length (cdr l)))))))
 ((lambda (length)
    (lambda (l)
      (cond
       ((null? l) 0)
       (else (add1 (length (cdr l)))))))
  eternity))
  
;;; 长度 <= 2
((lambda (length)
   (lambda (l)
     (cond
      ((null? l) 0)
      (else (add1 (length (cdr l)))))))
 ((lambda (length)
    (lambda (l)
      (cond
       ((null? l) 0)
       (else (add1 (length (cdr l)))))))
  ((lambda (length)
     (lambda (l)
       (cond
        ((null? l) 0)
        (else (add1 (length (cdr l)))))))
   eternity)))

这个就是按照上面的思路的写出的函数,仔细看看是那么回事吧。我们发现上面好像还是有那么一坨,那可是重复了一遍又一遍啊!所以加个参数,然后把那一坨传进去吧。这次一次性的都写出来,不bb直接上代码:

;;; 计算长度为0的
((lambda (mk-length)
   (mk-length eternity))
 (lambda (length)
   (lambda (l)
     (cond
      ((null? l) 0)
      (else (add1 (length (cdr l))))))))
      
;;; 所以计算为1的?
((lambda (mk-length)
   (mk-length
    (mk-length eternity)))
 (lambda (length)
   (lambda (l)
     (cond
      ((null? l) 0)
      (else (add1 (length (cdr l))))))))

;;; 计算为2的,哎!已经简单到我忍不住想写长度为3的了。
((lambda (mk-length)
   (mk-length
    (mk-length
     (mk-length eternity))))
 (lambda (length)
   (lambda (l)
     (cond
      ((null? l) 0)
      (else (add1 (length (cdr l))))))))

看起来已经比最开始的版本简洁多了,写个千八百遍的好像不成问题。但是作为超哥的同事,都是追求完美的人,这不能忍啊!思考个问题?我们为什么要把eternity传进去啊,那个好像是我们随便决定的,我们发现每调用一次mk-length好像就是一个可以计算多一个元素的函数。那既然这样,我们干脆做一个猜想:把mk-length作为初始的参数传进去,然后在length处,自己调用一下自己,好像就可以递归了。既然有了猜想,那就试试吧!

;;; 这是函数
((lambda (mk-length)
   (mk-length mk-length))
 (lambda (length)
   (lambda (l)
     (cond
      ((null? l) 0)
      (else (add1 ((length length)
                   (cdr l))))))))

;;; and我们做个测试
(((lambda (mk-length)
   (mk-length mk-length))
 (lambda (length)
   (lambda (l)
     (cond
      ((null? l) 0)
      (else (add1 ((length length)
                   (cdr l))))))))
 (list 1 2 3 4 5 6))

;;; and纯lambda版
((lambda (mk-length)
   (mk-length mk-length))
 (lambda (length)
   (lambda (l)
     (cond
      ((null? l) 0)
      (else
       ((lambda (x)
          (+ x 1))
        ((length length)
         (cdr l))))))))

测试的代码返回了6!(我真的没作弊,之前咱们最多就写到2)卧槽成功了,真的成功了!!!难道永动机是存在的?能量守恒定律是错的?YY的有点多。这个猜想多么重要,所以说学习在于培养直觉,三更灯火五更鸡的,除了感动了自己并没有什么卵用。

所以Y组合子是啥?别着急,还没粗线呢。我们把(length length)的调用作为一个参数传进来,然后Currying一下,就是下面这样:

;;; 把(length length)作为一个参数

((lambda (mk-length)
   (mk-length mk-length))
 (lambda (length)
   (lambda (l)
     ((lambda (function l1)
        (cond
         ((null? l1) 0)
         (else (add1 (function (cdr l1))))))
      (length length) l))))

;;; Currying 不懂Currying没关系,只要知道这两个是等价就可以
((lambda (mk-length)
   (mk-length mk-length))
 (lambda (length)
   (lambda (l)
     (((lambda (function)
         (lambda (l)
           (cond
            ((null? l) 0)
            (else (add1 (function (cdr l)))))))
       (length length)) l))))

在上面的代码中,你把形参function换做length,是不是有一段代码很是眼熟?是吧?提出来,作为一个参数传进去。

((lambda (le)
   ((lambda (mk-length)
      (mk-length mk-length))
    (lambda (length)
      (lambda (x)
        ((le
          (length length)) x)))))
 (lambda (function)
   (lambda (l)
     (cond
      ((null? l) 0)
      (else (add1 (function (cdr l))))))))

看看上面的调用,参数也就是下半部分,好像就是我们写的length。而操作符即上半部分,好像就是一个类似mk-length的东西,仔细看看是不是?

然而这个操作符有另一个名字——Y Combinator !

(define Y
  (lambda (f)
    ((lambda (g)
       (g g))
     (lambda (h)
       (lambda (x)
         ((f (h h)) x))))))

下集预告����

前面说了这篇主要讲用Scheme语言一步一步推导出Y组合子,既然已经推导出了Y组合子,本着单一职责原则,这篇文章到此就结束了。有这么几个问题需要思考一下:

  • 我们虽然推导出了Y组合子,但是它究竟干了点啥?为什么就能递归了?
  • 如果看过λ演算版的Y组合子,你会发现和我们推导出的并不一样,这是为什么?
  • 写Y组合子就写Y组合子吧,为什么要提停机问题?

上面的问题在下一篇文章中都会得到答案,还会给大家讲讲写递归程序的终极要义,敬请期待:)。(有语法高亮了)

你可能感兴趣的:(停机问题与Y Combinator(二))