【QCon】精华内容上线92%,全面覆盖“人工智能+”的典型案例!>>> 了解详情
写点什么

通过 Lisp 语言理解编程算法:Lisp 速成课程

  • 2019-08-19
  • 本文字数:9822 字

    阅读完需:约 32 分钟

通过 Lisp 语言理解编程算法:Lisp 速成课程

阅读完本章节后,你将会对 Lisp 写出的代码是什么样的有一个直观的认识。为什么 Lisp 代码如此短呢?就是因为 Lisp 使用 “自下而上” 的编程方法。你不是在基础语言上开发,而是在基础语言上构件一种你自己的语言,然后再用后者开发。你要是不能想象 Lisp 语言的代码是什么样,可以试着想象 XML,想象 XML 中的每个节点都是函数和自变量,而且可以执行。(Lisp 的代码都是嵌套和递归的,编译后就是一颗解析树。没有数据和代码之分,而且是动态类型语言。) Lisp 在所有语言里,具有最高的抽象层次,编程能力最强。(这里的抽象指编程语言本身的抽象,不是对待编程物的抽象。)Lisp 最突出的特点是“代码即数据,数据即代码”。Lisp 构建计算单元的方式尊崇由具体微小入手,像搭积木一般逐步构建规模,这点非常契合人类思维,并且让代码的呈现方式也契合这种方式,便于阅读,赏心悦目。



我预计本书将有两个读者群:


  • 希望在算法和编写高效程序方面取得进步的读者——这正是本书的主要读者群。

  • 使用 Lisp 的读者,无论他们是熟练的还是有抱负的,他们碰巧也对算法深感兴趣。


本章节主要针对第一个读者群。读完这个章节之后,你应该能够理解本书中其余部分的 Lisp 代码了。此外,如果你愿意的话,你还可以了解运行 Lisp 的基本知识并对其进行实验。


对于 Lisp 用户来说,你可能有兴趣阅读这一章节,不过,这么做只是为了熟悉我在本书中使用这种语言的风格。另外,你还会发现我对评论中多次提到的问题的立场:使用某些第三方扩展是否合理,以及在多大程度上,作者应该谨慎地只坚持使用标准提供的工具。

Lisp 的核心


东西:它们是预先定义的、始终存在的、不可变的。这些都是构件块,在其上构建所有其他操作符,包括顺序块操作符 block、条件表达式 if 和无条件跳转 go 等。如果 oper 是一个特殊操作符的名称,则执行此运算符的底层代码,该代码以自己独特的方式处理参数。


  • 还有普通函数的求值:如果 oper 是函数名,首先,使用相同的求值规则计算所有参数,然后用得到的值调用函数。

  • 最后就是宏(macro)求值。宏提供了一种更改特定表单求值规则的方法。如果 oper 为宏命名,则用它的代码代替表达式,然后进行求值。宏是 Lisp 中的主要内容,它们用于构建语言的大部分,并为用户提供了一种可访问的方式来扩展 Lisp 语言。但是,它们与本书的主题相互独立、完全不同的,因此,本书不再详细讨论。但你可以在 On LispLet Over Lambda 这样的书籍中深入研究 Lisp 的宏。


需要注意的是,在 Lisp 中,语句和表达式之间并没有区别,没有特殊的关键字,没有操作符优先规则,以及在其他语言中可能会遇到的其他类似的任意东西。一切都是统一的:从某种意义上说,一切都是表达式,它将被求值并返回一些值。

代码示例

综上所述,让我们考虑一个 Lisp 表达的求值示例。下面的示例代码实现了著名的二分搜索算法(binary search algorithm)(我们将在下一章中详细讨论):


(when (> (length vec) 0)  (let ((beg 0)        (end (length vec)))    (do ()        ((= beg end))      (let ((mid (floor (+ beg end) 2)))        (if (> (? vec mid) val)            (:= beg (1+ mid))            (:= end mid))))    (values beg            (? vec beg)            (= (? vec beg) val))))
复制代码


