在 2021 年学习 Lisp 编程语言

许多大型代码库中潜藏着 Lisp 代码,因此熟悉这门语言是很明智的。
81 位读者喜欢这篇文章。
Woman sitting in front of her laptop

kris krüg

Lisp 发明于 1958 年,使其成为第二古老的计算机编程语言。它衍生出了几个现代变体,包括 Common Lisp、Emacs Lisp (Elisp)、Clojure、Racket、Scheme、Fennel 和 GNU Guile。

喜欢思考编程语言设计的人通常喜欢 Lisp,因为它的语法和数据共享相同的结构:Lisp 代码本质上是列表的列表,它的名字是 LISt Processing(列表处理)的缩写。喜欢思考编程语言美学的人通常讨厌 Lisp,因为它频繁使用括号进行作用域划分;事实上,一个常见的笑话是 Lisp 代表 Lots of Irritating Superfluous Parentheses(大量令人恼火的多余括号)。

无论你喜欢还是讨厌它的设计理念,Lisp 都是对过去的一次有趣的瞥见,并且由于 Clojure 和 Guile,也看到了未来。你可能会惊讶地发现,在任何给定行业的大型代码库中,隐藏着大量的 Lisp 代码,因此至少对这门语言有一定的了解是个好主意。

安装 Lisp

Lisp 有许多实现。流行的开源版本包括 SBCLGNU LispGNU Common Lisp (GCL)。你可以使用你发行版的包管理器安装其中任何一个,但在本文中我使用 clisp

在 Fedora Linux 上

$ sudo dnf install clisp

在 Debian 上

$ sudo apt install clisp

对于 macOS,你可以使用 MacPortsHomebrew

$ sudo port install clisp

对于 Windows,你可以使用 Cygwin 上的 clisp,或者从 gnu.org/software/gcl 下载 GCL 二进制文件。

即使我使用的是 clisp 命令,本文中的大多数原则也适用于任何 Lisp。如果你选择使用不同的 Lisp 实现,运行 Lisp 代码的命令与我在本文中使用的命令不同(例如,gclsbcl 而不是 clisp),但其他一切都相同。

列表处理

Lisp 源代码的基本单元是表达式,它被写成列表。例如,这是一个运算符 (+) 和两个整数 (12) 的列表

(+ 1 2)

这也是一个 Lisp 表达式,它使用一个符号 (+),该符号求值为一个函数(加法),以及两个参数 (12)。你可以在一个名为 REPL(读取-求值-打印循环)的交互式 Common Lisp 环境中运行这个表达式和其他表达式。如果你熟悉 Python 的 IDLE,Lisp 的 REPL 应该会让你感到有些熟悉。

要启动 REPL,请启动 Common Lisp

$ clisp
[1]> 

在 REPL 提示符下,键入几个表达式

[1]> (+ 1 2)
3
[2]> (- 1 2)
-1
[3]> (- 2 1)
1
[4]> (+ 2 3 4)
9

函数

现在你已经了解了 Lisp 表达式的基本结构,你可以以有用的方式使用 Lisp 函数。print 函数接受你提供的任何参数并在你的终端上显示它,而 pprint 函数以“漂亮”的方式打印它。print 函数还有其他变体,但 pprint 在 REPL 中很好用

[1]> (pprint "hello world")

"hello world"

[2]>

你可以使用 defun 创建你自己的函数。defun 函数需要你的函数的名称以及你希望你的函数接受的任何参数

[1]> (defun myprinter (s) (pprint s))
MYPRINTER
[2]> (myprinter "hello world")

"hello world"

[3]>

变量

你可以在 Lisp 中使用 setf 创建变量

[1]> (setf foo "hello world")
"hello world"
[2]> (pprint foo)

"hello world"

[3]>

你可以将表达式嵌套在表达式中,形成一种管道。例如,你可以在调用 string-upcase 函数将其字符转换为大写后,以漂亮的方式打印你的变量的内容

[3]> (pprint (string-upcase foo))

"HELLO WORLD"

[4]> 

Lisp 是动态类型的,这意味着你在设置变量时不必声明变量类型。Lisp 默认将整数视为整数

[1]> (setf foo 2)
[2]> (setf bar 3)
[3]> (+ foo bar)
5

如果你打算将整数解释为字符串,你可以引用它

[4]> (setf foo "2")                                                                                                                      
"2"                                                                                                                                      
[5]> (setf bar "3")                                                                                                                       
"3"
[6]> (+ foo bar)

*** - +: "2" is not a number
The following restarts are available:
USE-VALUE      :R1      Input a value to be used instead.
ABORT          :R2      Abort main loop
Break 1 [7]>

在这个示例 REPL 会话中,foobar 都被设置为带引号的数字,因此 Lisp 将它们解释为字符串。数学运算符不能用于字符串,因此 REPL 进入调试器模式。要退出调试器,请按键盘上的 Ctrl+D

你可以使用 typep 函数对对象进行一些内省,该函数测试特定的数据类型。标记 TNIL 分别代表

