复习 Lisp: Syntax and Semantics

前两天, 大家在 add-hook和lambda ? - #78,来自 twlz0ne 中讨论了 lisp 语法方面的问题, 我回去翻了一下田春同学翻译的 《Practical Common Lisp》 发现了许多以前自己没有重视的东西, 值得大家复习一下。

下面是英文版的网页: http://www.gigamonkeys.com/book/syntax-and-semantics.html

2 个赞

中文版的建议买一本,当然是在舍不得的话,可以看看 http://vdisk.weibo.com/s/uutnsRMnC0MT8

The simplest Lisp forms, atoms, can be divided into two categories: symbols and everything else. A symbol, evaluated as a form, is considered the name of a variable and evaluates to the current value of the variable.

All other atoms–numbers and strings are the kinds you’ve seen so far–are self-evaluating objects. This means when such an expression is passed to the notional evaluation function, it’s simply returned.

Things get more interesting when we consider how lists are evaluated. All legal list forms start with a symbol, but three kinds of list forms are evaluated in three quite different ways. To determine what kind of form a given list is, the evaluator must determine whether the symbol that starts the list is the name of a function, a macro, or a special operator. If the symbol hasn’t been defined yet–as may be the case if you’re compiling code that contains references to functions that will be defined later–it’s assumed to be a function name.12 I’ll refer to the three kinds of forms as function call forms, macro forms, and special forms.

The evaluation rule for function call forms is simple: evaluate the remaining elements of the list as Lisp forms and pass the resulting values to the named function. This rule obviously places some additional syntactic constraints on a function call form: all the elements of the list after the first must themselves be well-formed Lisp forms.

When it seems like overkill to define a new function with DEFUN, you can create an “anonymous” function using a LAMBDA expression. As discussed in Chapter 3, a LAMBDA expression looks like this:

(lambda (parameters) body)

One way to think of LAMBDA expressions is as a special kind of function name where the name itself directly describes what the function does. This explains why you can use a LAMBDA expression in the place of a function name with #’.

