见:Let Over Lambda

Introduction

程序的关键在于合适的抽象,这样才不会有一堆重复的代码,增加更多复杂度

宏很强大,宏很灵活,宏无所不能,BLABLABLA……

正如学C一定要理解指针啥时候用,学LISP一定要理解宏啥时候用

风格是给不知道啥时该用语言的啥特性的人准备的(中枪一片)

看此书之前,最好熟读 On Lisp

宏又被描述为元编程meta-program

先理解一下On Lisp的几个宏,例如mkstr、symb、group、flatten等等

Closures

lisp的闭包一般就是先 let 分配变量,然后返回一个lambda匿名函数,所以叫let over lambda。

在lisp中,变量没有类型,只有值才有类型。

关于lexical scope(词法作用域)和 dynamic scope(动态作用域),注意,现代编程语言基本都是用的lexical scope,因为dynamic scope的函数结果与调用环境紧耦合,同名变量可能引发错误,可参考:CSE 341 – Lexical and Dynamic Scoping

闭包大多建立在 lexical scope之上

lisp可通过defvar定义dynamic scope的special variables,经常会在变量名前后加 * 号,例如 special-var

lexical variables 是在编译阶段分配的symbol

通过declare指定优化条件,指导编译器生成变量,例如 (declare (optimize (speed 3) (safety 0)))

lambda 是个symbol,自身是个宏(注意看实现代码),作用是:生成一个宏,展开时会call指定的function

生成的lambda没法直接像函数一样在form调用,得用funcall或apply调

defun里面定义的lambda,如果不在let包围的lexical scope内(null lexical),就会在编译期的时候生成,后续都是直接调用,不用重复生成;如果是let + lambda组合(non-null lexical),则是在运行期动态生成,每调一次生成一次

假设一个函数生成闭包,如果该函数被compile,则闭包也会连着被compile;但如果闭包在 non-null lexical 环境下生成,它就没法被compile

其实闭包也可看做是包含一些状态变量(与perl中的state变量含义基本相同)的函数

对象object常用于指代某些数据、程序的集合。闭包类似于只有funcall一个method的对象;对象类似于可以用不同方式funcall的闭包,例如计数器的闭包,可以生成增、减两个lambda(这边的口水话真不少)

初始化一个闭包的方式,所谓的 lambda over let over lambda

(lambda ()
 (let ((cnt 0))
  (lambda () (incf cnt))))

其实就相当于一个匿名类class

如果是用defun定义,则相当于显式指定外层lambda的名字,只是写的时候省掉了lambda的关键字

(defun  cnt-class()
 (let ((cnt 0))
  (lambda () (incf cnt))))

给了一个block-scanner的闭包例子,检查几个连续的字符串是否能够匹配预先给定的字符串

(defvar scanner 
 (block-scanner "jihad"))   ; 指定待匹配的字符串为jihad

# (funcall scanner "the ji")   ; 匹配了 ji

NIL
* (funcall scanner "had tomorrow.")  ;继续匹配了 had

T 

又给了个闭包之间共享变量的例子,就是在defun外再套一次let,所谓的 Let Over Lambda Over Let Over Lambda

有了闭包可以没有类class,blablabla。。。

Macro Basics

举例 sleep-units, 有小时、分钟、秒、毫秒、微秒等时间单位

而在lisp中,是用一个symbol来标记一个指定的单位,检查symbol是否相同的速度是很快di

函数调用(sleep-units% '.5 'h)

宏调用(sleep-units .5 h)

sleep-units的调用在编译期就确定了参数取值,因此速度会比sleep-units%快;但同时,sleep-units也就没法在运行期动态确定unit类型

    ; 错误,宏调用期望的是一个symbol,而这边传入的是一个以if开头的列表
    (sleep-units 1 (if super-slow-mode 'd 'h))   

一个nlet宏的例子(name let)