[4]> (typep foo 'string)
NIL
[5]> (typep foo 'integer)
T

stringinteger 前面的单引号 (') 阻止 Lisp(错误地)将这些关键字评估为变量

[6]> (typep foo string)
*** - SYSTEM::READ-EVAL-PRINT: variable STRING has no value
[...]

这是一种保护术语的简写方式,通常使用 quote 函数完成

[7]> (typep foo (quote string))
NIL
[5]> (typep foo (quote integer))
T

列表

毫不奇怪,你也可以在 Lisp 中创建列表

[1]> (setf foo (list "hello" "world"))
("hello" "world")

可以使用 nth 函数索引列表

[2]> (nth 0 foo)
"hello"
[3]> (pprint (string-capitalize (nth 1 foo)))

"World"

退出 REPL

要结束 REPL 会话,请按键盘上的 Ctrl+D,或者在 Lisp 中使用 quit 关键字

[99]> (quit)
$

脚本

Lisp 可以被编译或用作解释型脚本语言。后者可能是你刚开始时最简单的选择,特别是如果你已经熟悉 Python 或 shell 脚本

这是一个用 GNU Common Lisp 编写的简单掷骰子脚本

#!/usr/bin/clisp

(defun roller (num)  
  (pprint (random (parse-integer (nth 0 num))))
)

(setf userput *args*)
(setf *random-state* (make-random-state t))
(roller userput)

第一行告诉你的 POSIX 终端要使用哪个可执行文件来运行脚本。

defun 创建的 roller 函数使用 random 函数打印一个伪随机数,该数字最大为 num 列表的第零项(不包括第零项)。num 列表尚未在脚本中创建,但该函数直到被调用才会被执行。

下一行将启动时提供给脚本的任何参数分配给名为 userput 的变量。userput 变量是一个列表,它在传递给 roller 函数后会变成 num

脚本的倒数第二行启动一个随机种子。这为 Lisp 提供了足够的熵来生成一个大致随机的数字。

最后一行调用自定义的 roller 函数,并将 userput 列表作为其唯一参数提供。

将文件另存为 dice.lisp 并标记为可执行

$ chmod +x dice.lisp

最后,尝试运行它,并提供一个最大数字,从中选择其随机数

$ ./dice.lisp 21

13
$ ./dice.lisp 21

7
$ ./dice.lisp 21

20

不错!

你可能会注意到,你的模拟骰子可能的值为 0,并且永远不会达到你作为参数提供给它的最大数字。换句话说,这个脚本永远不会在 20 面骰子上掷出 20(除非你将 0 算作 20)。有一个简单的解决方法,你只需要从本文中学到的知识就可以做到。你能修复这个错误吗?

学习 Lisp

无论你能否想象将 Lisp 用作个人脚本的实用语言、提升你的职业生涯,还是仅仅作为一个有趣的实验,你都可以在年度 Lisp 游戏创作大赛 中看到一些特别有创意的用途(大多数提交的作品都是开源的,因此你可以查看代码,从你玩的游戏中学习)。

Lisp 是一门有趣而独特的语言,拥有不断增长的开发者基础,以及足够多历史悠久和新兴的方言,可以让所有学科的程序员都感到满意。

接下来阅读什么
标签
Seth Kenlon
Seth Kenlon 是一位 UNIX 极客、自由文化倡导者、独立多媒体艺术家和 D&D 爱好者。他曾在电影和计算机行业工作,通常同时进行。

12 条评论

我总是从你这里学到新东西。你知道 Seymour Papert 等人开发的 Logo 计算机语言是 Lisp 的一种改编吗?我记得 20 世纪 80 年代后期我在课堂上尝试使用 Logo 时,我的兄弟告诉我这件事。

非常喜欢这篇文章,我以前只听说过 Lisp,但看到这篇文章让我很想尝试一下。谢谢。

LISP:Lots of Insipid Stupid Parentheses(大量乏味愚蠢的括号)——这是 20 世纪 70 年代我学习它时(以及当我和一个朋友为我的 Altair 编写了一个 LISP 解释器时)我们对它的称呼,现在仍然如此。从学术角度来看,它很有趣。除此之外,它 a) 难以阅读 b) 难以调试 c) 使用的符号一点也不直观,因此 LISP 代码往往难以编写,而且非常难以维护,更不用说安全性了。FORTH 在当时也很有趣,而且在资源非常有限的机器上确实很有用。今天,它是一种应该成为的、并且确实是古董。LISP 也是一种古董,它属于学术界,几乎不属于其他任何地方,eMacs 除外——以及它最初构思的 IBM 704——那是真空管,伙计们——以及 IBM 7090。

如果你仔细听,你会听到银河系中 Racket、Guile、Fennel 和 Lisp 用户的声音同时在呐喊。

回复 作者 cube1

我开始起荨麻疹了。有人起荨麻疹吗?伙计们……Lisp 让我起荨麻疹了!

“GCL 及其 clisp 命令”

如果我 '$ sudo dnf install gcl' 然后 'clisp',它会说:“command not found”(命令未找到)

但是,只 '$ gcl' 可以工作。
这意味着 'shebang' 应该是 '#!/usr/bin/gcl'?

是的,在我急于展示你有多少 Lisp 选择时,恐怕我把事情搞混了。
我已经为未来的读者修改了文章。

安装 `clisp`,文章中的一切都可以正常工作,无需调整。

`sudo dnf install clisp`

回复 作者 openharry

./dice.lisp 不起作用。
它会启动 gcl,我看到一个提示符。
Fedora 34 live CD

在我急于展示你有多少 Lisp 选择时,恐怕我把事情搞混了。
我已经为未来的读者修改了文章。

安装 `clisp`,文章中的一切都可以正常工作,无需调整。

`sudo dnf install clisp`

回复 作者 openharry

你的错误是写 '$ sudo dnf install gcl',而应该安装 'clisp'。然后你的脚本就可以工作了。为了更好地模拟真实的骰子,它应该有
(pprint (+ 1 (random (parse-integer (nth 0 num)))))
那么 './dice.lisp 6' 将给出 {1..6} 范围内的结果

你作弊了 :-) 我正想把它留给读者作为家庭作业,让他们弄清楚如何调整范围。不过,有人会感谢你的提示!

回复 作者 openharry

© . All rights reserved.