【译】Brave Clojure 第七章:Clojure炼金术:读取,求值,宏

本文是我对Clojure书籍 CLOJURE FOR THE BRAVE AND TRUE第七章Clojure Alchemy: Reading, Evaluation, and Macros 做的翻译。翻译形式,中英对照,英文引用跟着中文翻译。如有错误,在所难免,欢迎指正。

其他章的翻译在这里

如果你对Clojure不熟悉,可以先看一下第3章A Clojure Crash Course;如果对某个函数,宏,特殊形式不熟悉,可以查看coojuredocs的参考,每个文档都有例子,非常好。

译文开始。


The philosopher’s stone, along with the elixir of life and Viagra, is one of the most well-known specimens of alchemical lore, pursued for its ability to transmute lead into gold. Clojure, however, offers a tool that makes the philosopher’s stone look like a mere trinket: the macro.

Clojure提供了一种工具:宏。

Macros allow you to transform arbitrary expressions into valid Clojure, so you can extend the language itself to fit your needs. And you don’t even have to be a wizened old dude or lady in a robe to use them!

宏允许你把任意表达式转换成合法的Clojure,所以你能够扩展语言以符合你的需求。

To get just a sip of this power, consider this trivial macro:

看下这个宏的例子:

1
2
3
4
5
6
(defmacro backwards
[form]
(reverse form))

(backwards (" backwards" " am" "I" str))
; => "I am backwards"

The backwards macro allows Clojure to successfully evaluate the expression (" backwards" " am" "I" str), even though it doesn’t follow Clojure’s built-in syntax rules, which require an expression’s operand to appear first (not to mention the rule that an expression not be written in reverse order). Without backwards, the expression would fail harder than millennia of alchemists ironically spending their entire lives pursuing an impossible means of achieving immortality. With backwards, you’ve created your own syntax! You’ve extended Clojure so you can write code however you please! Better than turning lead into gold, I tell you!

虽然表达式(" backwards" " am" "I" str)不是合法的Clojure表达式,但使用backwards宏使Clojure能成功对它求值。相当于你用这个宏创造了自己的语法。

This chapter gives you the conceptual foundation you need to go mad with power writing your own macros. It explains the elements of Clojure’s evaluation model: the reader, the evaluator, and the macro expander. It’s like the periodic table of Clojure elements. Think of how the periodic table reveals the properties of atoms: elements in the same column behave similarly because they have the same nuclear charge. Without the periodic table and its underlying theory, we’d be in the same position as the alchemists of yore, mixing stuff together randomly to see what blows up. But with a deeper understanding of the elements, you can see why stuff blows up and learn how to blow stuff up on purpose.

这章讲述写宏需要用到的基础概念。解释了Clojure求值模型的三个方面:reader(读入程序),evaluator(求值程序),macro expander(宏展开程序)。

An Overview of Clojure’s Evaluation Model

Clojure求值模型概述

Clojure (like all Lisps) has an evaluation model that differs from most other languages: it has a two-phase system where it reads textual source code, producing Clojure data structures. These data structures are then evaluated: Clojure traverses the data structures and performs actions like function application or var lookup based on the type of the data structure. For example, when Clojure reads the text (+ 1 2), the result is a list data structure whose first element is a + symbol, followed by the numbers 1 and 2. This data structure is passed to Clojure’s evaluator, which looks up the function corresponding to + and applies that function to 1 and 2.

Clojure(与所有的Lisp一样)的求值模型与其他语言不同:由两步组成。第一步读入文本源码,产生Clojure数据结构。第二步求值这些数据结构。求值时候会遍历这些数据结构,并根据不同的类型进行不同的动作,比如变量查找,函数调用。比如源码(+ 1 2),读入后产生一个list数据结构,第一个元素是+这个symbol(符号),后面跟着1和2。之后这个数据结构传给Clojure求值程序,求值程序找到对应符号+的函数,然后用1和2作为参数,调用这个函数。

Languages that have this relationship between source code, data, and evaluation are called homoiconic. (Incidentally, if you say homoiconic in front of your bathroom mirror three times with the lights out, the ghost of John McCarthy appears and hands you a parenthesis.) Homoiconic languages empower you to reason about your code as a set of data structures that you can manipulate programmatically. To put this into context, let’s take a jaunt through the land of compilation.

源码,数据结构,求值是这样关系的语言被称为homoiconic语言(同像性语言)。(顺便提一下,如果你开着灯,在浴室镜子前面说三声homoiconic,约翰·麦卡锡会出现,并交给你一对圆括号。)同像性语言使你能把代码看成是可以通过程序操纵的数据,即传说中的的代码即数据语言。

Programming languages require a compiler or interpreter for translating the code you write, which consists of Unicode characters, into something else: machine instructions, code in another programming language, whatever. During this process, the compiler constructs an abstract syntax tree (AST), which is a data structure that represents your program. You can think of the AST as the input to the evaluator, which you can think of as a function that traverses the tree to produce the machine code or whatever as its output.

编程语言需要编译器或解释器把Unicode字符组成的源码转换成机器指令,其他语言的源码等等。这个过程中,编译器构建一个抽象语法树(AST)代表源程序。你可以把AST看成求值程序的输入,求值程序遍历这个树,产生机器码或其他输出。