它是一种复合表单。其中,所谓的顶级表单是 when,它是一个宏,用于一个单子句条件表达式:一个只有 trye- 分支的 if。首先,它对表达式 (> (length vec) 0) 进行求值,这是一个应用于两个参数的逻辑操作符 > 的普通函数:得到的结果是变量 vec 的内容长度和一个常数 0。如果求值返回 true,那就说明 vec 的长度大于 0,则表单的其余部分将以相同的方式进行求值。如果没有异常发生,则求值结果要么为 false(在 Lisp 中为 nil),要么为从最后一个表单返回的 3 个值 (values…)。而 ? 是通用访问操作符,它通过不同的方式抽象来按键查询数据结构。在本例中,它从第二个参数的索引处 vec 中检索项。下面我们将讨论这里提到的其他操作符。


但首先我要谈一谈 RUTILS。它是一个第三方库,为标准 Lisp 语法及其基本操作符提供了许多扩展。它存在的原因是 Lisp 标准永远不会改变,而且,正如这个世界上的任何事物一样,它也有它的缺陷。此外,我们对优雅高效的代码的理解也随着时间的推移而不断发展。然而,Lisp 标准的最大优势在于,作者从最基本的语法开始,几乎在所有级别上都采用了多种方法来修改和发展语言,从而抵消了 Lisp 不可变性的问题。这样一来解决了我们的最终需求,毕竟:我们对改变标准远不如对改变语言那样感兴趣。因此,RUTILS 是 Lisp 的进化方式之一,其目的是在不损害语言原则的前提下,使 Lisp 的编程更易于理解。因此在本书中,我将使用 RUTILS 中的一些基本扩展,并将根据需要解释它们。当然,使用第三方库是个人偏好和品味的问题,可能不会被某些旧版本的 Lisp 所认可,但不必担心,在你的代码中,你完全可以轻松将它们替换为你喜欢的替代库。

REPL

Lisp 程序不仅应该以简单脚本的一次性方式运行,而且还应该作为实时系统运行,这些实时系统不仅需要长时间运行,还要经历数据的更改、代码的更改。这种与程序交互的一般方式称为“读取-求值-输出”循环(Read-Eval-Print-Loop,REPL),字面意思是 Lisp 编译器 read 一个表单,用上述规则对其进行 eval,将结果 print 回给用户,然后 loop


REPL 是与 Lisp 程序交互的默认方式,它与 Unix shell 非常相似。当你运行 Lisp 时(例如,通过在 shell 中输入 sbcl),你将会进入 REPL。在本书中,我将在所有基于 REPL 的代码交互之前使用 REPL 提示(CL-USER> 或类似的提示)。下面是一个例子:


CL-USER> (print "Hello world")"Hello world" "Hello world"
复制代码


好奇的读者可能会问,"hello world" 为什么打印两次?这证明了在 Lisp 中,一切都是表达式。与大多数其他语言不同,print 语句不仅将其参数打印到控制台(或其他输出流),但也会按原样返回。这在调试时非常方便,因为你可以在不更改程序流程的情况下,将几乎任何表单封装到一个 print 中。


显然,如果不需要交互的话,那么只需“读取-求值”部分即可。但是,更重要的是,Lisp 提供了一种方法来自定义流程的每个阶段:


  • read 阶段,可以通过称为读取宏(reader macro)机制引入特殊语法(“syntax sugar”)。

  • 普通宏是自定义 eval 阶段的一种方法。

  • 从概念上来讲,print 阶段是最简单的阶段,并且还有一种通过公共 Lisp 对象系统(Common Lisp Object System,CLOS)的 print-object 函数定制对象输出的标准方法。

  • 并且,loop 阶段可以被所需的任何程序逻辑所取代。


译注:syntax sugar,语法糖,由英国计算机科学家 Peter John Landin 发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便进程员使用。通常来说使用语法糖能够增加进程的可读性,从而减少进程代码出错的机会。

基本表达式

结构化编程范式指出,所有程序都可以用 3 种基本结构表示:顺序执行、分支和循环。让我们看看这些操作符在 Lisp 中是如何表示的。

顺序执行

顺序执行是最简单的程序流程。在所有命令式语言中,如果将多个表单放在一行中并对生成的代码块进行求值,则会出现这种情况。像这样:


CL-USER> (print "hello") (+ 2 2)"hello"4
复制代码


最后一个表达式返回的值将整个序列的值“变暗”。


这里,REPL- 交互表单形成了隐式顺序代码单元。然而,在许多情况下,我们需要明确界定这些单元这可以通过 block 操作符来完成:


CL-USER> (block test           (print "hello")           (+ 2 2))"hello"4
复制代码