(funcall #'(lambda (x y) (+ x y)) 2 3) ==> 5

You can even use a LAMBDA expression as the “name” of a function in a function call expression. If you wanted, you could write the previous FUNCALL expression more concisely.

((lambda (x y) (+ x y)) 2 3) ==> 5

But this is almost never done;

看到这句话后,我感受深刻,lambda 表达式就是函数名。

这句话也值得思考一下, 就是说,函数调用, 所有的参数都要求值, 但函数名那个 symbol 是不求值的,所以按理说

((lambda () "fff"))

应该是不合法的函数调用,因为它就不是一个符号,不是函数名, 但有意思的是,

 cl 规定 lambda 表达式就是一种 “特殊的” 函数名,

所以它就符合函数调用的规则,是合法的语句。

为什么函数名对应的那个符号不能求值呢? 应为符号的求值规则是:

就会出现类似的问题

   (setq a "111")
   (a 1 1)

函数调用的参数都要求值, 也许你觉得下面的例子就违反这条规矩:

(+ 1 2)

你也许会认为 1 和 2 就没有被求值,错了, 它们两个都被求值了,只不过 它们求值结果是它们本身,这就是所谓的 self-evaluating objects

1 个赞

和 Common Lisp 稍有不同,在 Emacs Lisp 中,lambda 表达式和 function 返回的也是函数名。利用 funcall 自动转化符号为对应的函数定义的特性使得两种语言表现出的行为几乎一致,但是实质是有很大区别的。

比如

(setq foo (list #'bar #'baz)) ;; `bar' `baz' 均为合法函数定义,且 `bar' 是无参数的函数。

对这样产生的列表,进行

(funcall (car foo))

在 Emacs Lisp 中,因为被装入列表的是函数的名称,所以当 bar 的函数定义被改变后,以上的结果也会对应改变。而在 Common Lisp 中,因为是函数的「定义」被装入列表,所以当 bar 的函数定义被修改后,以上代码的结果会不变。

1 个赞

(symbolp (lambda () “”)) => nil

lambda 返回的似乎就是一个 第一个元素是 lambda 的列表…

我感觉这句话是不是这么一个意思: lisp 解释器专门添加了一条规则来, 让lambda表达式有函数名的行为, 但其本质是一个列表?

确实 common-lisp 和 elisp 在这方面, 还是有一些区别

Welcome to GNU CLISP 2.49.60 (2017-06-25) <http://clisp.org/>

Copyright (c) Bruno Haible, Michael Stoll 1992, 1993
Copyright (c) Bruno Haible, Marcus Daniels 1994-1997
Copyright (c) Bruno Haible, Pierpaolo Bernardi, Sam Steingold 1998
Copyright (c) Bruno Haible, Sam Steingold 1999-2000
Copyright (c) Sam Steingold, Bruno Haible 2001-2010

Type :h and hit Enter for context help.

[1]> (defun test () "f")
TEST
[2]> (setq a (list #'test))
(#<FUNCTION TEST NIL (DECLARE (SYSTEM::IN-DEFUN TEST)) (BLOCK TEST "f")>)
[3]> (funcall (car a))
"f"
[4]> (defun test () "g")
TEST
[5]> (funcall (car a))
"f"
[6]> 

elisp:

;; This is *scratch* buffer.

(defun test () "fff") => test

(setq a (list #'test)) => (test)

(funcall (car a)) =>  "fff"

(defun test () "ggg") => test

(funcall (car a)) => "ggg"
2 个赞

用[定义]这个词不太合适, 应该是依据函数的定义生成的函数对象 (function object)

2 个赞

Emacs 里面 lambda 是个宏,展开来是 #'(lambda ...),后者就是对 funtion 这个 special form 的调用。你说的没错。

对于 CL 来说,你说错了:不是列表,是闭包。CL 为了提高效率是尽量避免在 run time 用列表这种原始而低效的数据结构的。闭包在 CL 里面是一种特殊的数据结构,是不能被当作列表操作。

但是你这句话对于动态作用域的 Lisp,比如 NewLisp,没有开 lexical 绑定的 Emacs Lisp 是正确的。在这些解释性语言中,lambda 本质的确就是列表。下面这段 Emacs Lisp 代码可以证明。

(setq foo (lambda () 1))

foo ;; => (lambda nil 1)

(car foo) ;; => lambda

甚至对于开了 lexical binding 的 Emacs Lisp,你说 lambda 本质是列表也是基本正确的:

(setq lexical-binding t)

(lambda () 1) ;; => (closure (t) nil 1)

(car (lambda nil 1)) ;; => closure

;; 最有趣的要来了

(fset 'bar '(clozure (t) nil 1))

(bar) ;; => 1

;; 还不够刺激?

(funcall '(closure (t) nil 1)) ;; => 1

((closure (t) (x) x) 2) ;; => 2

从这里我们看到了 Common Lisp 和 Emacs Lisp 一个重要的区别就是,很多在 Common Lisp 中打印成不可读入的数据结构,在 Emacs Lisp 中是用可重新读入的方法表示的。

见我翻译的:

所以我加了引号。

2 个赞

另外,就算是对于 Emacs Lisp,你只考虑了在未编译的前提下的情况,字节码编译了以后就是完全不同的一回事。

(setq foo (byte-compile (lambda nil "foo")))
;; => #[nil "\300\207" ["foo"] 1 "foo"]
(funcall foo)
;; => "foo"
(car foo) ;; => Err
;; 做个对比
(listp foo) ;; => nil
(listp (lambda nil "foo")) ;; => t

lambda 作为函数编译以后很明显就不是列表了。

所以直接修改 lambda 在 Emacs Lisp 里面是黑科技,随便用不得。具体一些细节因为实在是太多,我不在这里赘述了。

2 个赞

这点我赞同, 也许如果想搞清楚这一块, 看源代码是最好的方式, 用黑箱技术猜测它的内部运作可能就不太现实了.

闭包这个东西,以前就是无法理解是什么东西, 后来研究 lisp 的求值规则和自由变量相关的知识,才理解了 这到底是个什么玩意.

单靠闭包的定义来理解闭包,那简直是天方夜谭.

对对,自由变量 + dynamic scope vs lexical scope,我当时正好接连看了SICP开头的dynamic scope vs lexical scope,dynamic scope可以setq改变函数行为,和王垠的《lisp已死,lisp万岁》(没链接,他的博客404了)里面说lisp最初是dynamic scope,后来有了更好的lexical scope,才理解了 这到底是个什么玩意。

后来又仿佛听说lexical scope实现起来就是每个scope生成一个closure,把这个scope的bindings全部放里面(看上面帖子对【clisp的function object、lexical binding的elisp的closure object、elisp byte compile后的byte code】的解析,这个说法应该没错),有忍不住要长叹一声“哦~~~”的感觉 就像零距离见过飞碟飞了之后又登上去摸了下仪表板


是是是!心疼那些整天被面试官问“什么是闭包”的javascript程序员(包括以前的我)


这个emacs-document能搞个rss啥的不?

用 GitHub 的 RSS 吧。


还好你们不用研究 Continuation。不然更要完。

另外 Emacs Lisp 的闭包不能在 interactive 模式测试,需要写成文件以后才能生效。