本文是我对Clojure书籍 CLOJURE FOR THE BRAVE AND TRUE第八章Writing Macros 做的翻译。翻译形式,中英对照,英文引用跟着中文翻译。如有错误,在所难免,欢迎指正。
如果对Clojure宏的求值过程模型(特别是宏展开)不熟悉,可以先看看原文第七章,或英中对照翻译
如果你对Clojure不熟悉,可以先看一下第3章A Clojure Crash Course。
如果对某个函数,宏,特殊形式不熟悉,可以查看coojuredocs的参考,每个文档都有例子,非常好。
译文开始。
When I was 18, I got a job as a night auditor at a hotel in Santa Fe, New Mexico, working four nights a week from 11 pm till 7 am. After a few months of this sleepless schedule, my emotions took on a life of their own. One night, at about 3 am, I was watching an infomercial for a product claiming to restore men’s hair. As I watched the story of a formerly bald individual, I became overwhelmed with sincere joy. “At last!” my brain gushed. “This man has gotten the love and success he deserves! What an incredible product, giving hope to the hopeless!”
18岁的事。
Just as a potion would allow me to temporarily alter my fundamental nature, macros allow you to modify Clojure in ways that just aren’t possible with other languages. With macros, you can extend Clojure to suit your problem space, building up the language.
宏使你能够用其他语言不可能的方式修改Clojure。有了宏,你可以扩展Clojure使其符合自己的问题,也可以增强这门语言。
In this chapter, you’ll thoroughly examine how to write macros, starting with basic examples and moving up in complexity. You’ll close by donning your make-believe cap and using macros to validate customer orders in your imaginary online potion store.
这章会透彻讲解写宏,最后会用宏验证客户订单。
By the end of the chapter, you’ll understand all the tools you’ll use to write macros: quote, syntax quote, unquote, unquote splicing (aka the piñata tool), and gensym. You’ll also learn about the dangers lying in wait for unsuspecting macro authors: double evaluation, variable capture, and macro infection.
学习完这章,你会理解所有写宏工具:引用(quote),语法引用(syntax quote),不引用(unquote),打开不引用(unquote splicing)和符号生成(gensym)。你也会了解写宏时候遇到的问题:二次求值,变量捕获,宏传染。
Macros Are Essential
宏的必要性
Before you start writing macros, I want to help you put them in the proper context. Yes, macros are cooler than a polar bear’s toenails, but you shouldn’t think of macros as some esoteric tool you pull out when you feel like getting extra fancy with your code. In fact, macros allow Clojure to derive a lot of its built-in functionality from a tiny core of functions and special forms. Take
when
, for example.when
has this general form:
开始写宏前,你应该对宏的使用场景有个恰当的认识。宏很酷,但不应该把它当成什么深奥的工具。实际上,利用宏,用非常少量的核心函数和特殊形式,就能得到许多Clojure内置功能。比如when
通常是这种形式:
1 | (when boolean-expression |
You might think that
when
is a special form likeif
. Well guess what? It’s not! In most other languages, you can only create conditional expressions using special keywords, and there’s no way to create your own conditional operators. However,when
is actually a macro.
也许你觉得when
像if
一样,是个特殊形式。但它不是。大多数语言里,你只能用专门的关键字创建条件表达式,并且无法创建你自己的条件操作符。但when
是个宏。
In this macro expansion, you can see that
when
is implemented in terms ofif
anddo
:
在宏展开里可以看见,when
是用if
和do
实现的:
1 | (macroexpand '(when boolean-expression |
This shows that macros are an integral part of Clojure development—they’re even used to provide fundamental operations. Macros aren’t reserved for exotic special cases; you should think of macro writing as just another tool in your tool satchel. As you learn to write your own macros, you’ll see how they allow you to extend the language even further so that it fits the shape of your particular problem domain.
这说明宏是Clojure开发必不可少的部分–甚至用来提供基础操作。宏不是留给特殊情况使用的,宏只是工具袋里的另一个工具。你自己写宏的时候,会看到宏使你能进一步扩展语言,使之符合特定的问题。
Anatomy of a Macro
宏结构分析
Macro definitions look much like function definitions. They have a name, an optional document string, an argument list, and a body. The body will almost always return a list. This makes sense because macros are a way of transforming a data structure into a form Clojure can evaluate, and Clojure uses lists to represent function calls, special form calls, and macro calls. You can use any function, macro, or special form within the macro body, and you call macros just like you would a function or special form.
宏定义很像函数定义。一个名字,一个可选文档字符串,一个参数列表,一个主体。主体几乎总是返回一个list。这是有意义的,因为宏是一种把数据结构转换成Clojure能够求值的形式的方法,并且Clojure用list代表函数调用,特殊形式调用,和宏调用。在宏主体内部,你可以使用任何函数,宏,或特殊形式,并且你可以像调用函数或特殊形式那样调用宏。
As an example, here’s our old friend the
infix
macro:
例如,看下infix
宏:
1 | (defmacro infix |
This macro rearranges a list into the correct order for infix notation. Here’s an example:
这个宏把中置表示法的list重新安排成正确的顺序:
1 | (infix (1 + 1)) |
One key difference between functions and macros is that function arguments are fully evaluated before they’re passed to the function, whereas macros receive arguments as unevaluated data. You can see this in the example. If you tried evaluating
(1 + 1)
on its own, you would get an exception. However, because you’re making a macro call, the unevaluated list(1 + 1)
is passed toinfix
. Then the macro can usefirst
,second
, andlast
to rearrange the list so Clojure can evaluate it:
函数和宏的一个关键差别是函数参数被全部求值,再传给函数,而宏参数不被求值。可以从例子里看到这点。如果尝试求值(1 + 1)
本身,会出现异常。但因为进行的是宏调用,未被求值的list(1 + 1)
传给了infxi
。然后宏使用first
, second
, last
重新安排list,所以Clojure能对它求值:
1 | (macroexpand '(infix (1 + 1))) |
By expanding the macro, you can see that
infix
rearranges(1 + 1)
into(+ 1 1)
. Handy!
宏展开以后,可以看到infix
把(1 + 1)
重新安排成了(+ 1 1)
。很方便!
You can also use argument destructuring in macro definitions, just like you can with functions:
与函数一样,宏定义里也可以使用参数解构:
1 | (defmacro infix-2 |
Destructuring arguments lets you succinctly bind values to symbols based on their position in a sequential argument. Here,
infix-2
takes a sequential data structure as an argument and destructures by position so the first value is namedoperand1
, the second value is namedop
, and the third value is namedoperand2
within the macro.
参数解构让你能够简洁地基于值在序列参数里的位置绑定符号与值。这里,infix-2
接受一个序列数据结构作为参数,并基于位置解构,于是在宏内部,第一个命名为operand1
,第二个值命名为op
,第三个值命名为operand2
。
You can also create multiple-arity macros, and in fact the fundamental Boolean operations
and
andor
are defined as macros. Here’sand
’s source code:
也能创建有多种参数个数的宏,实际上基础布尔操作and
和or
都是用宏定义的。这是and
的源码:
1 | (defmacro and |
There’s a lot of stuff going on in this example, including the symbols
`
and~@
, which you’ll learn about soon. What’s important to realize for now is that there are three macro bodies here: a 0-arity macro body that always returnstrue
, a 1-arity macro body that returns the operand, and an n-arity macro body that recursively calls itself. That’s right: macros can be recursive, and they also can use rest args (& next
in the n-arity macro body), just like functions.
这个例子有很多东西,包括后面会学习的符号`
和~@
。现在最重要的是是认识到这里有三个宏主体:无参数,返回true
的;一个参数,返回这个参数;n个参数递,归调用自身。像函数一样,宏可以递归而且也可以使用剩余参数(n个参数宏主体里的& next
)。
Now that you’re comfortable with the anatomy of macros, it’s time to strap yourself to your thinking mast Odysseus-style and learn to write macro bodies.
熟悉了宏的解构后,继续学习写宏。
Building Lists for Evaluation
建立求值列表
Macro writing is all about building a list for Clojure to evaluate, and it requires a kind of inversion to your normal way of thinking. For one, you’ll often need to quote expressions to get unevaluated data structures in your final list (we’ll get back to that in a moment). More generally, you’ll need to be extra careful about the difference between a symbol and its value.
写宏的一切工作就是建立建立一个用于求值的list,这需要用一种与平常相反的方式思考。比如,最终的list里经常需要引用表达式以得到未求值的数据结构。更普遍的是,你需要对符号与他们的值之间的差别特别小心。
Distinguishing Symbols and Values
区分符号与值
Say you want to create a macro that takes an expression and both prints and returns its value. (This differs from
println
in thatprintln
always returnsnil
.) You want your macro to return lists that look like this:
比如说你希望一个宏接受一个表达式,打印并返回其值。(与println
的不同之处是println
总是返回nil
。)你想让你的宏返回这样的list:
1 | (let [result expression] |
Your first version of the macro might look like this, using the
list
function to create the list that Clojure should evaluate:
你的第一版的宏可能是这样的,用list
函数创建求值列表:
1 | (defmacro my-print-whoopsie |
However, if you tried this, you’d get the exception Can’t take the value of a macro: #’clojure.core/let. What’s going on here?
但试一下你会得到异常:Can’t take the value of a macro: #’clojure.core/let。怎么回事?
The reason this happens is that your macro body tries to get the value that the symbol
let
refers to, whereas what you actually want to do is return thelet
symbol itself. There are other problems, too: you’re trying to get the value ofresult
, which is unbound, and you’re trying to get the value ofprintln
instead of returning its symbol. Here’s how you would write the macro to do what you want:
原因是宏主体里在尝试获取符号let
引用的值,而你想要的是返回符号let
本身。还有其他问题:尝试获取result
的值,但它未绑定;尝试获取println
的值,而不是返回这个符号。这是你想要的宏:
1 | (defmacro my-print-whoopsie |
Here, you’re quoting each symbol you want to use as a symbol by prefixing it with the single quote character,
'
. This tells Clojure to turn off evaluation for whatever follows, in this case preventing Clojure from trying to resolve the symbols and instead just returning the symbols. The ability to use quoting to turn off evaluation is central to writing macros, so let’s give the topic its own section.
这里引用(quote)了每个想用作符号的符号,在它前面附加了单引号字符,'
。无论引号后面是什么,Clojure都不会对其求值,这里阻止了符号求值,直接返回符号。引用的不求值功能对于写宏非常重要,让我们单独讨论一下。
Simple Quoting
简单引用
You’ll almost always use quoting within your macros to obtain an unevaluated symbol. Let’s go through a brief refresher on quoting and then see how you might use it in a macro.
为了获得未求值的符号,宏里几乎总是使用引用。简单回顾一下引用(quote),然后看看宏里怎么使用它。
First, here’s a simple function call with no quoting:
首先,这是个没有引用的函数调用:
1 | (+ 1 2) |
If we add quote at the beginning, it returns an unevaluated data structure:
如果在前面加上引用,会返回未求值的数据结构:
1 | (quote (+ 1 2)) |
Here in the returned list,
+
is a symbol. If we evaluate this plus symbol, it yields the plus function:
返回的+
是符号。如果对它求值,是加法函数:
1 | + |
Whereas if we quote the plus symbol, it just yields the plus symbol:
如果引用加符号,只生成加符号:
1 | (quote +) |
Evaluating an unbound symbol raises an exception:
对未绑定的符号求值会引起异常:
1 | sweating-to-the-oldies |
But quoting the symbol returns a symbol regardless of whether the symbol has a value associated with it:
但引用符号返回符号,不论是否有值与它关联:
1 | (quote sweating-to-the-oldies) |
The single quote character is a reader macro for (quote x):
单引号字符是(quote x)的读入程序宏:
1 | '(+ 1 2) |
You can see quoting at work in the when macro. This is when’s actual source code:
这是宏when
里,单引号的使用。when
的源码如下:
1 | (defmacro when |
Notice that the macro definition quotes both
if
anddo
. That’s because you want these symbols to be in the final list thatwhen
returns for evaluation. Here’s an example of what that returned list might look like:
注意宏定义引用了if
和do
。因为你希望这些符号出现在when
返回的,用于求值的最终list里。返回的list是这样的:
1 | (macroexpand '(when (the-cows-come :home) |
Here’s another example of source code for a built-in macro, this time for
unless
:
这是另一个內建宏unless
的源码:
1 | (defmacro unless |
Again, you have to quote
if
because you want the unevaluated symbol to be placed in the resulting list, like this one:
if
还是必须被引用,因为返回的list里要的是未被求值的符号:
1 | (macroexpand '(unless (done-been slapped? me) |
In many cases, you’ll use simple quoting like this when writing macros, but most often you’ll use the more powerful syntax quote.
很多情况下,写宏使用这样的简单引用,但更经常的是使用更强大的语法引用。
Syntax Quoting
语法引用
So far, you’ve seen macros that build up lists by using the
list
function to create a list along with'
(quote), and functions that operate on lists likefirst
,second
,last
, and so on. Indeed, you could write macros that way until the cows come home. Sometimes, though, it leads to tedious and verbose code.
到此为止,你见到的宏是这样建立的:使用list
函数加上‘
(引用)和操作list的函数,比如first
,second
,last
等创建一个list。但这么写宏既慢又繁琐无聊。
Syntax quoting returns unevaluated data structures, similar to normal quoting. However, there are two important differences. One difference is that syntax quoting will return the fully qualified symbols (that is, with the symbol’s namespace included). Let’s compare quoting and syntax quoting.
语法引用返回未求值的数据结构,与普通引用类似。但有两个重要区别。一个区别是语法引用会返回完全规范的符号(即包含了符号的命名空间)。比较一下二者。
Quoting does not include a namespace if your code doesn’t include a namespace:
如果代码里没有命名空间,引用不包含命名空间:
1 | '+ |
Write out the namespace, and it’ll be returned by normal quote:
代码里有命名空间,普通引用结果里才有:
1 | 'clojure.core/+ |
Syntax quoting will always include the symbol’s full namespace:
语法引用总是返回符号的完整命名空间:
1 | `+ |
Quoting a list recursively quotes all the elements:
引用一个list递归地引用所有成员:
1 | '(+ 1 2) |
Syntax quoting a list recursively syntax quotes all the elements:
语法引用一个list递归地语法引用所有成员:
1 | `(+ 1 2) |
The reason syntax quotes include the namespace is to help you avoid name collisions, a topic covered in Chapter 6.
语法引用包含命名空间的原因是帮助你避免命名冲突,第6章讲过。
The other difference between quoting and syntax quoting is that the latter allows you to unquote forms using the tilde,
~
. It’s kind of like kryptonite in that way: whenever Superman is around kryptonite, his powers disappear. Whenever a tilde appears within a syntax-quoted form, the syntax quote’s power to return unevaluated, fully namespaced forms disappears. Here’s an example:
语法引用与引用的另一个区别是语法引用里可以使用波浪符,~
不引用波浪符后面的东西。就像超人挨着氪星石能力就会消失一样。只要波浪符出现在语法引用内,语法引用的返回未引用和完整命名空间能力就会消失。例子:
1 | `(+ 1 ~(inc 1)) |
Because it comes after the tilde,
(inc 1)
is evaluated instead of being quoted. Without the unquote, syntax quoting returns the unevaluated form with fully qualified symbols:
由于(inc 1)
在波浪符后面,所以被求值而不是被引用了。如果没有不引用,语法引用返回完全规范的未求值形式:
1 | `(+ 1 (inc 1)) |
If you’re familiar with string interpolation, you can think of syntax quoting/unquoting similarly. In both cases, you’re creating a kind of template, placing a few variables within a larger, static structure. For example, in Ruby you can create the string
"Churn your butter, Jebediah!"
through concatenation:
如果你熟悉字符串注入,语法引用/不引用与其类似。他们都是创建一种模版,在更大的,静态结构里放入变量。比如,用Ruby,你可以用连接创建字符串"Churn your butter, Jebediah!"
:
1 | name = "Jebediah" |
or through interpolation:
或使用字符串注入:
1 | "Churn your butter, #{name}!" |
In the same way that string interpolation leads to clearer and more concise code, syntax quoting and unquoting allow you to create lists more clearly and concisely. Compare using the
list
function, shown first, with using syntax quoting:
字符串注入的代码更清晰,更简洁,语法引用和不引用也是这样。比较一下使用list
函数与语法引用:
1 | (list '+ 1 (inc 1)) |
As you can see, the syntax-quote version is more concise. Also, its visual form is closer to the final form of the list, making it easier to understand.
可以看到,语法引用的版本更简洁。并且其视觉形状更接近最终结果,这使它更好理解。
Using Syntax Quoting in a Macro
宏里使用语法引用
Now that you have a good handle on how syntax quoting works, take a look at the
code-critic
macro. You’re going to write a more concise version using syntax quoting.
现在你了解了语法引用如何工作,看一下code-critic
宏。你将要用语法引用写一个更简洁的版本。
1 | (defmacro code-critic |
Just looking at all those tedious repetitions of
list
and single quotes makes me cringe. But if you rewritecode-critic
using syntax quoting, you can make it sleek and concise:
看看那些重复烦人的list
和单引号。如果用语法引用重写code-critic
,会更整齐简洁:
1 | (defmacro code-critic |
In this case, you want to quote everything except for the symbols
good
andbad
. In the original version, you have to quote each piece individually and explicitly place it in a list in an unwieldy fashion, just to prevent those two symbols from being quoted. With syntax quoting, you can just wrap the entiredo
expression in a quote and simply unquote the two symbols that you want to evaluate.
这个例子里,要引用除了符号good
和bad
的所有东西。前面的版本里,不得不单独引用每个符号,并放在list里。有了语法引用,只需要吧整个do
表达式用语法引用包起来,再不引用那些需要求值的符号就可以了。
And thus concludes the introduction to the mechanics of writing a macro! Sweet sacred boa of Western and Eastern Samoa, that was a lot!
写宏的机制就介绍到这里了,真不少啊!
To sum up, macros receive unevaluated, arbitrary data structures as arguments and return data structures that Clojure evaluates. When defining your macro, you can use argument destructuring just like you can with functions and
let
bindings. You can also write multiple-arity and recursive macros.
总结一下,宏接受任意的不求值的数据结构,并返回Clojure求值的数据结构。定义宏时候,可以用参数解构和let
绑定,跟函数一样。也可以写多套参数和递归宏。
Most of the time, your macros will return lists. You can build up the
list
to be returned by using list functions or by using syntax quoting. Syntax quoting usually leads to code that’s clearer and more concise because it lets you create a template of the data structure you want to return that’s easier to parse visually. Whether you use syntax quoting or plain quoting, it’s important to be clear about the distinction between a symbol and the value it evaluates to when building up your list. And if you want your macro to return multiple forms for Clojure to evaluate, make sure to wrap them in ado
.
大多数情况下,宏会返回list。可以使用list函数或语法引用建立这个list。由于语法引用建立的是返回的数据结构的模版,所以代码更清晰,简洁,视觉上更容易辨认。不管使用引用还是语法引用,重要的是在建立list时候你要区分清楚符号和值。还有,如果希望宏返回多个形式用于求值,要确保用do
把他们包起来。
Refactoring a Macro and Unquote Splicing
重构宏与打开不引用
That
code-critic
macro in the preceding section could still use some improvement. Look at the duplication! The twoprintln
calls are nearly identical. Let’s clean that up. First, let’s create a function to generate thoseprintln
lists. Functions are easier to think about and play with than macros, so it’s often a good idea to move macro guts to helper functions:
上一节的code-critic
宏还能优化。看下重复部分。两个println
调用几乎完全一样。开始清理。首先创建一个函数,用于生成那些println
list。函数比宏更容易思考和使用,所以经常把宏内部部分移到辅助函数里:
1 | (defn criticize-code |
Notice how the
criticize-code
function returns a syntax-quoted list. This is how you build up the list that the macro will return.
注意函数criticize-code
是如何返回一个语法引用list的。这就是构建宏返回的list的方法。
There’s more room for improvement, though. The code still has multiple, nearly identical calls to a function. In a situation like this where you want to apply the same function to a collection of values, it makes sense to use a seq function like
map
:
然而还有提升空间。这个代码仍然有多次几乎同样的函数调用。这种需要对值的集合调用同样函数的情况,可以使用map
这样的序列函数:
1 | (defmacro code-critic |
This is looking a little better. You’re mapping over each criticism/code pair and applying the
criticize-code
function to the pair. Let’s try to run the code:
这看起来好点了。对每对criticism/code调用criticize-code
函数。尝试运行一下:
1 | (code-critic (1 + 1) (+ 1 1)) |
Oh no! That didn’t work at all! What happened? The problem is that
map
returns a list, and in this case, it returned a list ofprintln
expressions. We just want the result of eachprintln
call, but instead, this code sticks both results in a list and then tries to evaluate that list.
出现异常了,什么情况?问题出在map
返回一个list,这个例子里返回的是一个println
表达式的list。我们想要的只是每个println
调用的结果,但这段代码把这些结果放在了一个list里,然后对这个list求值。
In other words, as it’s evaluating this code, Clojure gets to something like this:
换句话说,求值这段代码是,Clojure得到的是这个:
1 | (do |
then evaluates the first
println
call to give us this:
然后求值第一个println
调用,得到这个:
1 | (do |
and after evaluating the second
println
call, does this:
然后求值第二个println
调用得到这个:
1 | (do |
This is the cause of the exception.
println
evaluates tonil
, so we end up with something like(nil nil)
.nil
isn’t callable, and we get aNullPointerException
.
这就是异常的原因,println
求值为nil
,最后得到的是(nil nil)
。nil
不可调用,于是抛出异常NullPointerException
。
What an inconvenience! But as it happens, unquote splicing was invented precisely to handle this kind of situation. Unquote splicing is performed with
~@
. If you merely unquote a list, this is what you get:
太不方便了!打开不引用就是专门发明出来处理这种情况的。用~@
可以执行打开不引用。如果只是不引用一个list,会得到这个:
1 | `(+ ~(list 1 2 3)) |
However, if you use unquote splicing, this is what you get:
但如果使用打开不引用,会得到这个:
1 | `(+ ~@(list 1 2 3)) |
Unquote splicing unwraps a seqable data structure, placing its contents directly within the enclosing syntax-quoted data structure. It’s like the
~@
is a sledgehammer and whatever follows it is a piñata, and the result is the most terrifying and awesome party you’ve ever been to.
打开不引用打开一个可序列化的数据结构,把其内容直接放在包含他的语法引用数据结构里。~@
就像个锤子,跟在它后面的东西就像个皮纳塔盒子,结果可想而知。
Anyway, if you use unquote splicing in your code critic, then everything will work great:
前面的代码里使用了打开不引用,就一切ok了:
1 | (defmacro code-critic |
Woohoo! You’ve successfully extracted repetitive code into a function and made your macro code cleaner. Sweet guinea pig of Winnipeg, that is good code!
哇!你成功地把重复代码提取出来放入了一个函数,并使宏代码更加清晰。干的漂亮!
Things to Watch Out For
留意提防的东西
Macros have a couple of sneaky gotchas that you should be aware of. In this section, you’ll learn about some macro pitfalls and how to avoid them. I hope you haven’t unstrapped yourself from your thinking mast.
这章会讲述几个宏里隐蔽陷阱,需要特别注意。
Variable Capture
变量捕获
Variable capture occurs when a macro introduces a binding that, unknown to the macro’s user, eclipses an existing binding. For example, in the following code, a macro mischievously introduces its own
let
binding, and that messes with the code:
如果宏引入了宏用户不知道的绑定,会发生变量捕获,宏里的绑定优先级更高而屏蔽掉外面的绑定。比如下面的宏引入了自己的let
绑定:
1 | (def message "Good job!") |
The
println
call references the symbolmessage
, which we think is bound to the string"Good job!"
. However, thewith-mischief
macro has created a new binding formessage
.
println
调用引用了被认为是字符串"Good job!"
的符号message
。但with-mischief
宏为message
新建了一个绑定。
Notice that this macro didn’t use syntax quoting. Doing so would result in an exception:
注意这个宏没用语法引用。用了会引起异常:
1 | (def message "Good job!") |
This exception is for your own good: syntax quoting is designed to prevent you from accidentally capturing variables within macros. If you want to introduce
let
bindings in your macro, you can use a gensym. Thegensym
function produces unique symbols on each successive call:
这个异常是为了你好:语法引用就是这么设计的,让你不会意外捕获宏内的变量。如果想用let
在宏内引入绑定,可以使用符号生成。gensym
函数每次调用产生一个唯一符号:
1 | (gensym) |
You can also pass a symbol prefix:
也可以传入符号前缀:
1 | (gensym 'message) |
Here’s how you could rewrite
with-mischief
to be less mischievous:
这是重写的with-mischief
:
1 | (defmacro without-mischief |
This example avoids variable capture by using
gensym
to create a new, unique symbol that then gets bound tomacro-message
. Within the syntax-quotedlet
expression,macro-message
is unquoted, resolving to the gensym’d symbol. This gensym’d symbol is distinct from any symbols withinstuff-to-do
, so you avoid variable capture. Because this is such a common pattern, you can use an auto-gensym. Auto-gensyms are more concise and convenient ways to use gensyms:
这个例子使用gensym
创建了一个新的唯一符号,然后绑定到macro-message
,这就避免了变量捕获。在语法引用的let
表达式内部,macro-message
被不引用,求值为那个符号生成的符号。这个符号与stuff-to-do
里的任何符号不同,所以避免了变量捕获。由于这是个非常普遍的模式,可以使用自动符号生成。自动符号生成是符号生成的更简洁方便的用法:
1 | `(blarg# blarg#) |
In this example, you create an auto-gensym by appending a hash mark (or hashtag, if you must insist) to a symbol within a syntax-quoted list. Clojure automatically ensures that each instance of x
#
resolves to the same symbol within the same syntax-quoted list, that each instance of y#
resolves similarly, and so on.
这个例子中的语法引用里有符号,通过在这个符号后面增加一个井号,创建了一个自动符号生成。Clojure自动确保在同一个语法引用list里,每个x#
都解析为同样的符号。每个y#
也类似,诸如此类。
gensym
and auto-gensym are both used all the time when writing macros, and they allow you to avoid variable capture.
写宏时经常会用到gensym
和auto-gensym,使你能避免变量捕获。
Double Evaluation
二次求值
Another gotcha to watch out for when writing macros is double evaluation, which occurs when a form passed to a macro as an argument gets evaluated more than once. Consider the following:
写宏时另一个需要小心的地方是,当一个作为参数传给宏的形式被求值不止一次,就发生了二次求值。例子:
1 | (defmacro report |
This code is meant to test its argument for truthiness. If the argument is truthy, it’s considered successful; if it’s falsey, it’s unsuccessful. The macro prints whether or not its argument was successful. In this case, you would actually sleep for two seconds because
(Thread/sleep 1000)
gets evaluated twice: once right afterif
and again whenprintln
gets called. This happens because the code(do (Thread/sleep 1000) (+ 1 1))
is repeated throughout the macro expansion. It’s as if you’d written this:
这段代码想要测试参数的真伪。如果参数为真,则视为成功,否则视为失败。宏打印出参数是否成功。这个例子里,实际上休眠了两秒,因为(Thread/sleep 1000)
被求值了两次:if
后面一次,println
调用时候一次。宏展开的时候,代码(do (Thread/sleep 1000) (+ 1 1))
被重复了,就好像这么写一样:
1 | (if (do (Thread/sleep 1000) (+ 1 1)) |
“Big deal!” your inner example critic says. Well, if your code did something like transfer money between bank accounts, this would be a very big deal. Here’s how you could avoid this problem:
如果代码是银行账号间转账,这可是大问题!可以这么避免:
1 | (defmacro report |
By placing
to-try
in alet
expression, you only evaluate that code once and bind the result to an auto-gensym’d symbol,result#
, which you can now reference without reevaluating theto-try
code.
通过把to-try
放在let
表达式里,对其求值一次,并把结果与自动符号生成的符号result#
绑定。之后就可以引用result#
而不会对to-try
重复求值。
Macros All the Way Down
宏越写越多
One subtle pitfall of using macros is that you can end up having to write more and more of them to get anything done. This is a consequence of the fact that macro expansion happens before evaluation.
一个隐蔽的宏陷阱是为了完成一件事,要写的宏可能越来越多。这是宏先展开再求值的结果。
For example, let’s say you wanted to
doseq
using thereport
macro. Instead of multiple calls to report:
比如说你想在doseq
里使用report
宏,而不是多次调用report
:
1 | (report (= 1 1)) |
let’s iterate:
用doseq
迭代调用:
1 | (doseq [code ['(= 1 1) '(= 1 2)]] |
The report macro works fine when we pass it functions individually, but when we use
doseq
to iteratereport
over multiple functions, it’s a worthless failure. Here’s what a macro expansion for one of thedoseq
iterations would look like:
report宏单独使用时没有问题,但用在doseq
里迭代调用时候失败了。这是doseq
中某一次宏展开的样子:
1 | (if |
As you can see,
report
receives the unevaluated symbolcode
in each iteration; however, we want it to receive whatevercode
is bound to at evaluation time. Butreport
, operating at macro expansion time, just can’t access those values. It’s like it has T. rex arms, with runtime values forever out of its reach.
可以看见,每次迭代里report
接受的是未求值的符号code
;但我们想让他接受的是求值时候code
绑定的值。但report
作用于宏展开时,无法访问到这些值。就像暴龙的胳膊,对于运行时候的值,永远够不着。
译者补充:这里有点绕,可以直接用macroexpand
看一下展开结果,把上面的代码与下面展开结果的(if)
部分一起看,更方便理解下段的解释:
1 | (macroexpand '(report (= 1 1))) |
To resolve this situation, we might write another macro, like this:
为了解决这个问题,我们可能又写了一个这样的宏:
1 | (defmacro doseq-macro |
If you are ever in this situation, take some time to rethink your approach. It’s easy to paint yourself into a corner, making it impossible to accomplish anything with run-of-the-mill function calls. You’ll be stuck having to write more macros instead. Macros are extremely powerful and awesome, and you shouldn’t be afraid to use them. They turn Clojure’s facilities for working with data into facilities for creating new languages informed by your programming problems. For some programs, it’s appropriate for your code to be like 90 percent macros. As awesome as they are, they also add new composition challenges. They only really compose with each other, so by using them, you might be missing out on the other kinds of composition (functional, object-oriented) available to you in Clojure.
如果你经历过这种情况,花点时间重新思考你的方法。你很容易陷入用普通函数无法完成任务的困境,但你将会卡在写更多的宏上。宏很好很强大,不该害怕使用它。宏把Clojure从一个数据工具变成了一个建立用于解决问题的新语言的工具。对于一些程序,代码里有百分之90的宏是合适的。宏虽然很棒,但带来了新的组合挑战。他们只能互相组合,所以用了他们,可能会错过Clojure里其他可用组合(函数的,对象的)。
We’ve now covered all the mechanics of writing a macro. Pat yourself on the back! It’s a pretty big deal!
我们学习了写宏的所有机制,真棒!
To close out this chapter, it’s finally time to put on your pretending cap and work on that online potion store I talked about at the very beginning of the chapter.
最后,是一个验证订单例子。
Brews for the Brave and True
酝酿勇敢与真实
At the beginning of this chapter, I revealed a dream: to find some kind of drinkable that, once ingested, would temporarily give me the power and temperament of an ’80s fitness guru, freeing me from a prison of inhibition and self-awareness. I’m sure that someone somewhere will someday invent such an elixir, so we might as well get to work on a system for selling this mythical potion. Let’s call this hypothetical concoction the Brave and True Ale. The name just came to me for no reason whatsoever.
略过。
Before the orders come pouring in (pun! high-five!), we’ll need to have some validation in place. This section shows you a way to do this validation functionally and how to write the code that performs validations a bit more concisely using a macro you’ll write called
if-valid
. This will help you understand a typical situation for writing your own macro. If you just want the macro definition, it’s okay to skip ahead to “if-valid” on page 182.
这节学习如何写一个订单验证宏if-valid
。使你理解什么时候需要自己写宏。如果只想看宏定义,可以跳到下一节”“if-valid”。
Validation Functions
验证函数
To keep things simple, we’ll just worry about validating the name and email for each order. For our store, I’m thinking we’ll want to have those order details represented like this:
为了简单,只验证每个订单的姓名和邮件。可能会有这样的订单:
1 | (def order-details |
This particular map has an invalid email address (it’s missing the
@
symbol), so this is exactly the kind of order that our validation code should catch! Ideally, we want to write code that produces something like this:
这个map里的邮件地址不合法,缺少@
符号,验证程序应该能发现。验证程序应该产生这样的结果:
1 | (validate order-details order-details-validations) |
That is, we want to be able to call a function,
validate
, with the data that needs validation and a definition for how to validate it. The result should be a map where each key corresponds to an invalid field, and each value is a vector of one or more validation messages for that field. The following two functions do the job.
调用的函数是validate
,参数是需要验证的数据和验证定义。结果应该是一个map,map的每个key对应一个不合法项目,每个值是个包含一个或多个对应域的验证信息的vector。两个函数如下:
Let’s look at
order-details-validations
first. Here’s how you could represent validations:
先看看order-details-validations
,可以这么表示验证:
1 | (def order-details-validations |
This is a map where each key is associated with a vector of error message and validating function pairs. For example,
:name
has one validating function,not-empty
; if that validation fails, you should get the"Please enter a name"
error message.
这是个map,每个key与一个vector关联,vector包含的是错误消息与验证函数对。比如:name
有一个验证函数not-empty
,如果验证失败,会得到错误消息"Please enter a name"
。
Next, we need to write out the
validate
function. The validate function can be decomposed into two functions: one to apply validations to a single field and another to accumulate those error messages into a final map of error messages like{:email ["Your email address doesn't look like an email address."]}
. Here’s a function callederror-messages-for
that applies validations to a single value:
接下来是validate
函数。这个函数可以分解成两个函数:一个验证单个域,另一个累积错误消息到最终的这样的map里,{:email ["Your email address doesn't look like an email address."]}
。下面是函数error-messages-for
,验证单独值:
1 | (defn error-messages-for |
The first argument,
to-validate
, is the field you want to validate. The second argument,message-validator-pairs
, should be a seq with an even number of elements. This seq gets grouped into pairs with(partition 2 message-validator-pairs)
. The first element of the pair should be an error message, and the second element of the pair should be a function (just like the pairs are arranged inorder-details-validations
). Theerror-messages-for
function works by filtering out all error message and validation pairs where the validation function returnstrue
when applied toto-validate
. It then usesmap first
to get the first element of each pair, the error message. Here it is in action:
第一个参数to-validate
是要验证的域。第二个参数message-validator-pairs
应该是一个有偶数个成员的序列。用(partition 2 message-validator-pairs)
把这个序列分组成配对成员,每对第一个成员是错误信息,第二个是函数(就像order-details-validations
一样)。error-messages-for
过滤出所有调用to-validate
返回true
的错误信息。实际调用:
1 | (error-messages-for "" ["Please enter a name" not-empty]) |
Now we need to accumulate these error messages in a map.
现在需要把这些错误消息累积进一个map里。
Here’s the complete
validate
function, as well as the output when we apply it to ourorder-details
andorder-details-validations
:
这是完整的validate
函数,和用参数order-details
,order-details-validations
调用后的结果:
1 | (defn validate |
Success! This works by reducing over
order-details-validations
and associating the error messages (if there are any) for each key oforder-details
into a final map of error messages.
成功了!方法是对order-details-validations
进行reduce,如果有错误,就把order-details
每个key对应的所有错误信息关联进最终map里。
if-valid
if-valid
With our validation code in place, we can now validate records to our hearts’ content! Most often, validation will look something like this:
现在我们可以验证了!验证程序是这样的:
1 | (let [errors (validate order-details order-details-validations)] |
The pattern is to do the following:
- Validate a record and bind the result to
errors
- Check whether there were any errors
- If there were, do the success thing, here
(println :success)
- Otherwise, do the failure thing, here
(println :failure errors)
这段代码做了下列事情:
- 验证记录并把结果绑定到
errors
- 检查是否存在错误
- 如果不存在错误,运行成功部分,
(println :success)
- 否则运行失败部分,
(println :failure errors)
I’ve actually used this validation code in real production websites. At first, I found myself repeating minor variations of the code over and over, a sure sign that I needed to introduce an abstraction that would hide the repetitive parts: applying the
validate
function, binding the result to some symbol, and checking whether the result is empty. To create this abstraction, you might be tempted to write a function like this:
我在实际项目里使用了这个验证代码。我发现一些重复:调用validate
函数,结果绑定到一个符号,检查结果是否为空。这是个明确的信号,需要引入一个抽象隐藏这些重复部分。为此,你可能会这样的函数:
1 | (defn if-valid |
However, this wouldn’t work, because
success-code
andfailure-code
would get evaluated each time. A macro would work because macros let you control evaluation. Here’s how you’d use the macro:
但这样不行,因为每次success-code
和failure-code
都会被求值。但宏可行,因为宏可以控制求值。下面是宏调用:
1 | (if-valid order-details order-details-validations errors |
This macro hides the repetitive details and helps you express your intention more succinctly. It’s like asking someone to give you the bottle opener instead of saying, “Please give me the manual device for removing the temporary sealant from a glass container of liquid.” Here’s the implementation:
这个宏隐藏了重复的细节,有助于更简洁地表达出意图。就像说:给我瓶起子,而不是说:给我用于去除装液体的玻璃容器的临时封口材料的手动设备。下面是实现:
1 | (defmacro if-valid |
This macro takes four arguments:
to-validate
,validations
,errors-name
, and the rest argumentthen-else
. Usingerrors-name
like this is a new strategy. We want to have access to the errors returned by thevalidate
function within thethen-else
statements. To do this, we tell the macro what symbol it should bind the result to. The following macro expansion shows how this works:
这个宏接受四个参数:to-validate
,validations
,errors-name
,和剩余参数then-else
。这么用errors-name
是个新招。因为要在then-else
里访问validate
函数返回的错误结果,所以把这个结果对应的符号传给宏。看看下面的宏展开:
1 | (macroexpand |
The syntax quote abstracts the general form of the
let/validate/if
pattern you saw earlier. Then we use unquote splicing to unpack theif
branches, which were packed into thethen-else
rest argument.
语法引用抽象掉了前面的let/validate/if
模式。然后使用打开不引用展开包在then-else
生育参数里的if
分支。
That’s pretty simple! After all this talk about macros and going through their mechanics in such detail, I bet you were expecting something more complicated. Sorry, friend. If you’re having a hard time coping with your disappointment, I know of a certain drink that will help.
非常简单!我们已经讲述了宏和所有宏的机制。
Summary
总结
In this chapter, you learned how to write your own macros. Macros are defined very similarly to functions: they have arguments, a docstring, and a body. They can use argument destructuring and rest args, and they can be recursive. Your macros will almost always return lists. You’ll sometimes use
list
andseq
functions for simple macros, but most of the time you’ll use the syntax quote,`
, which lets you write macros using a safe template.
这章讲了如何写自己的宏。宏定义与函数类似:有参数,文档字符串和主体。可以使用参数解构和剩余参数,可以递归。宏几乎总是返回list。有时候会用list
和seq
函数写简单的宏,但大多数情况使用语法引用`
,它让我们用一个安全的模版写宏。
When you’re writing macros, it’s important to keep in mind the distinction between symbols and values: macros are expanded before code is evaluated and therefore don’t have access to the results of evaluation. Double evaluation and variable capture are two other subtle traps for the unwary, but you can avoid them through the judicious use of
let
expressions and gensyms.
写宏时候很重要的是时刻区分清楚符号和值:宏求值前先展开,因此无法访问求值结果。二次求值和变量捕获是两个隐蔽的陷阱,但可以通过使用let
表达式和符号生成避免。
Macros are fun tools that allow you to code with fewer inhibitions. By letting you control evaluation, macros give you a degree of freedom and expression that other languages simply don’t allow. Throughout your Clojure journey, you’ll probably hear people cautioning you against their use, saying things like “Macros are evil” and “You should never use macros.” Don’t listen to these prudes—at least, not at first! Go out there and have a good time. That’s the only way you’ll learn the situations where it’s appropriate to use macros. You’ll come out the other side knowing how to use macros with skill and panache.
宏是有趣的工具,使你写代码受到的限制更少。通过控制求值,宏给你提供了一些其他语言不允许的自由和表达力。在你的Clojurea旅途中,可能会有人警告你,说宏是魔鬼,永远也别使用宏之类的话。至少开始不要听他们的。该用就用,乐享其中。只有这样,才能学会应该何时用宏,才能学会使用宏的技巧。
Exercises
练习
Write the macro
when-valid
so that it behaves similarly towhen
. Here is an example of calling it:(when-valid order-details order-details-validations (println "It's a success!") (render :success))
When the data is valid, the println and render forms should be evaluated, and when-valid should return nil if the data is invalid.
You saw that
and
is implemented as a macro. Implementor
as a macro.In Chapter 5 you created a series of functions (
c-int
,c-str
,c-dex
) to read an RPG character’s attributes. Write a macro that defines an arbitrary number of attribute-retrieving functions using one macro call. Here’s how you would call it:(defattrs c-int :intelligence c-str :strength c-dex :dexterity)
- 写一个
when-valid
宏,使其与when
类似,调用例子:
数据合法时,println和render应该执行,否则返回nil。 - 我们看到
and
是用宏实现的,用宏实现or
。 - 第5章你建立了一些函数(
c-int
,c-str
,c-dex
)读取RPG角色的属性。写一个宏,使你用一个宏就能定义任意个属性获取函数。调用方法如下:
译文结束。