天天看点

《Clojure编程乐趣》—— 第1章,第1.2节为何(又一种)Lisp

本节书摘来自异步社区《clojure编程乐趣》一书中的第1章,第1.1节1.2 为何(又一种)lisp,作者 【美】michael fogus , chris houser,更多章节内容可以访问云栖社区“异步社区”公众号查看

1.2 为何(又一种)lisp

clojure编程乐趣

一套好的概念可以将大脑从无用功中解放出来,专注于更高级的问题。

—alfred north whitehead

去到任何一个开源项目托管站点,搜索“lisp interpreter”(lisp解析器)。这个貌似平淡无奇的搜索,可能会带给我们一个堆积如山1**的结果。事实上,计算机科学的历史中堆积着大量废弃的lisp实现(fogus 2009)。诸多初衷良好的lisp来了又走,一路遭到无数嘲笑,但到了明天,搜索结果依然会无限制增长。既然有如此惨痛的过往,为何还有人愿意将其崭新的程序设计语言构建于lisp模型之上呢?

1.2.1 优美

lisp吸引了计算机科学史上最聪明的一群头脑。但是,仅有权威的争论是不够的,我们不该仅凭此判断lisp。只有用其编写应用,才可以直接看得到lisp语言家族的真正价值。lisp的风格就在于极具表现力、非常实用,以及在大多数情况下表现出的美感。最初的lisp语言是john mccarthy在其惊天动地的论文“recursive functions of symbolic expressions and their computation by machine, part i”(mccarthy 1960)中定义的,只用区区7个函数和两个特殊form便定义出整个语言:atom、car、cdr、cond、cons、eq、quote、lambda和label。

通过这9个form的组合,mccarthy将整个计算以一种令人窒息的方式呈现出来。计算机程序员总在寻找美,而多数情况下,美会以简单的形式自我呈现出来。7个函数2个特殊form,美无过于此。

1.2.2 极度灵活

lisp何以历经五十余年而弥新,相较之下,无数语言却成了匆匆过客?个中原因可能极尽复杂,但究其根因,无外乎lisp自身的语言基因(tarver 2008)将语言的灵活性推向极致。lisp新手常常气馁,无处不在的括号和前缀记法,与非lisp程序设计语言大相径庭。然而,正是这种行为上的规律性,不仅让需要记忆的语法规则减少了,也让宏的编写变得很简单。我们会在第8章更详细地了解宏,但为了让你开开胃,这里先简单地看一下。下面是一个例子,稍后再来细究:

《Clojure编程乐趣》—— 第1章,第1.2节为何(又一种)Lisp

希望你对这些单词有所了解,因为这不是一本sql的书。这里要说的是,clojure并没有内建对sql的支持。select、from等这些词并不是内建的form。它们也不是常规的函数,如果select是,那么使用a、b和c就错了,因为它们还没有定义。

如何用clojure定义这样的领域特定语言(domain-specific language,dsl)呢?好吧,这不是产品就绪(production-ready)的代码,没有绑定到真实的数据库服务器上;但只要有了程序1.1列出的一个宏和三个函数,前面的查询就能够返回下面这些实际的值:

《Clojure编程乐趣》—— 第1章,第1.2节为何(又一种)Lisp

请注意,from和on这样的词是从输入表达式中直接取出来的,而其他诸如~max和and则要特殊对待。调用查询时,max得到一个5,这是从字面量sql字符串中提取的,由一个单独向量提供,以这种方式准备的查询颇为完美,可以免受sql注入攻击。and form由clojure的前缀表达式转成sql所需的中缀表达式。

程序1.1 以clojure编写领域特定语言,用以嵌入sql查询

《Clojure编程乐趣》—— 第1章,第1.2节为何(又一种)Lisp

但需要指出的是,这算不上是一种很好的sql dsl—还有实现更为完整的。2我们要说的是,一旦懂得了这种创建dsl的技巧,就可以识别出一些机会,定义自己的dsl,解决比sql更窄的、更加面向应用的问题。无论是给不常见的非sql数据库提供查询语言,还是给模糊的数学学科提供一种方式表现函数,抑或是处理其他自己都未曾想过的应用,能够拥有如此灵活易扩展的基础语言,且不损伤语言自身特性,都将成为游戏规则的改变者。

虽然我们不该太过深入细节地讨论实现,但还是要顺着之前讨论过的一些重要方面,简单看看列表1.1的实现。

自下而上阅读,首先映入眼帘的是入口点,select宏。它返回的是一个有两项的vector—第一项通过调用 expand-clause生成,返回的是一个经过转换的查询字符串,而第二项是另一个vector,表示输入里由~标记的表达式。~表示反quote,我们会在第8章讨论其更常见的用法。另外要注意的是这里用到的tree-seq,通过它可以很容易地将感兴趣的项从值树(也就是输入表达式)上提取出来。

expand-clause函数用语句的第一个词,在clause-map里进行了查询,然后,调用适当的函数,完成从clojure的s表达式(s-expression)到sql字符串的转换。clause-map为sql表达式各个部分提供了所需的详细功能:插入逗号或是其他sql语法,有时还要递归调用expand-clause进行子语句的转换。其中之一是where语句,通过委托给expand-expr函数,处理了sql所需的前缀表达式到中缀表达式的通用转换。

总的来说,这个例子展示的clojure灵活性大多是因为宏可以接受代码form(比如前面展示的这个sql dsl的例子),并将其当做数据对待—遍历树、转换值等。之所以可以这样做,不只是因为代码可以当做数据,还因为在clojure程序里,代码就是数据。

1.2.3 代码即数据

“代码即数据”这样的说法最初很难理解。实现一门程序设计语言,代码同数据一般对待,这需要语言本身具有非常强的可塑性。当语言就是以这种本质的数据结构表现时,语言本身就可以操作自己的结构和行为了(graham 1995)。读到上面这句话,我们脑海中可能会浮现出一条衔尾蛇(ouroboros)3,也许这么说不合适,因为lisp可以比作一个自我舔食的棒棒糖—更正规的说法是同像性(homoiconicity)。要完全掌握lisp的同像性,需要跨越一个巨大的概念鸿沟,但在本书里,我们尽力帮你理解这个概念,希望你最终能够领会其巨大的威力。

初学lisp是一番乐趣,如果你能从本书得到同样的体验,那么我们欢迎你—甚至有点嫉妒。

1……且疯狂的。

3译注:衔尾蛇(ouroboros)是自古流传至今的一种符号,大致形象是一条蛇正吞食自己的尾巴,结果形成了一个圆环。