这样的块有一个名称(本例中为 test)。这允许通过使用操作符 return-from 提前结束执行:


CL-USER> (block test           (return-from test 0)           (print "hello")           (+ 2 2))0
复制代码


一个简短的 reture 用于从名称为 nil 的块中退出(在我们将进一步讨论的大多数循环结构中都是隐式的):


CL-USER> (block nil           (return 0)           (print "hello")           (+ 2 2))0
复制代码


最后,如果我们甚至不打算过早地从一个块返回,可以使用不需要名称的 progn 操作符:


CL-USER> (progn           (print "hello")           (+ 2 2))"hello"4
复制代码

分支

条件表达式计算它们的第一个表单的值,并根据它执行几个可选代码路径之一。基本条件表达式是 if


CL-USER> (if nil             (print "hello")             (print "world"))"world"
复制代码


正如我们所见,nil 在 Lisp 中用来表示逻辑的 “false”。所有其他值在逻辑上都被认为是 “true”,包括符号 Tt ,它直接就代表了 “true” 的含义。


当我们需要一次做几件事时,在其中一个条件分支中,这是我们需要使用 prognblock 的情况之一:


CL-USER> (if (+ 2 2)             (progn               (print "hello")               4)             (print "world"))"hello"4
复制代码


然而,我们通常不需要表达式的两个分支,也就是说,我们并不在乎条件不成立(或成立)时会发生什么。这是一种非常常见的情况,在 Lisp 中有它的特殊表达式: whenunless


CL-USER> (when (+ 2 2)           (print "hello")           4)"world"4CL-USER> (unless (+ 2 2)           (print "hello")           4)NIL
复制代码


正如你所看到的,它也很方便,因为你不必显式地将顺序表单封装在 progn 中。


另一个标准条件表达式是 cond,当我们想要连续求值几个条件时就使用它:


CL-USER> (cond           ((typep 4 'string)            (print "hello"))           ((> 4 2)            (print "world")            nil)           (t            (print "can't get here")))"world"NIL
复制代码


如果前面的条件都不起作用(因为它的条件总是 “true” 的话)那么 t 情况就是一个全面控制(catch-all),将被触发。上面的代码相当于下面的代码:


(if (typep 4 'string)    (print "hello")    (if (> 4 2)        (progn          (print "world")          nil)        (print "can't get here")))
复制代码


在 Lisp 中还有更多的条件表达式,用宏来定义自己的条件表达式非常容易(实际上,是关于使用 whenunlesscond 来如何定义的问题),当需要使用特殊表达式时,我们将讨论它的实现。

循环

与分支一样,Lisp 也具有丰富的循环结构,并且在必要时也很容易定义新的结构。这种方法不同于主流语言,主流语言通常只有少量这样的语句,有时还通过多态性提供扩展机制。它甚至被认为是一种“美德”,因为它对初学者而言,不那么令人困惑。这在一定程度上还是有意义的。不过,在 Lisp 中,通用方法和自定义方法都可以共存并相互补充。然而,定义自定义控件结构的传统非常强大。为什么呢?其中一个理由就是与人类语言相似:实际上,whenunless,以及 dotimesloop 都是直接来自人类语言中的单词,或者是来自自然语言表达。我们的母语并没有那么原始和枯燥。另一个原因就是因为你可以。也就是说,在 Lisp 中定义自定义语法扩展要比其他语言中容易得多,有时简直让人无法抗拒。在许多用例中,它们使代码变得更加简单明了。


无论如何,对于一个完全的初学者来说,实际上,你必须知道与任何其他语言差不多的迭代结构。最简单的是 dotimes,它将计数器变量迭代给定次数(从 0 到 (- times 1)),并在每次迭代中执行主体。它类似于 C 语言中的 for (int i = 0; i < times; i++) 循环。


CL-USER> (dotimes (i 3)           (print i))012NIL
复制代码


尽管返回值可以在循环头部中指定,但默认情况下,返回值为 nil


另一方面,最通用(和底层)的循环结构是 do


CL-USER> (do ((i 0 (1+ i))              (prompt (read-line) (read-line)))             ((> i 1) i)           (print (pair i prompt))           (terpri))foo
(0 "foo") bar
(1 "bar")
2
复制代码


do 迭代在第一部分(本例中是 iprompt)中定义的多个变量(零或更多),直到满足第二部分的终止条件(本例中是 (> i 1)),与 dotimes(以及其他 do-style 宏)一样,执行它的主体——其余的表单(本例中是 printterpri,是打印换行符的简写)。read-line 从标准输入读取,直到遇到换行符,并且 1+ 返回 i 的当前值增加 1。


所有 do-style 宏(有很多这样的宏,既有内置的,也有外部库提供的:dolistdotreedo-register-groupsdolines 等)都有一个可选的返回值。在 do 中,它遵循终止条件,在本例中,只返回 i 的最终值。


除了 do-style 的迭代外,CL 生态系统中还有一个大不相同的“猛兽”:臭名昭著的 loop 宏。它用途非常广泛,尽管在语法方面有点不顺畅,并且有一些令人惊讶的行为。但是详细阐述它已经超出了本书的范畴,特别是因为在 Peter Seibel 的《LOOP for Black Belts》中关于 loop 有一个很不错的介绍。


许多语言提供了一个通用循环结构,它能够迭代任意序列、生成器和其他类似的行为,通常是 foreach 的一些变体。在更详细地讨论序列之后,我们将回到这种结构。


此外还有另一种迭代射血:函数式迭代,基于高阶函数(mapreduce 和类似的函数)——我们也将在接下来的章节中对其进行更为详细的介绍。

过程和变量

我们已经讨论了结构化变成的三大支柱,但其中一个重要的,实际上也是最重要的结构仍然是变量和过程。


如果我告诉你,你可以多次执行相同的计算,但是要改变一些参数的话……好吧,好吧,这个差劲的玩笑。因此,过程是重用计算的最简单方法,并且过程接受参数,允许将值传递到他们的主体中。在 Lisp 中,过程称为 lambda。可以这样定义一个: (lambda (x y) (+ x y))。当使用时,这样的过程——通常也被称为函数,尽管它与我们所认为的数学函数大不相同,并且在这种情况下,它被称为匿名函数,因为它没有任何名称,将产生期输入的总和:


CL-USER> ((lambda (x y) (+ x y)) 2 2)4
复制代码


通过完整的代码签名来引用过程是相当麻烦的,显而易见的解决方案是为它们指定名称。在 Lisp 中,一个常见的方法是通过 defun 宏:


CL-USER> (defun add2 (x y) (+ x y))ADD2CL-USER> (add2 2 2)4
复制代码


过程的参数是变量的例子。变量用于命名存储单元(memory cells),这些单元的内容被多次使用,并且可能在过程中被更改。他们有不同的用途:


  • 将数据传递给程序

  • 作为代码块中某些变化数据的临时占位符(如循环计数器)

  • 作为存储计算结果以便进一步重用的一种方法

  • 定义程序配置参数(如 OS 环境变量,也可以将其视为程序主函数的参数)

  • 引用应该可以从程序中的任何地方访问的全局对象(如 *standard-output* 流)

  • 还有更多


我们可以没有变量吗?从理论上来说,也许可以。至少,编程中有一种所谓的“无点”风格(point-free style),就强烈反对使用变量。但是,就像他们所说,不要在工作中尝试这个(至少在你完全明白你在做什么之前)。我们是否可以用常量或者单赋值变量来替换变量,即不能随时间变化的变量?这种方法是由所谓的纯函数语言所提倡的。在某种程度上来说,是这样。但是,从算法开发的角度来看,它使许多优化变得复杂了,即使没有完全超越它们,也会令开发者头疼。


那么,如何在 Lisp 中定义变量呢?你已经看到了一些变体:过程参数和 let-bindings。用 Lisp 的话说,这样的变量叫做局部变量或词法变量(lexical variable)。这是因为在整个代码块的执行过程中,它们只能在本地访问,在代码中定义它们。let 是引入此类局部变量的一般方法,它是伪装的 lambda(“在它上面有一层薄薄的语法糖”):


CL-USER> (let ((x 2))           (+ x x))4CL-USER> ((lambda (x) (+ x x))          2)4
复制代码


使用 lambda,你可以在一个地方创建一个过程,可能的话,也许可以将它分配一个一个变量(本质上就是 defun 所做的),然后在不同的地方多次应用,让你定义一个过程并立即调用它,这样就没有办法存储它并在以后再次重新应用。这甚至比匿名函数更加匿名!而且,它还不需要编译器的额外开销。但机制是相同的。


通过 let 创建变量称为绑定(binding),因为他们会立即被赋值(绑定)。可以一次绑定多个变量:


CL-USER> (let ((x 2)               (y 2))           (+ x y))4
复制代码


但是,我们通常需要使用前一个变量的值来定义一行变量和下一个变量。使用 let 很麻烦,因为需要嵌套(因为过程参数是独立分配的):


(let ((len (length list)))  (let ((mid (floor len 2)))    (let ((left-part (subseq list 0 mid))          (right-part (subseq list mid)))      ...)))
复制代码


为了简化这个用例,我们用 let 来演示:


(let* ((len (length list))       (mid (floor len 2))       (left-part (subseq list 0 mid))       (right-part (subseq list mid)))  ...)
复制代码


然而,还有许多其他方法可以定义变量:一次绑定多个值;当数据结构(通常是列表)的内容被分配给多个变量时,执行所谓的“析构(destructuring)绑定”,第一个元素分配给第一个变量,第二个元素分配给第二个变量,以此类推;访问某个结构的槽(slots)等。对于这样的用例,有来自 RUTILS 的 with 绑定,其工作方式类似于 let,具有额外的功能。这里有一个非常简单的例子:


(with ((len (length list))       (mid rem (floor len 2))       ;; this group produces a list of 2 sublists       ;; that are bound to left-part and right-part       ;; and ; character starts a comment in lisp       ((left-part right-part) (group mid list))) ...
复制代码


在本书的代码中,你将只看到这两种绑定结构:let 用于普定绑定和并行绑定,以及 with 用于其余所有绑定。


正如我们所说的,变量不仅可以被定义,或者它们可以被称为“常量“,而且还可以被修改。要改变变量的值,我们将使用来自 RUTILS 的 :=(它是标准 psetf 宏的缩写):


CL-USER> (let ((x 2))           (print (+ x x))           (:= x 4)           (+ x x))48
复制代码


一般来说,修改是一种危险的够早,因为它可能会产生意想不到的“超距作用”效果,当在代码的某个位置更改变量的值时,会影响使用相同变量的不同部分的执行。然而,这不可能发生在词法变量上:每个 let 都创建自己的作用域,以保护前面的值不被修改(就像将参数传递给过程调用,并在调用中修改它们不会改变调用代码中那些值一样):


CL-USER> (let ((x 2))           (print (+ x x))           (let ((x 4))             (print (+ x x)))           (print (+ x x)))484
复制代码


显然,当两个 let 在不同的地方使用同一个变量名时,它们不会相互影响,这两个变量实际上是完全不同的。


然而,有时在一个地方修改变量,然后在另一个地方查看效果还是有用的。具有这种行为的变量称为全局变量或动态变量(在 Lisp 术语中也称为特殊变量)。它们有几个重要的目的。其中之一是定义需要在任何地方都可访问的重要配置参数。另一个是引用通用单例对象,如标准流或随机数生成器的状态。还有一个是指向某些上下文,这些上下文可以根据特定过程的需要在某些地方进行更高(,*package* 全局变量确定我们在哪个包中操作——前面所有的示例中的 CL-USER)。全局变量也有更高级的用法。定义全局变量的常用方法是使用 defparameter,它指定全局变量的初始值:


(defparameter *connection* nil  "A default connection object.")  ; this is a docstring describing the variable
复制代码


在 Lisp 中,全局变量通常在其名称周围有所谓的“耳罩”,以提醒用户他们正在处理的是什么内容。由于它们的超距作用效果,它并不是最安全的编程语言特性,甚至还有一句“全局变量被认为是有害的”咒语。但是,Lisp 并不是那种“娇气”的语言,它发现了许多特殊变量的用途。顺便说一句,它们之所以被称为是“特殊的”,是因为它们有一个特殊的功能,极大地拓宽了它们正常使用的可能性:如果将它们绑定在 let 中,则它们将充当词法变量,也就是说,即前一个值在离开 let 主体时被保留和恢复:


CL-USER> (defparameter *temp* 1)*TEMP*CL-USER> (print *temp*)1CL-USER> (progn           (let ((*temp* 2))             (print *temp*)             (:= *temp* 4)             (print *temp*))           *temp*)241
复制代码


Lisp 中的过程是一类对象。这意味着你可以将其分配给变量,以及在运行时检查和重新定义,因此可以使用它来做许多其他有用的操作。RUTILS 函数 call 将调用作为参数传递给它的过程:


CL-USER> (call 'add2 2 2)4CL-USER> (let ((add2 (lambda (x y) (+ x y))))           (call add2 2 2))4
复制代码


注:call 是标准 funcall 的 RUTILS 缩写。在 20 世纪 60 年代,从变量中调用函数肯定很有趣,但现在它变得如此普遍,以至于不需要前缀了。


实际上,使用 defun 定义函数也会创建一个全局变量,尽管是在函数名称空间中。函数、类型、类——所有这些对下你给通常都定义为全局对象。不过,对于函数,有一种方法可以用 flet 在本地定义它们:


CL-USER> (foo 1);; ERROR: The function COMMON-LISP-USER::FOO is undefined.CL-USER> (flet ((foo (x) (1+ x)))           (foo 1))2CL-USER> (foo 1);; ERROR: The function COMMON-LISP-USER::FOO is undefined.
复制代码

注释

最后,还有一个语法我们需要只到:如何在代码中添加注释。只有失败者才不会注释他们的代码,在本书中,注释将会被广泛使用,贯穿本书,来解释代码示例的某些部分。Lisp 中,注释以 ; 字符开头,以行位结束。因此,下面的代码段是一个注释: ; this is a comment。还有一种常见的注释风格,即当前代码行之后的简短注释以单个 ; 开头,如果某个代码块前面有较长的注释,占据整行或多行,并以 ;; 开头。包含多个 LISP 顶级表单(全局定义)的代码部分的注释以 ;;; 开头,也占用整行。此外,每个全局定义都可以有一个特殊的类似注释的字符串,称为“文档字符串”(docstring),用于描述其用途和用法,并且可以通过编程查询。综上所述,不同的注释可能是这样子的:


;;; Some code section
(defun this () "This has a curious docstring." ...)
(defun that () ... ;; this is an interesting block don't you find? (block interesting (print "hello"))) ; it prints hello
复制代码

入门指南

我强烈建议你尝试使用本书后面章节中的代码。并试着改进这些代码,找出问题,并相处解决方案,测量和跟踪一切。这不仅能够帮助你掌握一些 Lisp 技能,而且还能更深入理解所讨论的算法和数据结构的描述、他们的缺陷以及极端情况。事实上,做到这一点相当容易。你需要做的就是安装一些 Lisp(最好是 SBCL 或 CCL),添加 Quicklisp,并在其帮助下添加 RUTILS。


正如我上面所说,使用 Lisp 的通常方法是与其 REPL 进行交互。运行 REPL 相当简单,在我的 Mint Linux 上,运行以下命令:


$ apt-get install sbcl rlwrap...$ rlwrap sbcl...* (print "hello world")
"hello world" "hello world"*
复制代码


* 是 Lisp 的原始提示符。它基本上和你在 SLIME 中看到的 CL-USER> 提示符是一样的。你还可以运行 Lisp 脚本文件: sbcl --script hello.lisp。如果它只包含一行 (print "hello world"),我们将会看到“hello world”短语被打印到控制台上。


这是一个有效的设置,但并不是最方便的设置。一个更高级的环境是在 Emacs 内部运行的 SLIME(类似于 vim 的项目,称为 SLIMV)。还有许多其他解决方案:一些 Lisp 实现提供了集成开发环境(IDE),某些 IDE 和编辑器也提供了集成。


进入 REPL 后,你必须发出以下命令:


* (ql:quickload :rutilsx)* (use-package :rutilsx)* (named-readtables:in-readtable rutilsx-readtable)
复制代码


好了,以上就是你需要知道的 Lisp 知识,已经足以开始了。我们将熟悉其他 Lisp 概念,因为它们将在本书下一章中用到。但是,你现在就可以准备好读写 Lisp 程序了。一开始,它们可能看起来很陌生的感觉,但当你克服了最初的障碍并习惯它们的古怪的前缀表层语法,我保证,你将能够看懂并欣赏它们的清晰和简洁。


所以,就像他们在 Lisp 岛上说的那样,快乐去探险吧!


作者介绍:


Vsevolod Dyomkin,Lisp 程序员,居住在乌克兰基辅。精通乌克兰语、俄语和英语。目前正在撰写关于 Lisp 的书籍 Programming Algorithms in Lisp,该书将使用 CC BY-NC-ND 许可(创作公用许可协议),供公众免费阅读。


原文链接:


LISP, THE UNIVERSE AND EVERYTHING


相关文章:


《通过 Lisp 语言理解编程算法:简介和复杂度》


《通过 Lisp 语言理解编程算法:数据结构篇》


《通过 Lisp 语言理解编程算法:数组篇(上)》


《通过 Lisp 语言理解编程算法:数组篇(下)》


《通过 Lisp 语言理解编程算法:链表篇(上)》


2019-08-19 09:0913089
用户头像

发布了 367 篇内容, 共 168.8 次阅读, 收获喜欢 936 次。

关注

评论

发布
暂无评论
发现更多内容

手把手教你免费获取正版 Jetbrains 全家桶 License

郭旭东

ide JetBrains

社区团购大战升级:零售行业再次面临挑战

石头IT视角

破51项国际榜单纪录!解读华为云擎天架构调度求解引擎

华为云开发者联盟

华为 架构 华为云

XMEX交易所系统软件开发|XMEX交易所APP开发

系统开发

如何在软件发布计划中自动化语义化版本与变更日志

华为云开发者联盟

自动化 工具 发布

LeetCode题解:429. N叉树的层序遍历,BFS,JavaScript,详细注释

Lee Chen

算法 大前端 LeetCode

海淀区政府携手百度,打造数字政务时代新门户

DT极客

聚焦LS-MIMO的四大层面,浅谈5G关键技术

华为云开发者联盟

华为 5G 华为云

Mybatis是如何解析配置文件的?

田维常

mybatis

快来!开源一份阿里微服务指导手册:springBoot+springCloud+MQ

小Q

学习 面试 微服务 MQ SpringCloud

网络入门模拟器:Cisco Packet Tracer 实验教程

没弄懂这些Java基础,简历上千万别写熟悉:异常+反射+注解+泛型

小Q

Java 学习 编程 面试 基础

《构建高性能WEB站点》.pdf

田维常

架构

我叫你不要重试,你非得重试。这下玩坏了吧?

比伯

Java 编程 架构 面试 程序人生

基于LiteOS Studio零成本学习LiteOS物联网操作系统

华为云开发者联盟

操作系统 物联网 华为云

有了Git这个功能,再也不需要依赖IDE了!

编程 架构

BATJ面试常被问到的100+题:Spring+微服务+SpringMVC+MyBatis

Java架构之路

Java 程序员 架构 面试 编程语言

终于学完了阿里云大数据架构师推荐的Flink入门与实战PDF

小Q

大数据 flink 学习 编程 面试

BAT等大厂面试复习资料文档整理:ActiveMQ+redis+Spring+高并发多线程+JVM

Java架构之路

Java 程序员 架构 面试 编程语言

Athena雅典娜交易所系统开发|Athena雅典娜交易所软件APP开发

系统开发

一个改变世界的“箱子”

阿里巴巴云原生

Docker 阿里云 容器 云原生 k8s

探营苏州数字人民币试点

CECBC

数字人民币

终于有人通过笔记+脑图+视频的模式把Spring源码讲明白了

Java架构追梦

Java 学习 架构 面试 spring源码

你只修改了2行代码,为什么需要两天时间?

Java架构师迁哥

盘点 2020 | 坚持写技术博客一年能有多少收获!

小傅哥

Java 小傅哥 技术人 盘点2020

即构SDK12月迭代:新增多项质量回调,互动白板、云录制SDK同步更新

ZEGO即构

SpringBoot魔法堂:应用热部署实践与原理浅析

设计原则 框架 spring Boot Starter

工作日志:一文总结HBase从搭建到实操,大家一起进步

小Q

大数据 学习 编程 面试 HBase

阿里三面惨遭被虐,spring,jvm,mybatis,并发编程等一窍不通

Java架构之路

Java 程序员 架构 面试 编程语言

赶紧看!阿里架构师必备“绝杀版”Tomact架构笔记堪称绝技

比伯

Java tomcat 编程 架构 程序人生

互联网新规鼓励保险与大数据、区块链等新技术融合!业内呼吁配套产品管理制度尽快出炉

CECBC

互联网金融

通过 Lisp 语言理解编程算法:Lisp 速成课程_语言 & 开发_Vsevolod Dyomkin_InfoQ精选文章