(defmacro nlet (n letargs &rest body)
 `(labels ((,n ,(mapcar #'car letargs)
             ,@body))
     (,n ,@(mapcar #'cadr letargs))))

* (macroexpand
    '(nlet fact ((n n))
        (if (zerop n)
         1
         (* n (fact (- n 1))))))

(LABELS ((FACT (N)
          (IF (ZEROP N)
           1
           (* N (FACT (- N 1))))))
 (FACT N))

T

函数会对它的所有参数求值,然后把这些求值结果绑定到调用的环境中,再执行之前生成的lambda机器码

宏不对直接对参数求职,而是把它们嵌入一些lisp代码块中,令其执行某些特定的操作

如果碰到尾递归调用已定义的name let, scheme会默认优化,不会增加栈空间;common lisp可能会栈溢出。

关于 free variable 的介绍,blablabla。。。

讨论 nif 的宏展开,变量命名冲突的问题,用gensym解决

lisp-1 与 lisp-2,主要差别有一点,是否为函数提供分离的名字空间。参考 Lisp-1与Lisp-2比较

defmacro/g! 的宏实现,自动找出g!开头的变量,加上gensym声明,省得打那么多字

(defmacro defmacro/g! (name args &rest body)
 (let ((syms (remove-duplicates
              (remove-if-not #'g!-symbol-p
               (flatten body)))))
  `(defmacro ,name ,args
      (let ,(mapcar
             (lambda (s)
              `(,s (gensym ,(subseq
                          (symbol-name s)
                          2))))
             syms)
       ,@body))))

    * (macroexpand-1
            '(defmacro/g! nif (expr pos zero neg)
                `(let ((,g!result ,expr))
                    (cond ((plusp ,g!result) ,pos)
                     ((zerop ,g!result) ,zero)
                     (t ,neg)))))

(DEFMACRO NIF (EXPR POS ZERO NEG)
 (LET ((G!RESULT (GENSYM "RESULT")))
  `(LET ((,G!RESULT ,EXPR))
      (COND ((PLUSP ,G!RESULT) ,POS)
       ((ZEROP ,G!RESULT) ,ZERO)
       (T ,NEG)))))
    T 

双层的defmacro/g!那个junk-outer没搞懂

Paradigms Of Artificial Intelligence Programming: Case Studies in COMMON LISP 的 Once-only: A Lesson in Macrology 的宏实现值得仔细学习

once-only 是对传入宏的表达式只求值一次,将值绑定到新变量,在后续代码中使用该变量的值,避免side-effect

一个重点的宏实现demacro!,同时支持只求值一次(once-only),自动加gensym

(defmacro defmacro! (name args &rest body)
 (let* ((os (remove-if-not #'o!-symbol-p args))    ;抽出o!开头的变量名
        (gs (mapcar #'o!-symbol-to-g!-symbol os))) ;将o!x 转换为 g!x
  `(defmacro/g! ,name ,args
      `(let ,(mapcar #'list (list ,@gs) (list ,@os))  ;(let (g!x o!x) (g!y o!y))赋值,此时o!x只求值一次
          ,(progn ,@body)))))

(defmacro! square (o!x)
 `(* ,g!x ,g!x))

    * (macroexpand
            '(square (incf x)))

(LET ((#:X1633 (INCF X)))
 (* #:X1633 #:X1633))
    T

dynamic variable的取值决定于该表达式什么时候执行,而非它在何处被定义或被编译;lexical varible与之相反。

注意下面这个虽然是let over lambda,但它不是lexical闭包,因为它被解析求值一次后,下回调用直接返回绑定的temp-special值了

    * (let ((temp-special 'whatever)) 
            (lambda () temp-special)) 

#<FUNCTION :LAMBDA NIL TEMP-SPECIAL>

扯了一通common lisp的 Duality of Syntax 有多好,唉,口水话太多,有空再领略其中深意吧

喷了一把 special-var 的code style

Read Macros

#. 开头的form在读入的时候被求值,而不是等到整个外层form被求值的时候

* '(football-game
            (game-started-at
#.(get-internal-real-time))
            (coin-flip
#.(if (zerop (random 2)) 'heads 'tails)))

    (FOOTBALL-GAME
     (GAME-STARTED-AT 309)
     (COIN-FLIP TAILS))

Backquote ` 不是lisp必须的,但是宏重度使用此符号,虽然一堆人抱怨这个很晕

` 负责停止evaluate一个form,而 , 可以暂停此效果临时evaluate(称为 unquote)

    ; 1) regular unquote

    * (let ((s 'hello))
            `(,s world))

(HELLO WORLD)


; 2) splicing unquote

* (let ((s '(b c d)))
        `(a . ,s)) ; 只能在列表尾部接一下

(A B C D)

    * (let ((s '(b c d)))
            `(a ,@s e)) ; 可以在列表中间展开插入,重新分配空间

(A B C D E) 


; 3) destructive splicing unquote

* (defvar to-splice '(B C D))

TO-SPLICE
* `(A ,.to-splice E)  ; to-splice 尾部接上了E

(A B C D E)
    * to-splice

(B C D E)

危险的用法

(defun dangerous-use-of-bq ()
 `(a ,.'(b c d) e)) 

第一次调 dangerous-use-of-bq 会得到 (A B C D E),同时 ‘(b c d) 变成了 ‘(b c d e);第二次就会导致死循环,因为 ‘(b c d e) 尾部的e又要指向自身了

一种解决方案是

(defun safer-use-of-bq ()
 `(a ,.(mapcar #'identity '(b c d))  e))

defun |#"-reader|比较简单,就是读stream,直到碰上 “# 就停止

(set-dispatch-macro-character #\# #\" #'|#"-reader|)

参考 set-dispatch-macro-character

defun |#>-reader|山寨perl的 « 多行字符串,代码可以看看

CL-PPCRE 是lisp的正则库,跟perl的pcre有些不同:

  • CL-PPCRE很快,因为它是用lisp实现di,运行时编译。。。c编译器编译的程序没法去折腾c编译器本身,所以用C写的pcre引擎优化的没lisp彻底,blablabla
  • CL-PPCRE不限制匹配字符串,可以自定义一些格式匹配,即所谓的DSL (我咋觉得perl的Parse::RecDescent也能搞这些呢,唔,可能没lisp的S表达式灵活)
  • CL-PPCRE语法自由度更大

搞了一个 segment-reader 的函数,跟perl的字符串/g匹配差不多

正则匹配和替换的宏

#+cl-ppcre
(defmacro! match-mode-ppcre-lambda-form (o!args)
 ``(lambda (,',g!str)
     (cl-ppcre:scan
      ,(car ,g!args)
      ,',g!str)))

#+cl-ppcre
(defmacro! subst-mode-ppcre-lambda-form (o!args)
 ``(lambda (,',g!str)
     (cl-ppcre:regex-replace-all
      ,(car ,g!args)
      ,',g!str
      ,(cadr ,g!args))))

注意,上面的宏都以两个反引号``开始,生成的是一个列表list,而不是待evaluate的expression

后面接着defun |#~-reader|山寨perl的 =~m//, =~s//,用到了上面两个宏 搞好之后就能这么替换字符串了:

(funcall #~s/abc/def/ "Testing abc testing abc")

打印循环结构的例子(注意最后的nil不能省,不然卡住死循环)

* (let ((*print-circle* t))
        (print '#1=(hello . #1#))
        nil)

#1=(HELLO . #1#)
    NIL

lisp的read macro示例是v587的,lisp-reader 、lisp-printer

一个安全措施是置 read-eval 为 nil,禁止从不可信的源读入数据做为代码执行

另一个安全措施是禁止 #. 宏

还有一个是destructuring-bind指定参数绑定

剩下的是lisp error检查更…的blablabla… common lisp 检查

Programs That Program

lisp不是函数式语言,人家想咋改就咋改,可以比命令式还命令式,也可以比函数式还函数式,blablabla。。。

defmacro! defunits%,有两层反引号`

`与 , 结合,调整代码在运行期的执行先后顺序

defun defunits-chaining% 递归调用

defmacro! defunits%% 单位转换

defmacro! defunits 加上一些安全检查

defun tree-leaves%%和 defmacro tree-leaves,两者对比,tree-leaves的用法比较简洁。(与perl的 匿名函数sub {} 加 默认变量 $_ 组合有点类似,lisp版的写法更简短一点)

注意 tree-leaves里的x,没声明直接往上写,嵌入了一个implicit lexical variable bound,所谓violate lexical transparency

高级一点的macrolet

Macrolet is a COMMON LISP special form that introduces new macros into its enclosed lexical scope.

macrolet-defined macros will be expanded by the lisp system when it code-walks your expressions.

支持在不同的lexical contexts下进行不同的macro expand => defmacro没法这么牛x

code-walking是lisp系统在对一个表达式进行编译或求值之前要做的一些检查

对于macrolet指定的表达式在编译或求值时,lisp系统会先code-walk一下,并做宏展开

nlet-tail 裸实现尾递归,macrolet + tagbody + go 省栈空间,这个看着比较晕

两个命名比较:second 表示想要的东西是啥(What),cadr 表示怎么拿到想要的(How)

又一堆口水,lisp的car、cdr函数组合成cadr这种风格挺好,blablabla……

写个宏cxr%,无限组合car/cdr(这个宏木啥特别的)

cxr宏比较有意思,基于cxr%优化了2点:

  • n可以传变量进去
  • 搞成尾递归节省栈空间,省的n比较大(例如n>10)时,原来的版本展开的inline代码搞太长,不划算

def-english-list-accessors 批量命名多个宏,这个不错

with-all-cxrs 根据指定的’cxr字符串动态生成lambda

dlambda 支持通过key指定运行闭包中的某个函数(类似一个类支持多个方法,尼玛看到这,绝对知道类的好处了)

> (setf (symbol-function 'count-test)
        (let ((count 0))
             (dlambda
              (:inc () (incf count))
              (:dec () (decf count)))))
#<FUNCTION :LAMBDA (&REST #:ARGS8148)
    (CASE (CAR #:ARGS8148) 
     ((:INC) (APPLY (LAMBDA NIL (INCF COUNT)) (CDR #:ARGS8148)))
     ((:DEC) (APPLY (LAMBDA NIL (DECF COUNT)) (CDR #:ARGS8148))))

> (count-test :inc)
    1

    (defmacro! dlambda (&rest ds)
     `(lambda (&rest ,g!args)
         (case (car ,g!args)
          ,@(mapcar
              (lambda (d)
               `(,(if (eq t (car d)) t (list (car d)))
                   (apply (lambda ,@(cdr d))
                    ,(if (eq t (car d)) g!args `(cdr ,g!args)))))
              ds)
         )))

Anaphoric Macros

An anaphoric macro is one that deliberately captures a variable from forms supplied to the macro.

On Lisp中的 alambda 是一个anaphoric宏,capture的变量是其中的self

    (defmacro alambda (parms &body body)
     `(labels ((self ,parms ,@body))
#'self))

    > (alambda (n)
            (if (> n 0)
             (cons n (self (- n 1)))))
#<FUNCTION SELF (N) (BLOCK SELF (IF (> N 0) (CONS N (SELF (- N 1)))))>

defun |#`-reader|的用法举例,注意可能隐含了a1、a2、a3等参数定义

alet% 宏调整了let body里某些语句的执行顺序,注意看this前后

alet 与alet% 大体相同,就是加了个指针功能,每次碰到’invert就返回不一样的lambda,注意宏里面用到了alambda的self,alet的this

(alet ((acc 0))
 (alambda (n)
  (if (eq n 'invert)
   (setq this
    (lambda (n)
     (if (eq n 'invert)
      (setq this #'self)
      (decf acc n))))
   (incf acc n))))

也就是通过alet + alambda 在运行期间动态改变实际执行的代码片断

用labels实现了going-up/going-down的版本,清楚多了

再用alet-fsm,引入了一个state用于切换labels定义的多个lambda

(alet ((acc 0))
 (alet-fsm
  (going-up (n)
   (if (eq n 'invert)
    (state going-down)
    (incf acc n)))
  (going-down (n)
   (if (eq n 'invert)
    (state going-up)
    (decf acc n))))) 

alet-fsm是个anaphor injection的例子,虽然引入了this,可是光看lexical context的外层代码是没有出现di(押一个桔子,多碰到几次这种,bug就要出来了)

作者说free variable injection写法类似于c指针写法,有N种风格建议(茴字有4种写法,哈哈)

free variable injection常用于两个宏之间通信,复杂度较高,blablabla。。。

ichain-before / ichain-after 注意多个before时,后面的先执行,具体看代码

ichain-intercept% / ichain-intercept 作用类似于参数检查,引入的block为intercept、g!intercept

Hotpatching Closures 运行期动态改变调用的闭包

以 alet-hotpatch%、alet-hotpatch为例,区别在于后者用了dlambda

为了避免alet-hotpatch中this被unwant captured,又优化了一版let-hotpatch,换成上文中的g!this,即所谓的几个宏组合使用成一个整体,与外界隔开。(方法满工巧,就是组合调用的背景略复杂)

上文中demacro!是在sub-lexical scope中绑定变量

指向sub-lexical scope变量的symbol只有在宏展开之前传到lisp的raw lists才会生效(绕不绕呀)

demacro!预处理了g!开头的参数,the G-bang symbols are sub-lexically bound。

举例 junk / junk2 对比,junk中g!var会被替换成#:VAR1663,而junk2调用返回生成g!var字符串的宏,生成的g!var是在sub-lexical scope中,不会再转换成gensyms(这边区别明显)

因此,如果一些symbol reference出现在不同场合,可能展开的结果就不同,例如junk3

另一个例子是with-all-cxrs,宏参数往里传就不要(所以写法有学问)

sublet是搞sub-lexical binding的指令,在看sublet之前得研究let-binding-transform先,不过这个transform比较简单

sublet还用到了tree-leaves。可以看到它的宏展开不保留原来的符号a,而是生成了一个新的标记,连'a都自动转过去了。

sublet*把 body 先做了一次 macroexpand-1,这样body里的宏引用的变量名就可以预先展开了。这个称为supre sub-lexical scope。 sublet*解决了上面的 injector-for-a 问题。不过只能展开一层,嵌套还是不行。原因是“nested macros in the expression are not expanded by macroexpand-1”

sublet*这类宏,可以调整宏展开时,可见的变量,form展开的方式。对宏编程的宏。(确实工巧)

另一本书 Lisp in Small Pieces

pandoriclet 宏,支持根据key进行 变量取值、赋值、执行指定代码,注意这边也用到了this。有些还没定义也可以先写上要用。

What we have done is created an inter-closure protocol, or message passing system, for communicating between closures.

扯了一通generalised variable

Defsetf 宏 implicitly binds gensyms around provided forms

with-pandoric uses symbol-macrolet to install these generalised variables as seemingly new lexical variables with the same names as the closed-over variables. 这些变量由 pandoriclet定义,但lexical contexts分离

跟 Hotpatching Closures 的差别在于,这回的pandoric更精巧,Hotpatching 整个共享lexical binding

Macros are not for inlining, compilers are for inlining

plambda 宏

plambda creates another anaphor—self. While the anaphor this refers to the actual closure that is to be invoked, self refers to the indirection environment that calls this closure.

plambda 和 with-pandoric 可以重写lexical scope。

eval对form求值时,是在一个null lexical环境下的,所以下面这个会出错:

* (let ((x 1))
    (eval
      '(+ x 1)))

Error: The variable X is unbound.

eval 比较慢,而且经常出错,还有限制。所以,想用eval的时候,先想想能不能用宏。

pandoric-eval 先用plambda把变量搞成闭包,传到eval,变成dynamic环境变量生效

* (let ((x 1))
    (pandoric-eval (x)
      '(+ 1 x)))

2

这几段比较晕

More Efficiency Topics

lisp 就是比较快,blablabla…

看 Edi Weitz 的 CL-PPCRE,blablabla…

一堆举例,point就是人家也可以很快的,注意得这么用,blablabla…

Lisp Moving Forth Moving Lisp

又一种语言:Forth

这章我没看

附录

除了lisp,作者推荐学一下C、Perl、Forth、smalltalk、haskell。

为毛要perl呢,作者给出的原因:if lisp is the result of taking syntax away, perl is the result of taking syntax all the way.

哈哈!

CMUCL / SBCL 等等



Published

13 December 2013

Tags


Share On