So far this sounds a lot like what I described for Clojure. However, in most languages the AST’s data structure is inaccessible within the programming language; the programming language space and the compiler space are forever separated, and never the twain shall meet. Figure 7-1 shows how you might visualize the compilation process for an expression in a non-Lisp programming language.

在大多数语言里,编程语言是无法访问AST的,编程语言与编译器永远是隔离的。图7-1演示了非Lisp语言对一个表达式的编译过程。

图7-1图7-1

But Clojure is different, because Clojure is a Lisp and Lisps are hotter than a stolen tamale. Instead of evaluating an AST that’s represented as some inaccessible internal data structure, Lisps evaluate native data structures. Clojure still evaluates tree structures, but the trees are structured using Clojure lists and the nodes are Clojure values.

但Clojure不同,因为Clojure是Lisp。Lisp求值程序的求值对象不是不可访问的内部数据结构,而是源生数据结构。Clojure也是对树结构求值,但树是由Clojure源生list构成,树节点是源生Clojure值。

Lists are ideal for constructing tree structures. The first element of a list is treated as the root, and each subsequent element is treated as a branch. To create a nested tree, you can just use nested lists, as shown in Figure 7-2.

用list构造树结构很合适。list的第一个元素是根,每个后续元素都是一个分支。如需构建嵌套树结构,只需要使用嵌套的list,如图7-2。

图7-2图7-2

First, Clojure’s reader converts the text (+ 1 (* 6 7)) into a nested list. (You’ll learn more about the reader in the next section.) Then, Clojure’s evaluator takes that data as input and produces a result. (It also compiles Java Virtual Machine ( JVM) bytecode, which you’ll learn about in Chapter 12. For now, we’ll just focus on the evaluation model on a conceptual level.)

首先,Clojure的读入程序把文本(+ 1 (* 6 7))转换成一个嵌套list。然后Clojure的求值程序接受这个list数据并产生结果。

With this in mind, Figure 7-3 shows what Clojure’s evaluation process looks like.

图7-3表示了Clojure求值过程

图7-3图7-3

S-Expressions
In your Lisp adventures, you’ll come across resources that explain that Lisps evaluate s-expressions. I avoid that term here because it’s ambiguous: you’ll see it used to refer to both the actual data object that gets evaluated and the source code that represents that data. Using the same term for two different components of Lisp evaluation (code and data) obscures what’s important: your text represents native data structures, and Lisps evaluate native data structures, which is unique and awesome. For a great treatment of s-expressions, check out http://www.gigamonkeys.com/book/syntax-and-semantics.html.

在学习Lisp的过程中,你会碰上s-expressions(s表达式)的解释。我避免使用这个术语,因为它会引起歧义。你会发现它既可以指求值程序接受的数据,又可以指源码。用同一个术语代表Lips求值的两个组成部分,掩盖了要点:源码文本代表源生的数据结构,Lisp对源生的数据结构求值,这很独特很棒。这里有一篇很好的s表达式文章

However, the evaluator doesn’t actually care where its input comes from; it doesn’t have to come from the reader. As a result, you can send your program’s data structures directly to the Clojure evaluator with eval. Behold!

注意:求值程序不在乎输入从哪来,输入不一定来自读入程序。因此,你可以直接用eval发送数据结构给求值程序。

1
2
3
(def addition-list (list + 1 2))
(eval addition-list)
; => 3

That’s right, baby! Your program just evaluated a Clojure list. You’ll read all about Clojure’s evaluation rules soon, but briefly, this is what happened: when Clojure evaluated the list, it looked up the list that addition-list refers to; then it looked up the function corresponding to the + symbol; and then it called that function with 1 and 2 as arguments, returning 3. The data structures of your running program and those of the evaluator live in the same space, and the upshot is that you can use the full power of Clojure and all the code you’ve written to construct data structures for evaluation:

这里,Clojure求值了一个list。过程是这样的:当Clojure求值这个list时候,找到addition-list引用的list,然后找到符号+对应的函数,然后以12为参数调用这个函数,返回3。运行中的程序的数据结构和求值程序使用的数据结构处于同一空间,因此你可以运用所有的Clojure力量构建数据结构用于求值:

1
2
3
4
5
6
7
8
(eval (concat addition-list [10]))
; => 13

(eval (list 'def 'lucky-number (concat addition-list [10])))
; => #'user/lucky-number

lucky-number
; => 13

Figure 7-4 shows the lists you sent to the evaluator in these two examples.

图7-4显示了这两个例子里发送给求值程序的list

图7-4图7-4

Your program can talk directly to its own evaluator, using its own functions and data to modify itself as it runs! Are you going mad with power yet? I hope so! Hold on to some of your sanity, though, because there’s still more to learn.

程序可以直接跟求值程序对话,程序运行时候可以用程序里的函数和数据结构修改自身!

So Clojure is homoiconic: it represents abstract syntax trees using lists, and you write textual representations of lists when you write Clojure code. Because the code you write represents data structures that you’re used to manipulating and the evaluator consumes those data structures, it’s easy to reason about how to programmatically modify your program.

Clojure是同像性语言:用list表示抽象语法树,并且源码也用list表示。因为代码表示的就是要操作的数据结构和求值程序接受的数据结构,修改程序就容易了。

