IT技术博客大学习 共学习 共进步

为什么我喜欢Lisp语言

外刊IT评论 2011-10-14 13:40:37 浏览 3,923 次

这篇文章是我在Simplificator――我工作的地方――的一次座谈内容的摘录,座谈的题目叫做“为什么我喜欢Smalltalk语言和Lisp语言”。在此之前,我曾发布过一篇叫做“ 为什么我喜欢Smalltalk?”的文章。

Lisp是一种很老的语言。非常的老。Lisp有很多变种,但如今已没有一种语言叫Lisp的了。事实上,有多少Lisp程序员,就有多少种Lisp。这是因为,只有当你独自一人深入荒漠,用树枝在黄沙上为自己喜欢的Lisp方言写解释器时,你才成为一名真正的Lisp程序员

目前主要有两种Lisp语言分支:Common LispScheme,每一种都有无数种的语言实现。各种Common Lisp实现都大同小异,而各种Scheme实现表现各异,有些看起来非常的不同,但它们的基本规则都相同。这两种语言都非常有趣,但我却没有在实际工作中用过其中的任何一种。这两种语言中分别在不同的方面让我苦恼,在所有的Lisp方言中,我最喜欢的是Clojure语言。我不想在这个问题上做更多的讨论,这是个人喜好,说起来很麻烦。

Clojure,就像其它种的Lisp语言一样,有一个REPL(Read Eval Print Loop)环境,你可以在里面写代码,而且能马上得到运行结果。例如:

1 5
2 ;=> 5
3
4 "Hello world"
5 ;=> "Hello world"

通常,你会看到一个提示符,就像user>,但在本文中,我使用的是更实用的显示风格。这篇文章中的任何REPL代码你都可以直接拷贝到Try Clojure运行。

我们可以像这样调用一个函数:

1 (println "Hello World")
2 ; Hello World
3 ;=> nil

程序打印出“Hello World”,并返回nil。我知道,这里的括弧看起来好像放错了地方,但这是有原因的,你会发现,他跟Java风格的代码没有多少不同:

1 println("Hello World")

这种Clojure在执行任何操作时都要用到括弧:

1 (+ 1 2)
2 ;=> 3

在Clojure中,我们同样能使用向量(vector):

1 [1 2 3 4]
2 ;=> [1 2 3 4]

还有符号(symbol):

1 'symbol
2 ;=> symbol

这里要用引号('),因为Symbol跟变量一样,如果不用引号前缀,Clojure会把它变成它的值。list数据类型也一样:

1 '(li st)
2 ;=> (li st)

以及嵌套的list:

1 '(l (i s) t)
2 ;=> (l (i s) t)

定义变量和使用变量的方法像这样:

1 (def hello-world "Hello world")
2 ;=> #'user/hello-world
3
4 hello-world
5 ;=> "Hello world"

我的讲解会很快,很多细节问题都会忽略掉,有些我讲的东西可能完全是错误的。请原谅,我尽力做到最好。

在Clojure中,创建函数的方法是这样:

1 (fn [n] (* n 2))
2 ;=> #<user$eval1$fn__2 user$eval1$fn__2@175bc6c8>

这显示的又长又难看的东西是被编译后的函数被打印出的样子。不要担心,你不会经常看到它们。这是个函数,使用fn操作符创建,有一个参数n。这个参数和2相乘,并当作结果返回。Clojure和其它所有的Lisp语言一样,函数的最后一个表达式产生的值会被当作返回值返回。

如果你查看一个函数如何被调用:

1 (println "Hello World")

你会发现它的形式是,括弧,函数,参数,反括弧。或者用另一种方式描述,这是一个列表序列,序列的第一位是操作符,其余的都是参数。

让我们来调用这个函数:

1 ((fn [n] (* n 2)) 10)
2 ;=> 20

我在这里所做的是定义了一个匿名函数,并立即应用它。让我们来给这个函数起个名字:

1 (def twice (fn [n] (* n 2)))
2 ;=> #'user/twice

现在我们通过这个名字来使用它:

1 (twice 32)
2 ;=> 64

正像你看到的,函数就像其它数据一样被存放到了变量里。因为有些操作会反复使用,我们可以使用简化写法:

1 (defn twice [n] (* 2 n))
2 ;=> #'user/twice
3
4 (twice 32)
5 ;=> 64

我们使用if来给这个函数设定一个最大值:

1 (defn twice [n] (if (> n 50) 100 (* n 2))))

