简介
大家好!希望大家一切都好。
Exercism 最近几周真的非常激动人心:我们推出了 Exercism Premium 和 Exercism Insiders。我们还举办了几场很棒的社区线上会议,网站上也有许多改进已经上线,还有更多即将推出。眼下值得期待的事情很多,但最令人兴奋的,莫过于我们进入了 #12in23 的第 6 个月!也就是 S-表达式的夏天,更顺口的简称就是 Summer of Sexps。
和往常一样,编程界的智者 Erik 也和我一起。
这个月我们有五门语言:Clojure、Common Lisp、Emacs Lisp、Racket 和 Scheme。它们都是 Lisp 语言的方言,所以在这个视频里,我们不会太侧重逐个讲这些语言,而是多花点时间看看 Lisp 本身,以及它独特在哪里。最后我们再简要过一遍这几种语言。
不过在开始之前,先交代几件事!要获得 Summer of Sexps 徽章,你需要在六月期间用其中任意一门语言完成任意五道练习。
徽章
还有一个贯穿全年的 12in23 徽章。要拿到它,你需要用这门语言解答五道精选练习。如果你是在六月之后看到这个视频,这一部分在年内任何时候都可以做,所以你没有错过。考虑到很多人从未接触过 Lisp,我们尽量挑选了相对简单的练习,让你尝一尝 Lisp 语言的味道。
- 闰年: 处理布尔条件和真值性(可选地还有词法作用域)
- 一赠一: 格式化字符串,并处理一个可选参数
- 平方差: 调用自定义函数,用前缀记法做数学运算
- 机器人名: 处理随机性、原子和结构化数据
- 括号匹配: 用递归验证字符串
这些练习以及前几个月的练习,都可以在 #12in23 页面上找到。
概览
那么,说回基于 Lisp 的语言。我想我们得先稍微了解一下 Lisp。先来对 Lisp 做个简单的总体介绍吧。
Lisp
- 首先要注意的是,Lisp 是最古老的语言之一。
- 它由 John McCarthy 于 1958 年在 MIT 创造,那时计算机还能从上到下占满整个房间 🙂
- Lisp 这个名字代表 LISt Processing(列表处理),或者 LISt Processor(列表处理器),凸显了列表这种数据结构的重要性。
- 它最初是为人工智能研究而设计的。
- Lisp 基于 lambda 演算,它由 Alonzo Church 提出,是一种在数学中描述计算的形式系统(简化版)。
- 它是一门影响极为深远的语言,原因有很多:
- 它是至今仍被广泛使用的高级编程语言中第二古老的(仅次于 Fortran)
- 它是第一种高级函数式编程语言,如今我们与函数式编程联系起来的许多特性,都是由它引入的。
- 注意,Lisp 仍然支持命令式编程
- 它是第一种带有垃圾回收器的语言,让作者不必再手动管理内存
- 它语法相对较少、语义相对简单,因此非常适合教学。
- 因此,Lisp(更准确地说,是它的某个方言)常被用来教授编程
- 它衍生出了(而且仍在衍生!)大量语言方言(我们会讨论其中 Exercism 支持的那些)。
- 换句话说,在编程语言这棵树上,Lisp 系语言自成一个分支(就像 C 系语言也有一个分支一样)。
最近我和 Haskell 的创造者之一 Simon Peyton Jones 聊天时,他谈到了围绕图灵机构建的语言与围绕 lambda 演算构建的语言之间的差异。如果你想了解更多,值得去看一看那次访谈。
括号
Lisp 里有很多括号,但这未必是坏事(就像在 C 系语言里有大量大括号也未必是坏事一样)。 Lisp 围绕一种叫做 S-表达式的东西构建。 S-表达式(symbolic expression 的简称,缩写为 sexpr 或 sexp,这也是本月挑战名字的由来)是一种用来表示数据的表达式。它最初是为最初的 Lisp 语言发明的,并由 Lisp 推广开来。 S-表达式可以有两种形式:
- 原子(例如 'x')。可以把它们看作非嵌套的“值”,或者树中的叶子
- 表达式 x . y,其中 x 和 y 都是 S-表达式。可以把它们看作数对,其中 y 是列表中的下一个元素(如果有的话),或者树中的节点。注意这是一个递归定义,在叶子层终止。通常,这种 S-表达式用括号表示。
S-表达式
在 Lisp 中,S-表达式既用来表示数据,也用来表示列表。 因此,每当你定义一个列表,都会用到括号。 再加上: 列表是 Lisp 的核心数据结构(名字就是这么来的), 在某些 Lisp 里它还是唯一的数据结构, 结果就是你会看到一大堆括号。 为了说明列表有多核心:如果你想在 Lisp 里调用一个函数,做法就是创建一个列表。
有意思的是,列表的第一个元素(也就是头部)代表被调用的函数,其余元素(也就是尾部)作为实参传入。 这叫做前缀记法(运算符位于操作数之前),一开始可能有点怪,但其实非常有用:
- 你可以把运算符作用于多个实参,而无需重复写运算符(例如 (+ 1 2 3))
- 运算符的优先级变得明确,因为要调用不同的运算符,你反正都得新建一个 S-表达式
有趣的是,列表甚至被用来表示源代码,不过这个我们稍后再讲。
总的来说,大多数 Lisp 语法都非常精简,语义也相对简单,因此相对容易学习,理解代码也变得更容易。 精简的语法并没有让它们变得更弱! 把这两点结合起来(精简的语法 + 简单的语义),Lisp 就成了编写编译器和解释器的理想选择。 如果你曾想自己写一个编译器,用 Lisp 来练手是个不错的选择!
Lisp 的酷炫特性
前面提到过,Lisp 在内部用相同的数据类型和数据结构来表示代码。 这个性质叫做同像性(homoiconicity,也叫同像的)。 换句话说,如果一门语言写的程序可以用这门语言当作数据来操作,那么它就是同像的,程序的内部表示只需读程序本身就能推断出来。 这个性质常被概括为:这门语言把代码当作数据。
各种语言
Scheme
- 由 Guy Steele 和 Gerald Sussman 于 20 世纪 70 年代在 MIT 人工智能实验室创造。
- 最初是为了通过一个极小的 Lisp 解释器来理解 Carl Hewitt 的 Actor 模型。
- 这门语言本身是在一系列人工智能研究备忘录(AI Memos)中提出的,这些备忘录后来被统称为 Lambda 论文(Lambda Papers)。
- 第一个使用词法作用域的 Lisp 方言(值只在其定义所在的作用域内可见),也是最早支持一等续延的语言之一。
- 有官方的 IEEE 标准,还有一个事实上的标准,叫做《算法语言 Scheme 修订报告》(Revised Report on the Algorithmic Language Scheme,RnRS)。
- 实现很多:ChezScheme、Guile(这两个 Exercism 都支持)、MIT/GNU Scheme 和 Racket
- 语言非常精简,语法很少,但这并非有意为之。
- 作者本想构建一个复杂的东西,最终却设计出了比预想简单得多的东西
- 正规尾递归。做迭代的惯用方式是通过递归。
- Scheme 会优化尾递归调用,使其不消耗栈空间或其他资源。这意味着递归可以用于任意大的数据,或任意长的计算。
- 强大的数值类型,包括有理数和复数
- 延迟求值,有点像 promise。
- 强大的宏系统。
- 卫生宏能降低定义宏时出现意外结果的可能性。
Common Lisp
- Common Lisp 的工作始于 1981 年,由 ARPA 主管 Bob Engelmore 发起,目的是开发一个单一的社区标准 Lisp 方言,因为当时使用的各种方言常常互不兼容,导致代码和知识无法共享
- 第一个标准于 1984 年发布,最终版本于 1994 年发布(规范非常稳定)
- 由于它是一份标准,所以有各种不同的实现,例如 Steel Bank Common Lisp(Exercism 的默认实现)和 CLisp。
- 也有商业实现,例如 Allegro CL 和 LispWorks,还有可以嵌入 C 程序的 ECL(Embeddable Common Lisp),以及在 Java 虚拟机上运行的 ABCL。
- 由标准定义(ANSI INCITS 226-1994),所以 30 年前写的代码今天仍能正常运行
- 丰富且可扩展的类型系统
- 为镜像(image)和 REPL 开发而设计,因此非常便于内省。
Emacs Lisp
- 于 1985 年开发,目的是拥有一门高效的语言来扩展文本编辑器
- 动态类型
- Emacs 约 80% 是用 Emacs Lisp 写的(出于性能考虑,20% 用 C)
- 和其他 Lisp 有点不同:
- 没有标准化,仍在缓慢演进
- 没有自动的尾调用消除,可通过 named-let 宏支持(会转换成 while 循环)
- 默认使用动态作用域,建议新代码使用词法作用域
- 编辑器内置了不错的文档
- 多平台(凡是有 Emacs 的地方都能运行)
- 通过阅读你每天使用的功能的代码来学习这门语言(Emacs 核心 + 各种包)
- 通过 cl-lib 包可以使用 Common Lisp 的一个子集。Emacs Lisp 相当精简,而 Common Lisp 的功能则丰富得多。cl-lib 包让 Common Lisp 的一个子集变得可用。
Racket
- Matthias Felleisen 创办了 PLT Inc.,该公司于 1995 年 1 月决定开发一个基于 Scheme 的教学编程环境。它最初叫 PLT Scheme,后来更名为 Racket。
- 除了是教学编程环境,它还被设计成编程语言设计与实现的平台。
- 现代的 LISP,Scheme 的后裔
- 支持逻辑编程!
- 语法简单而富有表现力,既适合初学者,在专家手中又很强大
- 支持多种编程范式:函数式编程、面向对象编程、契约式设计、逻辑编程、元编程
- 全面的标准库
- 附带 DrRacket,一个完整的 IDE,专为学习和探索而设计,尽量少给你添麻烦
- 出色的文档,包含大量背景信息和示例
Clojure
- 由 Rich Hickey 开发,目标是拥有一门运行在 JVM 上、并发能力出色的现代 LISP
- 它是 LISP 的方言,但和其他 LISP 也有些不同:它不支持隐式尾递归(不知道这是什么也没关系),而且除了列表之外还有更多数据结构:映射、集合、向量。这些数据结构都有自己的字面量语法。
- 它们还都是不可变的,但性能依然很好,查找复杂度为 O(log32 n),可以说是“近似”常数时间
- 通过多重方法和协议实现运行时多态
- 与 JVM 的互操作很好
- Clojure Spec 数据规范系统(运行时,而非编译期),允许你定义数据结构、生成数据、进行基于属性的测试等等
使用场景
Scheme
- 用于教育,帮助教授计算机科学(颇具影响力的《计算机程序的构造和解释》就使用了 Scheme)。
- 用于人工智能。也用作脚本语言,例如在 GIMP(图形编辑器)、CAD 工具(计算机辅助设计)中,甚至电影里,比如《最终幻想:灵魂深处》的渲染引擎管理脚本。
Common Lisp
- Common Lisp 被用在很多地方,例如人工智能和研究,也有商业应用:NASA 用 Common Lisp 为“深空一号”探测器编写了自动驾驶软件;Viaweb 是用 Common Lisp 写的,后来被 Yahoo 收购并改名为 Yahoo Store!;还有第一版的 Reddit。
Emacs Lisp
- Emacs Lisp 用在,嗯,Emacs 里!
- 从本质上讲,Emacs 是 Emacs Lisp 的解释器,Emacs Lisp 是 Lisp 编程语言的一个方言,但增加了支持文本编辑的扩展。
Racket
- 用于教育,因为 Racket 的设计重点就是支持语言的创建、简化和分析。
- 用于研究,因为它可扩展的语法和语义适合设计和原型化新的语言及语言特性。
- 用于游戏,例如 John Carmack(《毁灭战士》的作者)在 VR 的交互式脚本环境中使用了它,开发商 Naughty Dog 也用它来做脚本(例如在《神秘海域》中)。Hacker News 是用 Arc 写的,Arc 也是一门 Lisp,而它本身是用 Racket 写的。
Clojure
- Clojure 用于许多不同的领域,例如 Docker 在 2022 年收购了 Atomist,后者是一个用 Clojure 实现的容器安全和自动化平台。
- 世界上最大的 Clojure 用户是 Nubank,一家新型银行,它在几年前进行了相关收购,如今还聘用了 Clojure 核心团队。
- 它被大量用于快速原型开发,因为它动态且高度交互。
编程视角
所有语言都支持函数式、命令式和符号式范式。 其中一些还支持面向对象编程,尤其是 Common Lisp。
Lisp 大多是动态语言,不过 Racket 支持静态类型。
这并不意味着它们全都是解释型的,因为情况是混合的:有解释执行(没有任何编译步骤)、编译成字节码再解释执行,以及直接编译成机器码这几种。
Scheme
- 极简,语义清晰简单,构成表达式的方式很少。
- 便于学习这门语言、理解代码。
- 正因如此,Scheme 也常被用于许多计算机科学入门课程
- 一等续延。
- 续延是程序状态的一种表示。
- 续延可以用来对控制流建模(例如
return结构),也可以用来实现协程(允许多任务)。
Common Lisp
- 可扩展的面向对象系统,带有可编程的方法组合(既包括子类/超类方法如何组合,也包括 before、after 和 around 方法,从而可以让系统在不修改自身的情况下得到扩展)
- 可编程的条件系统(是“异常”的超集),它能把条件的识别与处理方式的选择解耦。条件系统比异常系统更灵活,因为它不是把职责分成两部分(发出错误的代码和处理它的代码),而是把职责分成三部分:发出条件、处理条件、重启。
- 宏可以扩展语言的语法,而不只是生成样板代码。这有助于你打造一门贴合业务领域的语言,而不是反过来。
Emacs Lisp
- 出色的编辑器支持和集成
- 可以在 Emacs 运行时定制它(“就像给自己做脑部手术” :))
- 可以用于批处理模式,此时编辑器的文本处理能力你都可以使用(例如缓冲区和移动命令)
Racket
- 强大的宏系统。像线程宏这样的语法糖就建立在此之上。宏还是卫生的,这是对一个简单问题的回答:宏生成的代码会被放到别的地方。当这些代码被求值时,我们该如何确定其中标识符的绑定?卫生宏能降低定义宏时出现意外结果的可能性。
- 面向语言。
- Racket 自带编写你自己的编程语言或 DSL 的工具,这些都建立在 Racket 的宏之上。
- 内置了多种语言,例如 typed Racket(支持静态检查的类型标注)、datalog(一种类似 Prolog 的语言,DrRacket IDE 支持它),以及 scribble,一个用于创建 HTML 或 PDF 格式文档的工具。
- REPL 是开发工作流的核心部分,不只是用来试东西、查文档
Clojure
- 强大的宏系统。
- 像线程宏这样的语法糖就建立在此之上
- REPL 是开发工作流的核心部分,不只是用来试东西、查文档
该试哪一个
- 如果你从未尝试过 Lisp,Scheme 和 Racket 是不错的选择,因为它们的语法都非常精简。
- 话虽如此,Common Lisp 和 Clojure 都有学习模式,所以它们大概是 Exercism 上最适合入门学习的。
- 如果你已经在用 Emacs,Emacs Lisp 是自然而然的选择。
- 同样,如果你在用 JVM 语言,Clojure 也是自然而然的选择。
- Emacs Lisp(通过 Emacs)、Clojure(通过 IntelliJ)和 Racket(通过 DrRacket)都有出色的 IDE 支持。
- 当然,Common Lisp 和 Scheme 也有不错的 IDE。
- 如果你想要一个功能非常齐全的 Lisp,Common Lisp、Clojure 和 Racket 都非常丰富
- 如果你想要一个有点不一样的 Lisp,Clojure 有一些对 Lisp 而言相当独特的语法。
- 如果你对宏和元编程感兴趣,它们基本上都是不错的选择!但如果你想构建新语言,Racket 尤其出色
当然,如果你有时间,我建议你试上几种! 还有,不要害怕那些括号!我知道我以前就怕,所以我把学 Lisp 拖了很久。 不过你很快就会习惯它们,说不定还会像我一样开始欣赏它们。 事实上,我现在很喜欢 Lisp 系语言:语法精简,语义简单,却依然极富表现力。