Macros are what allow you to perform those manipulations easily. The rest of this chapter covers Clojure’s reader and evaluation rules in detail to give you a precise understanding of how macros work.

用宏可以轻松完成这些数据结构操作。下面讲述Clojure的读入程序和求值规则,使你能准确理解宏是如何工作的。

The Reader

读入程序

The reader converts the textual source code you save in a file or enter in the REPL into Clojure data structures. It’s like a translator between the human world of Unicode characters and Clojure’s world of lists, vectors, maps, symbols, and other data structures. In this section, you’ll interact directly with the reader and learn how a handy feature, the reader macro, lets you write code more succinctly.

读入程序把存在文件里的或REPL输入的源码转换成Clojure数据结构。就像一个翻译,把人类世界的Unicode字符翻译成Clojure世界的list, vector, map, symbol和其他数据结构。接下来我们将要直接与读入程序交互,并学习一个方便的功能,读入程序宏,使代码更简洁。

Reading

读入

To understand reading, let’s first take a close look at how Clojure handles the text you type in the REPL. First, the REPL prompts you for text:

为了理解读入,让我们仔细看看Clojure如何处理REPL里输入的文字。首先,REPL提示你输入文字:

1
user=>

Then you enter a bit of text. Maybe something like this:

然后你输入一些文字。也许是这样:

1
user=> (str "To understand what recursion is," " you must first understand recursion.")

That text is really just a sequence of Unicode characters, but it’s meant to represent a combination of Clojure data structures. This textual representation of data structures is called a reader form. In this example, the form represents a list data structure that contains three more forms: the str symbol and two strings.

这些文字只是一系列Unicode字符,但代表的是Clojure数据结构组合。这个表示数据结构的文本形式叫做reader form(读入程序形式)。这个例子里,这个形式表示的是一个list数据结构,这个list又包含了3个形式:str符号和两个字符串。

Once you type those characters into the prompt and press enter, that text goes to the reader (remember REPL stands for read-eval-print-loop). Clojure reads the stream of characters and internally produces the corresponding data structures. It then evaluates the data structures and prints the textual representation of the result:

输入完成并回车之后,这些文本被读入程序接受(REPL代表了读入-求值-打印-循环)。Clojure内部用这些文本产生对应的数据结构。然后求值,打印结果的文本表示:

1
"To understand what recursion is, you must first understand recursion."

Reading and evaluation are discrete processes that you can perform independently. One way to interact with the reader directly is by using the read-string function. read-string takes a string as an argument and processes it using Clojure’s reader, returning a data structure:

读入和求值是能独立执行的分离步骤。使用read-string函数可以直接与读入程序交互。read-string接受一个字符串参数,用Clojure的读入程序处理它,然后返回数据结构:

1
2
3
4
5
6
7
8
(read-string "(+ 1 2)")
; => (+ 1 2)

(list? (read-string "(+ 1 2)"))
; => true

(conj (read-string "(+ 1 2)") :zagglewag)
; => (:zagglewag + 1 2)

In the first example, read-string reads the string representation of a list containing a plus symbol and the numbers 1 and 2. The return value is an actual list, as proven by the second example. The last example uses conj to prepend a keyword to the list. The takeaway is that reading and evaluating are independent of each other. You can read text without evaluating it, and you can pass the result to other functions. You can also evaluate the result, if you want:

第一个例子,read-string读入了一个包含加号符号和数字1,2的list字符串,返回实际list,从第二个例子可以证实。第三个例子用conj把一个keyword附加到这个list上。这里的要点是读入和求值互相独立。你可以读入文本但不求值,也可以把读入结果传给其他函数。如果愿意,你也可以求值结果:

1
2
(eval (read-string "(+ 1 2)"))
; => 3

In all the examples so far, there’s been a one-to-one relationship between the reader form and the corresponding data structures. Here are more examples of simple reader forms that directly map to the data structures they represent:

到现在的所有例子,都是读入程序形式与数据结构一一对应。更多的例子:

() A list reader form
str A symbol reader form
[1 2] A vector reader form containing two number reader forms
{:sound “hoot”} A map reader form with a keyword reader form and string reader form

  • () list读入程序形式
  • str symbol读入程序形式
  • [1 2] vector读入程序形式,包含了两个数字读入形式
  • {:sound “hoot”} map读入程序形式,包含两个读入形式,一个keyword和一个字符串

However, the reader can employ more complex behavior when converting text to data structures. For example, remember anonymous functions?

但是,读入程序把文本转换成数据结构时候,可以采用更复杂的行为。还记得匿名函数吗?