if操作符有三个参数:断言,当断言是true时将要执行的语句,当断言是 false 时将要执行的语句。也许写成这样更容易理解:

1 (defn twice [n]
2   (if (> n 50)
3       100
4       (* n 2)))

非常基础的东西。让我们来看一下更有趣的东西。

假设说你想把Lisp语句反着写。把操作符放到最后,像这样:

1 (4 5 +)

我们且把这种语言叫做Psil(反着写的Lisp...我很聪明吧)。很显然,如果你试图执行这条语句,它会报错:

1 (4 5 +)
2 ;=> java.lang.ClassCastException: java.lang.Integer cannot be cast to clojure.lang.IFn (NO_SOURCE_FILE:0)

Clojure会告诉你4不是一个函数(函数是必须是clojure.lang.IFn接口的实现)。

我们可以写一个简单的函数把Psil转变成Lisp:

1 (defn psil [exp]
2   (reverse exp))

当我执行它时出现了问题:

1 (psil (4 5 +))
2 ;=> java.lang.ClassCastException: java.lang.Integer cannot be cast to clojure.lang.IFn (NO_SOURCE_FILE:0)

很明显,我弄错了一个地方,因为在psil被调用之前,Clojure会先去执行它的参数,也就是(4 5 +),于是报错了。我们可以显式的把这个参数转化成list,像这样:

