在NewLisp中實現匿名函數的遞歸

匿名函數在不少語言中的表現形式大概以下:編程

(lambda (n)
  (* (+ n 1) (- n 1)))

只有參數列表和函數體,而沒有名字。在大部分狀況下沒問題,可是一旦須要用到遞歸的話,就有點麻煩了,由於不知道如何去遞歸的調用一個匿名函數。app


在學術界中有一些解決這個問題的辦法,其中一個就是Y組合子,可是那個太繁瑣,並且難以經過宏自動將一個lambda變成可遞歸形式,沒什麼好處。編程語言

根據歷史經驗,目前比較好的辦法,就是實現一個操做符,匿名函數經過這個操做符來調用自身:函數

(lambda (n) ... (this (- n 1))) 或者是 (lambda (n) ... (lambda (- n 1)))測試

第一種是用this或其餘東西來表示當前匿名函數自己,直接調用就能夠遞歸。第二種是和有名函數同樣,用和定義匿名函數同樣的操做符來調用自身。this

然而第二種不實際,由於這樣會形成混亂,好比須要嵌套lambda時,並且其語義也不對。spa

因此此文主要圍繞第一種方式:實現讓this指向當前匿名函數,從而能夠遞歸調用自身。設計


NewLisp是一個Lisp語言的實現,也能夠說是一個方言,其與Common Lisp相比,少了不少東西,但遠比Common Lisp容易使用。Lisp系列的語言有一個特色:沒有語法。或者說極小語法,用Lisp編寫程序,直接沒有了語法階段,從語義開始起步,因此很是接近編譯器。Lisp是一個多範式編程語言,支持命令式、函數式、面向對象等等,固然Lisp其實應該被稱爲:列表處理語言。或者說是:抽象語法樹處理語言。源程序就是AST,經過設計AST來構建軟件。rest

首先咱們找個最簡單的辦法,而後逐步改善。code

在匿名函數內部定義一個局部函數,經過調用這個函數,就能夠實現遞歸自身了。好比在要定義的遞歸匿名函數中再定義一個函數做爲主體函數,經過調用這個函數來實現遞歸的效果。

大概的形式以下:

(lambda (n)
  (define this (n) (if (< n 2) 1 (* n (this (- n 1)))))
  (this n))

如今能夠設計一個定義可遞歸匿名函數的宏了:

(define-macro
  (lambda* _args)
  (letex ((fargs _args) 
          (fbody (cons 'begin $args))
          (fcall (cons 'this (flat _args))))
    (lambda
      fargs
      (define (this fargs) fbody)
      fcall)))

這樣,只須要用lambda*,就能夠定義一個可遞歸的匿名函數:

; 經過this來調用自身
(lambda* (n) (if (< n 2) 1 (* n (this (- n 1)))))

可是這樣有一個很大的問題,這個定義的函數會污染名稱空間,並且不一樣的lambda*會覆蓋掉this,由於define是在全局中定義的。在Common Lisp中,能夠經過定義僅在函數內部可見的嵌套函數來解決:

(defmacro re-lambda (&rest body)
  `(lambda (&rest args)
     (labels (,(cons 'this body))
        (apply #'this args))))

可是在NewLisp中,我沒有找到如何定義僅在函數內部可見的嵌套函數,因此還須要經過其餘的辦法才能解決。


因而我想到了自動生成函數名的方式,NewLisp有一個函數(time-of-day),返回一個以較爲精確的時間戳:

> (time-of-day)
66359119.140
> (time-of-day)
66359415.039

這樣咱們能夠在擴展宏時自動生成一個函數名,以免衝突:

 (define-macro
  (lambda* _args)
  (let ((f (string "$."(time-of-day)))) ;生成一個隨時間變化的函數名
    (eval
      (list
        'define
        (cons (sym f) (flat _args))
        (list
          'let
          (list (list 'this (sym f))) ; this指向這個函數
          (cons 'begin $args))))))

因而當用lambda*定義時,會自動生成一個每一個時刻都不一樣的名稱,好比$.66789291.99二、$.66922513.671,幾乎不會有重複。


可是這樣依然不行,由於只是避免的名稱衝突,但仍是會污染名稱空間,並且在某些狀況下依然會形成難以預料的問題。因此在最後,設計了一個終極版本的lambda*,沒有任何負面做用。


終極版本

(define-macro
  (lambda* _args)
  (letex ((fargs _args) 
          (fbody (cons 'begin $args))
          (fcall (cons 'this (flat _args))))
    (lambda
      fargs
      (let ((this (lambda fargs fbody)))
        fcall))))

經過在擴展宏時,在lambda內部在定義一個lambda做爲主體函數,使用let將局部變量this指向這個主體函數,因而就能夠經過this來模擬調用自身了,函數不須要有名字,而只有一個指向函數的局部變量this,效果很是好:

; 階乘函數
(lambda* (n)
  (if (< n 2)
    1
    (* n (this (- n 1)))))

; 宏展開後以下=>
(lambda (n)
  (let ((this
        (lambda (n)
          (begin
            (if (< n 2)
              1
              (* n (this (- n 1))))))))
    (this n)))

一段測試結果:
> ((lambda* (n) (if (< n 2) 1 (* n (this (- n 1))))) 1)
1
> ((lambda* (n) (if (< n 2) 1 (* n (this (- n 1))))) 2)
2
> ((lambda* (n) (if (< n 2) 1 (* n (this (- n 1))))) 3)
6
> ((lambda* (n) (if (< n 2) 1 (* n (this (- n 1))))) 4)
24
> ((lambda* (n) (if (< n 2) 1 (* n (this (- n 1))))) 5)
120
> ((lambda* (n) (if (< n 2) 1 (* n (this (- n 1))))) 20)
2432902008176640000
> this
nil
> (setf this 100)
100
> ((lambda* (n) (if (< n 2) 1 (* n (this (- n 1))))) 10)
3628800
> this
100


* 勿輕易嘗試Lisp的宏,更不要輕易嘗試NewLisp的宏,前者你會受傷,後者你能體會到和查C++模板錯誤同樣的過程,甚至更加爆炸。

用農業界的一個術語來講:就像一顆原子彈。

相關文章
相關標籤/搜索