1
2
(#(+ 1 %) 3)
; => 4

Well, try this out:

试试这个:

1
2
(read-string "#(+ 1 %)")
; => (fn* [p1__423#] (+ 1 p1__423#))

Whoa! This is not the one-to-one mapping that we’re used to. Reading #(+ 1 %) somehow resulted in a list consisting of the fn* symbol, a vector containing a symbol, and a list containing three elements. What just happened?

擦!这不是一一对应。读入#(+ 1 %)生成了一个list,包含fn*符号,一个vector(含有一个符号),一个list(含有3个成员)。发生了什么?

Reader Macros

读入程序宏

I’ll answer my own question: the reader used a reader macro to transform #(+ 1 %). Reader macros are sets of rules for transforming text into data structures. They often allow you to represent data structures in more compact ways because they take an abbreviated reader form and expand it into a full form. They’re designated by macro characters, like ' (the single quote), #, and @. They’re also completely different from the macros we’ll get to later. So as not to get the two confused, I’ll always refer to reader macros using the full term reader macros.

读入程序用一个读入程序宏转换了#(+ 1 %)。读入程序宏是一组规则,用于把文本转换成数据结构。由于读入程序宏采用了缩写的读入程序形式,并被展开成完整形式,所以可以更简洁地表示数据结构。他们由宏字符标识,比如(单引号),#,@。他们与后面讲的宏完全是两码事。不要混为一谈。引用他们时候,我会总是用完整的读入程序宏

For example, you can see how the quote reader macro expands the single quote character here:

例如,看看引用读入程序宏是如何扩展单引号字符的:

1
2
(read-string "'(a b c)")
; => (quote (a b c))

When the reader encounters the single quote, it expands it to a list whose first member is the symbol quote and whose second member is the data structure that followed the single quote. The deref reader macro works similarly for the @ character:

读入程序遇到单引号时,会把它展开成一个list,其第一个成员是符号quote,第二个成员是单引号后面的数据结构。类似地,对于@字符,读入程序用defef读入程序宏展开:

1
2
(read-string "@var")
; => (clojure.core/deref var)

Reader macros can also do crazy stuff like cause text to be ignored. The semicolon designates the single-line comment reader macro:

读入程序宏还能忽略文本。分号是单行注释读入程序宏:

1
2
(read-string "; ignore!\n(+ 1 2)")
; => (+ 1 2)

And that’s the reader! Your humble companion, toiling away at transforming text into data structures. Now let’s look at how Clojure evaluates those data structures.

这就是读入程序,把文本转换成数据结构。现在我们看看Clojure是如何求值这些数据结构的。

The Evaluator

求值程序

You can think of Clojure’s evaluator as a function that takes a data structure as an argument, processes the data structure using rules corresponding to the data structure’s type, and returns a result. To evaluate a symbol, Clojure looks up what the symbol refers to. To evaluate a list, Clojure looks at the first element of the list and calls a function, macro, or special form. Any other values (including strings, numbers, and keywords) simply evaluate to themselves.

你可以把Clojure的求值程序看成一个接受数据结构的函数,用与数据结构类型对应的规则处理数据结构,并返回结果。求值符号时,Clojure查找符号的引用。求值list时候,Clojure查看list的第一个成员,并调用一个函数或宏或特殊形式。任何其他值,包括字符串,数字,keyword,求值结果都是他们自身。

For example, let’s say you’ve typed (+ 1 2) in the REPL. Figure 7-5 shows a diagram of the data structure that gets sent to the evaluator.

比如,你在REPL输入了(+ 1 2)。图7-5示意了发送给求值程序的数据结构。

图7-57-5

Because it’s a list, the evaluator starts by evaluating the first element in the list. The first element is the plus symbol, and the evaluator resolves that by returning the corresponding function. Because the first element in the list is a function, the evaluator evaluates each of the operands. The operands 1 and 2 evaluate to themselves because they’re not lists or symbols. Then the evaluator calls the addition function with 1 and 2 as the operands, and returns the result.

因为这是个list,求值程序先求值list里的第一个元素。第一个元素是加符号,求值程序解析它并返回对应的函数。由于list第一个元素是函数,求值程序继续求值每个操作数。因为1和2不是符号,也不是list,所以都求值为自身。接下来求值程序用1和2作为操作数调用函数,并返回结果。

The rest of this section explains the evaluator’s rules for each kind of data structure more fully. To show how the evaluator works, we’ll just run each example in the REPL. Keep in mind that the REPL first reads your text to get a data structure, then sends that data structure to the evaluator, and then prints the result as text.

这节的剩余部分全面解释求值程序对每种数据结构的求值规则。

Data
I write about how Clojure evaluates data structures in this chapter, but that’s imprecise. Technically, data structure refers to some kind of collection, like a linked list or b-tree, or whatever, but I also use the term to refer to scalar (singular, noncollection) values like symbols and numbers. I considered using the term data objects but didn’t want to imply object-oriented programming, or using just data but didn’t want to confuse that with data as a concept. So, data structure it is, and if you find this offensive, I will give you a thousand apologies, thoughtfully organized in a Van Emde Boas tree.

数据
这章里我经常提到Clojure求值数据结构,但这不太精确。技术上讲,数据结构是指某种集合,但我也用这个词代表纯量(单个的,非集合)值,比如符号,数字。因为我觉得用数据结构比用数据对象或数据更合适。如有不妥,深表歉意。

These Things Evaluate to Themselves

这些东西求值结果是他们自身

Whenever Clojure evaluates data structures that aren’t a list or symbol, the result is the data structure itself:

无论何时,Clojure求值的数据结构如果不是list或符号,结果都是他们自身:

1
2
3
4
5
6
7
8
9
10
11
true
; => true

false
; => false

{}
; => {}

:huzzah
; => :huzzah

Empty lists evaluate to themselves, too:

空list也求值为自身:

1
2
()
; => ()

symbols

符号

One of your fundamental tasks as a programmer is creating abstractions by associating names with values. You learned how to do this in Chapter 3 by using def, let, and function definitions. Clojure uses symbols to name functions, macros, data, and anything else you can use, and evaluates them by resolving them. To resolve a symbol, Clojure traverses any bindings you’ve created and then looks up the symbol’s entry in a namespace mapping, which you learned about in Chapter 6. Ultimately, a symbol resolves to either a value or a special form—a built-in Clojure operator that provides fundamental behavior.

程序员的一个重要任务就是建立名字和值之间的关联。第3章讲述的def, let,函数定义都是干这个的。Clojure用符号命名函数,宏,数据和任何其他可用的东西,并通过解析来对其求值。要解析一个符号,Clojure遍历所有你建立的绑定,然后在一个命名空间里查找这个符号条目,第6章讲解过这些。最终一个符号解析成一个或一个特殊形式–提供基础行为的Clojure內建操作符。

In general, Clojure resolves a symbol by:

  1. Looking up whether the symbol names a special form. If it doesn’t . . .
  2. Looking up whether the symbol corresponds to a local binding. If it doesn’t . . .
  3. Trying to find a namespace mapping introduced by def. If it doesn’t . . .
  4. Throwing an exception

通常,Clojure这样解析一个符号:

  1. 查找这个符号是否是特殊形式。如果不是…
  2. 查找是否是本地绑定。如果不是…
  3. 在命名空间里查找是否是一个def定义的映射。如果不是…
  4. 抛出异常

Let’s first look at a symbol resolving to a special form. Special forms, like if, are always used in the context of an operation; they’re always the first element in a list:

先看一个解析成特殊形式的符号。特殊形式,比如if,总是作为操作符使用,总是list里的第一个元素:

1
2
(if true :a :b)
; => :a

In this case, if is a special form and it’s being used as an operator. If you try to refer to a special form outside of this context, you’ll get an exception:

这个例子里,if是特殊形式,作为一个操作符使用。如果在其他情况下引用特殊形式,会引起异常:

1
2
if
; => CompilerException java.lang.RuntimeException: Unable to resolve symbol: if in this context

Next, let’s evaluate some local bindings. A local binding is any association between a symbol and a value that wasn’t created by def. In the next example, the symbol x is bound to 5 using let. When the evaluator resolves x, it resolves the symbol x to the value 5:

接下来,求值本地绑定。本地绑定是任何非def建立的符号与值的关联。这个例子里,符号x通过let与5绑定。求值程序解析x时候,解析成值5:

1
2
3
(let [x 5]
(+ x 3))
; => 8

Now if we create a namespace mapping of x to 15, Clojure resolves it accordingly:

如果建立一个x与15的命名空间映射,Clojure会相应地解析它:

1
2
3
(def x 15)
(+ x 3)
; => 18

In the next example, x is mapped to 15, but we introduce a local binding of x to 5 using let. So x is resolved to 5:

下个例子里,x映射成15,但在本地绑定里x通过let绑定为5,所以x解析为5:

1
2
3
4
(def x 15)
(let [x 5]
(+ x 3))
; => 8

You can nest bindings, in which case the most recently defined binding takes precedence:

也可以嵌套绑定,最内部的绑定优先级最高:

1
2
3
4
(let [x 5]
(let [x 6]
(+ x 3)))
; => 9

Functions also create local bindings, binding parameters to arguments within the function body. In this next example, exclaim is mapped to a function. Within the function body, the parameter name exclamation is bound to the argument passed to the function:

函数也会创建本地绑定,在函数体里,把参数名与参数值绑定.这个例子里,exclaim是函数.函数体里,参数名exclamation与传给函数的参数值绑定:

1
2
3
4
5
6
(defn exclaim
[exclamation]
(str exclamation "!"))

(exclaim "Hadoken")
; => "Hadoken!"

Finally, in this last example, map and inc both refer to functions:

最后这个例子里,mapinc都是函数引用:

1
2
(map inc [1 2 3])
; => (2 3 4)

When Clojure evaluates this code, it first evaluates the map symbol, looking up the corresponding function and applying it to its arguments. The symbol map refers to the map function, but it shouldn’t be confused with the function itself. The map symbol is still a data structure, the same way that the string "fried salad" is a data structure, but it’s not the same as the function itself:

Clojure解析这个代码时,先求值map符号,查找相应的函数并用其参数调用它.符号map引用的是map函数,但不要把它与函数自身混淆。符号map仍然是数据结构,就像字符串"fried salad"是数据结构一样,符号不是函数本身:

1
2
3
4
5
6
7
8
(read-string "+")
; => +

(type (read-string "+"))
; => clojure.lang.Symbol

(list (read-string "+") 1 2)
; => (+ 1 2)

In these examples, you’re interacting with the plus symbol, +, as a data structure. You’re not interacting with the addition function that it refers to. If you evaluate it, Clojure looks up the function and applies it:

在这个例子里,你直接与作为数据结构的符号+交互.不是与它引用的加法函数交互.如果对它求值,Clojure查找这个函数并调用:

1
2
(eval (list (read-string "+") 1 2))
; => 3

On their own, symbols and their referents don’t actually do anything; Clojure performs work by evaluating lists.

对于符号和符号的引用自身来说,他们实际上什么也不做;Clojure靠求值list干活.

Lists

列表

If the data structure is an empty list, it evaluates to an empty list:

如果求值的数据结构是空list, 求值结果是空list:

1
2
(eval (read-string "()"))
; => ()

Otherwise, it is evaluated as a call to the first element in the list. The way the call is performed depends on the nature of that first element.

否则,就是对列表里第一个元素的调用的求值.调用的方法由第一个元素的性质决定.

Function Calls

函数调用

When performing a function call, each operand is fully evaluated and then passed to the function as an argument. In this example, the + symbol resolves to a function:

进行函数调用时,每个操作数都完全求值,然后作为参数传递给函数.这个例子中,符号+解析为函数:

1
2
(+ 1 2)
; => 3

Clojure sees that the list’s head is a function, so it proceeds to evaluate the rest of the elements in the list. The operands 1 and 2 both evaluate to themselves, and after they’re evaluated, Clojure applies the addition function to them.

Clojure看到list的第一个元素是个函数,所以它继续求值list里剩余的元素.操作数1和2都求值为自身,之后用这些参数调用函数.

You can also nest function calls:

函数调用也可能嵌套:

1
2
(+ 1 (+ 2 3))
; => 6

Even though the second argument is a list, Clojure follows the same process here: look up the + symbol and evaluate each argument. To evaluate the list (+ 2 3), Clojure resolves the first member to the addition function and proceeds to evaluate each of the arguments. In this way, evaluation is recursive.

尽管第二个参加是个list,Clojure仍采用同样的过程: 查找符号+并求值其每个参数.求值list(+ 2 3)时,Clojure把第一个成员解析为加法函数并继续求值每个参数.求值以这种方式递归进行.

Special Forms

特殊形式

You can also call special forms. In general, special forms are special because they implement core behavior that can’t be implemented with functions. For example:

特殊形式 也可以调用.总的来说,特殊形式的特殊之处在于: 它们实现了函数无法实现的核心功能.比如:

1
2
(if true 1 2)
; => 1

Here, we ask Clojure to evaluate a list beginning with the symbol if. That if symbol gets resolved to the if special form, and Clojure calls that special form with the operands true, 1, and 2.

这里,Clojure求值了一个以符号if开始的list。这个if符号解析为if特殊形式,Clojure用操作数true, 1, 2调用这个特殊形式。

Special forms don’t follow the same evaluation rules as normal functions. For example, when you call a function, each operand gets evaluated. However, with if you don’t want each operand to be evaluated. You only want certain operands to be evaluated, depending on whether the condition is true or false.

特殊形式不遵从普通函数的求值规则。比如,调用函数时,每个操作数都被求值。但对于if,不希望每个操作数都被求值。只希望根据条件是否为true或false,某个操作数被求值。

Another important special form is quote. You’ve seen lists represented like this:

另一个重要特殊形式是引用(quote)。你已经见过这么表示的list:

1
'(a b c)

As you saw in “The Reader” on page 153, this invokes a reader macro so that we end up with this:

前面见过,这会调用会使用一个读入程序宏最终得到这个:

1
(quote (a b c))

Normally, Clojure would try to resolve the a symbol and then call it because it’s the first element in a list. The quote special form tells the evaluator, “Instead of evaluating my next data structure like normal, just return the data structure itself.” In this case, you end up with a list consisting of the symbols a, b, and c.

通常,因为符号a是list第一个成员,Clojure会尝试解析并调用它。引用特殊形式的用处是告诉求值程序,不要求值下一个数据结构,直接返回这个数据结构自身。这个例子里,就是个符号a,b,c组成的list。

def, let, loop, fn, do, and recur are all special forms as well. You can see why: they don’t get evaluated the same way as functions. For example, normally when the evaluator evaluates a symbol, it resolves that symbol, but def and let obviously don’t behave that way. Instead of resolving symbols, they actually create associations between symbols and values. So the evaluator receives a combination of data structures from the reader, and it goes about resolving the symbols and calling the functions or special forms at the beginning of each list. But there’s more! You can also place a macro at the beginning of a list instead of a function or a special form, and this can give you tremendous power over how the rest of the data structures are evaluated.

def, let, loop, fn, dorecur都是特殊形式。原因是他们与函数的求值方法不同。例如,一般情况下,求值程序求值一个符号时,会解析那个符号,但deflet明显不是这样,而是创建了符号与值的关联。就是这样:求值程序从读入程序接受数据结构,并解析其中的符号,并调用每个list的第一个函数或特殊形式。但还没完!list第一个成员除了是函数或特殊形式,还可以是,宏赋予你极其强大的对于其后的数据结构如何求值的能力。

Macros

Hmm . . . Clojure evaluates data structures—the same data structures that we write and manipulate in our Clojure programs. Wouldn’t it be awesome if we could use Clojure to manipulate the data structures that Clojure evaluates? Yes, yes it would! And guess what? You can do this with macros! Did your head just explode? Mine did!

Clojure求值的数据结构和我们写的数据结构是一样的。如果能够修改Clojure求值的数据结构,是不是很棒呢?宏就是干这个的!宏可以操纵Clojure求值的数据结构!

To get an idea of what macros do, let’s look at some code. Say we want to write a function that makes Clojure read infix notation (such as 1 + 1) instead of its normal notation with the operator first (+ 1 1). This example is not a macro. Rather, it merely shows that you can write code using infix notation and then use Clojure to transform it so it will actually execute. First, create a list that represents infix addition:

为了感受一下宏使干啥的,来看个例子。比如说需要一个函数,使我们能读取中置表示法(比如1 + 1),而不是正常的前置表示法(+ 1 1)。这个例子不是宏,只是让你知道,你可以写中置表示法的代码,然后转换成可以执行的代码。首先建立一个中置加法list:

1
2
(read-string "(1 + 1)")
; => (1 + 1)

Clojure will throw an exception if you try to make it evaluate this list:

如果你对这个list求值,会抛出异常:

1
2
(eval (read-string "(1 + 1)"))
; => ClassCastException java.lang.Long cannot be cast to clojure.lang.IFn

However, read-string returns a list, and you can use Clojure to reorganize that list into something it can successfully evaluate:

read-string返回的是list,你可以重组这个list,使它能够成功求值:

1
2
3
(let [infix (read-string "(1 + 1)")]
(list (second infix) (first infix) (last infix)))
; => (+ 1 1)

If you eval this, it returns 2, just as you’d expect:

如果求值,会返回2,如你所愿:

1
2
3
4
(eval
(let [infix (read-string "(1 + 1)")]
(list (second infix) (first infix) (last infix))))
; => 2

This is cool, but it’s also quite clunky. That’s where macros come in. Macros give you a convenient way to manipulate lists before Clojure evaluates them. Macros are a lot like functions: they take arguments and return a value, just like a function would. They work on Clojure data structures, just like functions do. What makes them unique and powerful is the way they fit in to the evaluation process. They are executed in between the reader and the evaluator—so they can manipulate the data structures that the reader spits out and transform with those data structures before passing them to the evaluator.

这很酷,但很麻烦。宏就是为了解决这个问题。Clojure求值list之前,用宏可以方便地操纵它。宏很像函数:接受参数,返回值。宏像函数一样,也对数据结构起作用。宏的独特和强大之处在于它如何融入求值过程。宏执行于读入程序和求值程序之间–所以宏能操纵并转换读入程序生成的数据结构,然后再传给求值程序。

Let’s look at an example:

看个例子:

1
2
3
4
5
6
7
8
9
10
(defmacro ignore-last-operand
[function-call]
(butlast function-call))

➊ (ignore-last-operand (+ 1 2 10))
; => 3

;; This will not print anything
(ignore-last-operand (+ 1 2 (println "look at me!!!")))
; => 3

At ➊ the macro ignore-last-operand receives the list (+ 1 2 10) as its argument, not the value 13. This is very different from a function call, because function calls always evaluate all of the arguments passed in, so there is no possible way for a function to reach into one of its operands and alter or ignore it. By contrast, when you call a macro, the operands are not evaluated. In particular, symbols are not resolved; they are passed as symbols. Lists are not evaluated either; that is, the first element in the list is not called as a function, special form, or macro. Rather, the unevaluated list data structure is passed in.

在 ➊ 处,宏ignore-last-operand接受了的参数是list(+ 1 2 10),而不是13。这与函数很不同,因为函数总是求值所有传入的参数,所以函数无法忽略或修改某个参数。与之相反,宏不对参数求值。特别是宏不对符号求值,符号传给宏还是符号。宏也不对list求值,即list的第一个成员不会作为函数,特殊形式或宏被调用,传给宏的还是未求值的list数据结构。

Another difference is that the data structure returned by a function is not evaluated, but the data structure returned by a macro is. The process of determining the return value of a macro is called macro expansion, and you can use the function macroexpand to see what data structure a macro returns before that data structure is evaluated. Note that you have to quote the form that you pass to macroexpand:

另一个差别是函数返回的数据结构不被求值,宏返回的被求值。宏返回数据结构的过程叫宏展开,并且在这个数据结构被求值之前,可以用函数macroexpand看返回结果。注意,传给macroexpand的形式必须用quote包住:

1
2
3
4
5
(macroexpand '(ignore-last-operand (+ 1 2 10)))
; => (+ 1 2)

(macroexpand '(ignore-last-operand (+ 1 2 (println "look at me!!!"))))
; => (+ 1 2)

As you can see, both expansions result in the list (+ 1 2). When this list is evaluated, as in the previous example, the result is 3.

可以看到,两个展开结果都是list(+ 1 2)。当这个list被求值时候,结果是3

Just for fun, here’s a macro for doing simple infix notation:

只是为了好玩,这个宏实现简单的中置表示法:

1
2
3
4
5
6
7
8
(defmacro infix
[infixed]
(list (second infixed)
(first infixed)
(last infixed)))

(infix (1 + 2))
; => 3

The best way to think about this whole process is to picture a phase between reading and evaluation: the macro expansion phase. Figure 7-6 shows how you can visualize the entire evaluation process for (infix (1 + 2)).

思考整个过程的最好方法是在图上的读入和求值阶段中间画出宏展开阶段。图7-6演示了(infix (1 + 2))的整个求值过程。

图7-67-6

And that’s how macros fit into the evaluation process. But why would you want to do this? The reason is that macros allow you to transform an arbitrary data structure like (1 + 2) into one that can Clojure can evaluate, (+ 1 2). That means you can use Clojure to extend itself so you can write programs however you please. In other words, macros enable syntactic abstraction. Syntactic abstraction may sound a bit abstract (ha ha!), so let’s explore that a little.

这就是宏融入求值过程的方式。但为什么要这么干呢?因为宏可以把任意数据结构,比如(1 + 2),转换成Clojure能求值的数据结构,(+ 1 2)。这意味着可以用Clojure语言扩展自身,只要你高兴,程序爱怎么写怎么写。换句话说,Clojure允许你进行语法抽象。语法抽象听起来有点抽象,让我们探索一下。

Syntactic Abstraction and the -> Macro

语法抽象与 -> 宏

Often, Clojure code consists of a bunch of nested function calls. For example, I use the following function in one of my projects:

Clojure代码经常由大量嵌套函数调用构成。比如下面是一个用在我项目里的函数:

1
2
3
4
(defn read-resource
"Read a resource into a string"
[path]
(read-string (slurp (clojure.java.io/resource path))))

To understand the function body, you have to find the innermost form, in this case (clojure.java.io/resource path), and then work your way outward from right to left to see how the result of each function gets passed to another function. This right-to-left flow is opposite of what non-Lisp programmers are used to. As you get used to writing in Clojure, this kind of code gets easier and easier to understand. But if you want to translate Clojure code so you can read it in a more familiar, left-to-right, top-to-bottom manner, you can use the built-in -> macro, which is also known as the threading or stabby macro. It lets you rewrite the preceding function like this:

要理解函数体,你必须找到最里面的形式,这里是(clojure.java.io/resource path),然后从右至左地往外层走,查看每个函数调用的结果是如何传给下一个函数的。这种从右往左的过程与非Lisp程序员习惯的过程相反。如果你习惯了Clojure,这样的代码就会越来越容易理解。但如果你想转换Clojure代码,让你用更习惯的,从左到右,从上到下的方式阅读,你可以用內建的->宏做到,这个宏也叫穿线穿串宏。前面的函数可以这么写:

1
2
3
4
5
6
(defn read-resource
[path]
(-> path
clojure.java.io/resource
slurp
read-string))

You can read this as a pipeline that goes from top to bottom instead of from inner parentheses to outer parentheses. First, path gets passed to io/resource, then the result gets passed to slurp, and finally the result of that gets passed to read-string.

可以看到,这是一个从上到下的管道,而不是从内到外的括号。首先,path被传给io/resource,然后结果被传给slurp,最后又被传给read-string

These two ways of defining read-resource are entirely equivalent. However, the second one might be easier understand because we can approach it from top to bottom, a direction we’re used to. The -> also lets us omit parentheses, which means there’s less visual noise to contend with. This is a syntactic abstraction because it lets you write code in a syntax that’s different from Clojure’s built-in syntax but is preferable for human consumption. Better than lead into gold!!!

用这两种方法定义read-resource完全一样。但第二种可能更容易理解,因为可以用习惯的从上到下的方向。->还省去了括号,视觉上更清晰。这是个语法抽象,因为它使我们能够用与Clojure内置语法不同,但人类更喜欢的方式写代码。真屌!

Summary

总结

In this chapter, you learned about Clojure’s evaluation process. First, the reader transforms text into Clojure data structures. Next, the macro expander transforms those data structures with macros, converting your custom syntax into syntactically valid data structures. Finally, those data structures get sent to the evaluator. The evaluator processes data structures based on their type: symbols are resolved to their referents; lists result in function, macro, or special form calls; and everything else evaluates to itself.

这章讲述了Clojure的求值过程。首先,读入程序把文本转换成Clojure数据结构。然后,宏展开程序用宏转换这些数据结构,把自定义语法转换成语法合法的数据结构。最后,这些数据结构被传给求值程序。求值程序基于数据结构的类型处理数据结构:符号被解析成符号的引用;list的解析结果是函数、宏或特殊形式的调用;所有其他类型都解析为自身。

The coolest thing about this process is that it allows you to use Clojure to expand its own syntax. This process is made easier because Clojure is homoiconic: its text represents data structures, and those data structures represent abstract syntax trees, allowing you to more easily reason about how to construct syntax-expanding macros.

最酷的是这个过程允许你用Clojure扩展自己的语法。因为Clojure是同像性语言,这个过程更容易了:代码代表数据结构,数据结构代表抽象语法树,使你推断如何构建语法扩展宏变得更加容易。

With all these new concepts in your brainacles, you’re now ready to blow stuff up on purpose, just like I promised. The next chapter will teach you everything you need to know about writing macros. Hold on to your socks or they’re liable to get knocked off!

这些概念为下章做好了准备。下章会讲述写宏需要的所有知识!

Exercises

练习

These exercises focus on reading and evaluation. Chapter 8 has exercises for writing macros.

这些练习专注于读入和求值。第8章有写宏练习。

  1. Use the list function, quoting, and read-string to create a list that, when evaluated, prints your first name and your favorite sci-fi movie.
  2. Create an infix function that takes a list like (1 + 3 * 4 - 5) and transforms it into the lists that Clojure needs in order to correctly evaluate the expression using operator precedence rules.
  1. 使用list函数,quoteread-string创建一个list,这个list被求值时,打印你的姓和你最喜欢的科幻电影。
  2. 编写一个infix函数,接受形如(1 + 3 * 4 - 5)的list,并转换成优先级规则正确的Clojure需要的list。

译文结束。