1 (psil '(4 5 +))
2 ;=> (+ 5 4)

这回它就没有被执行,但却反转了。要想运行它并不困难:

1 (eval (psil '(4 5 +)))
2 ;=> 9

你开始发现Lisp的强大之处了。事实上,Lisp代码就是一堆层层嵌套的列表序列,你可以很容易从这些序列数据中产生可以运行的程序。

如果你还没明白,你可以在你常用的语言中试一下。在数组里放入2个数和一个加号,通过数组来执行这个运算。你最终得到的很可能是一个被连接的字符串,或是其它怪异的结果。

这种编程方式在Lisp是如此的非常的常见,于是Lisp就提供了叫做宏(macro)的可重用的东西来抽象出这种功能。宏是一种函数,它接受未执行的参数,而返回的结果是可执行的Lisp代码。

让我们把psil传化成宏:

1 (defmacro psil [exp]
2   (reverse exp))

唯一不同之处是我们现在使用defmacro来替换defn。这是一个非常大的改动:

1 (psil (4 5 +))
2 ;=> 9

请注意,虽然参数并不是一个有效的Clojure参数,但程序并没有报错。这是因为参数并没有被执行,只有当psil处理它时才被执行。psil把它的参数按数据看待。如果你听说过有人说Lisp里代码就是数据,这就是我们现在在讨论的东西了。数据可以被编辑,产生出其它的程序。这种特征使你可以在Lisp语言上创建出任何你需要的新型语法语言。

在Clojure里有一种操作符叫做macroexpand,它可以使一个宏跳过可执行部分,这样你就能看到是什么样的代码将会被执行:

1 (macroexpand '(psil (4 5 +)))
2 ;=> (+ 5 4)

你可以把宏看作一个在编译期运行的函数。事实上,在Lisp里,编译期和运行期是杂混在一起的,你的程序可以在这两种状态下来回切换。我们可以让psil宏变的罗嗦些,让我们看看代码是如何运行的,但首先,我要先告诉你do这个东西。

do是一个很简单的操作符,它接受一批语句,依次运行它们,但这些语句是被整体当作一个表达式,例如:

1 (do (println "Hello") (println "world"))
2 ; Hello
3 ; world
4 ;=> nil

通过使用do,我们可以使宏返回多个表达式,我们能看到更多的东西:

1 (defmacro psil [exp]
2   (println "compile time")
3   `(do (println "run time")
4        ~(reverse exp)))

新宏会打印出“compile time”,并且返回一个do代码块,这个代码块打印出“run time”,并且反着运行一个表达式。这个反引号`的作用很像引号',但它的独特之处是你可以使用~符号在其内部解除引号。如果你听不明白,不要担心,让我们来运行它一下:

1 (psil (4 5 +))
2 ; compile time
3 ; run time
4 ;=> 9

如预期的结果,编译期发生在运行期之前。如果我们使用macroexpand,或得到更清晰的信息:

1 (macroexpand '(psil (4 5 +)))
2 ; compile time
3 ;=> (do (clojure.core/println "run time") (+ 5 4))

可以看出,编译阶段已经发生,得到的是一个将要打印出“run time”的语句,然后会执行(+ 5 4)println也被扩展成了它的完整形式,clojure.core/println,不过你可以忽略这个。然后代码在运行期被执行。

这个宏的输出本质上是:

1 (do (println "run time")
2     (+ 5 4))

而在宏里,它需要被写成这样:

1 `(do (println "run time")
2      ~(reverse exp))

反引号实际上是产生了一种模板形式的代码,而波浪号让其中的某些部分被执行((reverse exp)),而其余部分被保留。

对于宏,其实还有更令人惊奇的东西,但现在,它已经很能变戏法了。

这种技术的力量还没有被完全展现出来。按着" 为什么我喜欢Smalltalk?"的思路,我们假设Clojure里没有if语法,只有cond语法。也许在这里,这并不是一个太好的例子,但这个例子很简单。

cond 功能跟其它语言里的switchcase 很相似:

1 (cond (= x 0) "It's zero"
2       (= x 1) "It's one"
3       :else "It's something else")

使用 cond,我们可以直接创建出my-if函数:

1 (defn my-if [predicate if-true if-false]
2   (cond predicate if-true
3         :else if-false))

初看起来似乎好使:

1 (my-if (= 0 0) "equals" "not-equals")
2 ;=> "equals"
3 (my-if (= 0 1) "equals" "not-equals")
4 ;=> "not-equals"

但有一个问题。你能发现它吗?my-if执行了它所有的参数,所以,如果我们像这样做,它就不能产生预期的结果了:

1 (my-if (= 0 0) (println "equals") (println "not-equals"))
2 ; equals
3 ; not-equals
4 ;=> nil

my-if转变成宏:

1 (defmacro my-if [predicate if-true if-false]
2   `(cond ~predicate ~if-true
3          :else ~if-false))

问题解决了:

1 (my-if (= 0 0) (println "equals") (println "not-equals"))
2 ; equals
3 ;=> nil

这只是对宏的强大功能的窥豹一斑。一个非常有趣的案例是,当面向对象编程被发明出来后(Lisp的出现先于这概念),Lisp程序员想使用这种技术。

C程序员不得不使用他们的编译器发明出新的语言,C++和Object C。Lisp程序员却创建了一堆宏,就像defclass, defmethod等。这全都要归功于宏。变革,在Lisp里,只是一种进化。

建议继续学习

  1. 每个程序员都应该学习使用Python或Ruby (阅读 17,741)
  2. 敲击最多的键和编程语言语法 (阅读 7,305)
  3. 为什么Lisp语言如此先进?(译文) (阅读 6,404)
  4. 编程语言的选择很重要 (阅读 5,124)
  5. 编程语言的可读性 (阅读 4,964)
  6. PHP很烂?我的看法 (阅读 4,501)
  7. php语言漫谈 (阅读 4,184)
  8. 几种计算机语言的评价(修订版) (阅读 4,141)
  9. 分清“语言/规范”以及“平台/实现”,以及跨平台.NET开发 (阅读 4,143)
  10. 再谈非主流工业语言 (阅读 3,744)