├── 1_Quote_and_unquote.markdown ├── 2_Macros.markdown └── 3_Domain_Specific_Languages.markdown /1_Quote_and_unquote.markdown: -------------------------------------------------------------------------------- 1 | #引用与去引用 2 | 3 | 1. 引用(Quoting) 4 | 2. 去引用(Unquoting) 5 | 3. 释放(Escaping) 6 | 7 | 一个Elixir程序可以用它自己的数据结构来表现.本章,我们将会学习这些结构体的特点和如何组成它们.本章我们要学习的概念是为宏的积木(building blocks),在下一章中我们将深入研究它. 8 | 9 | #引用(Quoting) 10 | 11 | Elixir程序中的积木是一个三元素元组.例如,函数`sum(1, 2, 3)`的内部表示是: 12 | 13 | ``` 14 | {:sum, [], [1, 2, 3]} 15 | ``` 16 | 17 | 你可以用`quote`宏来得到任何表达式的内部表现: 18 | 19 | ``` 20 | iex> quote do: sum(1, 2, 3) 21 | {:sum, [], [1, 2, 3]} 22 | ``` 23 | 24 | 第一个元素是函数名,第二个元素是一个包含了元数据的关键词列表,第三个元素是参数列表. 25 | 26 | 操作符也可以用元组来表示: 27 | 28 | ``` 29 | iex> quote do: 1 + 2 30 | {:+, [context: Elixir, import: Kernel], [1, 2]} 31 | ``` 32 | 33 | 甚至一个映射都被表示成对`%{}`的调用: 34 | 35 | ``` 36 | iex> quote do: %{1 => 2} 37 | {:%{}, [], [{1, 2}]} 38 | ``` 39 | 40 | 变量也能这样三段式表示,只不过最后的元素换成了一个原子: 41 | 42 | ``` 43 | iex> quote do: x 44 | {:x, [], Elixir} 45 | ``` 46 | 47 | 当引用更复杂的表达式时,代码被表示成了一个树状的嵌套元组.许多语言将这种表示称为抽象语法树(AST).ELixir称其为引用表达式: 48 | 49 | ``` 50 | iex> quote do: sum(1, 2 + 3, 4) 51 | {:sum, [], [1, {:+, [context: Elixir, import: Kernel], [2, 3]}, 4]} 52 | ``` 53 | 54 | 有时,把引用表达式还原为文本代码会很有用.可以用`Macro.to_string/1`来完成: 55 | 56 | ``` 57 | iex> Macro.to_string(quote do: sum(1, 2 + 3, 4)) 58 | "sum(1, 2 + 3, 4)" 59 | ``` 60 | 61 | 通常,上述元组的结构会是这种格式: 62 | 63 | ``` 64 | {atom | tuple, list, list | atom} 65 | ``` 66 | 67 | \- 第一个元素是一个原子,或者是同样表达方式的另一个元组; 68 | \- 第二个元素是一个关键词列表,包含元数据,比如数字和语境; 69 | \- 第三个元素是函数调用的参数列表或者是一个原子.当为原子时,意味着元组表示的是一个变量. 70 | 71 | 除了上面定义的元组,有五个Elixir字面量,在被引用时,会返回它们自己(而不是一个元组).它们是: 72 | 73 | ``` 74 | :sum #=> Atoms 75 | 1.0 #=> Numbers 76 | [1, 2] #=> Lists 77 | "strings" #=> Strings 78 | {key, value} #=> Tuples with two elements 79 | ``` 80 | 81 | 大多数Elixir代码都有一个直截了当的引用表达式.我们建议你尝试不同的代码,看看结果如何.比如,`String.upcase("foo")`会如何展开?我们已经知道`if(true, do: :this, else: :that)`等同于`if true do :this else :that end`.它要如何用引用表达式来容纳? 82 | 83 | #去引用(Unquoting) 84 | 85 | 引用可以获得一些特定代码块的内部表达式.然而,有时我们需要注入一些特定代码块到我们想要获取的表达式中. 86 | 87 | 例如,假设我们有一个变量,包含了我们想注入一个引用表达式中的数字,变量名为`number`. 88 | 89 | ``` 90 | iex> number = 13 91 | iex> Macro.to_string(quote do: 11 + number) 92 | "11 + number" 93 | ``` 94 | 95 | 那不是我们想要的,`number`变量被引入了表达式,但`number`变量的值没有被注入.为了注入`number`变量的值,我们需要在引用表达式中使用`unquote`: 96 | 97 | ``` 98 | iex> number = 13 99 | iex> Macro.to_string(quote do: 11 + unquote(number)) 100 | "11 + 13" 101 | ``` 102 | 103 | `unquote`甚至可以被用于注入函数名: 104 | 105 | ``` 106 | iex> fun = :hello 107 | iex> Macro.to_string(quote do: unquote(fun)(:world)) 108 | "hello(:world)" 109 | ``` 110 | 111 | 在相同的情况下,可能需要注入一个有许多值的列表.比如,假设你有一个列表`[1, 2, 6]`,我们想把`[3, 4, 5]`注入进去.使用`unquote`却不能得到想要的结果: 112 | 113 | ``` 114 | iex> inner = [3, 4, 5] 115 | iex> Macro.to_string(quote do: [1, 2, unquote(inner), 6]) 116 | "[1, 2, [3, 4, 5], 6]" 117 | ``` 118 | 119 | 这时就轮到`unquote_splicing`出场了: 120 | 121 | ``` 122 | iex> inner = [3, 4, 5] 123 | iex> Macro.to_string(quote do: [1, 2, unquote_splicing(inner), 6]) 124 | "[1, 2, 3, 4, 5, 6]" 125 | ``` 126 | 127 | 去引用在操作宏的时候很有用.编写宏的时候,开发者可以获取代码块并将它们注入到其它代码块中,这可以被用于改变代码或者编写能在编译时生成代码的代码. 128 | 129 | #释放(Escaping) 130 | 131 | 如本章开头所见,Elixir中只有一部分值有合法的引用表达式.比如,一个映射没有合法的引用表达式.四元素的元组也没有.然而,这些值可以被表示成一个引用表达式: 132 | 133 | ``` 134 | iex> quote do: %{1 => 2} 135 | {:%{}, [], [{1, 2}]} 136 | ``` 137 | 138 | 有时你需要将这种值注入引用表达式中.我们首先需要将这些值释放到引用表达式中,使用`Macro.escape/1`来完成: 139 | 140 | ``` 141 | iex> map = %{hello: :world} 142 | iex> Macro.escape(map) 143 | {:%{}, [], [hello: :world]} 144 | ``` 145 | 146 | 宏接收引用表达式,并必须返回引用表达式.然而执行宏的过程中,你可能需要处理一些值,并且区分值与引用表达式. 147 | 148 | 也就是说,区分常规的Elixir值(例如列表,映射,进程,参考等等)与引用表达式,是很重要的.有一些值的引用表达式就是它们自己,例如整数,原子核字符串.另一些值,比如映射,需要被显式转换.最后,函数与参考完全不能被转化成引用表达式. 149 | 150 | 你可以在`Kernel.SpecialForms`模块中阅读更多关于`quote`和`unquote`的内容.在`Macro`模块中可以找到`Macro.escape/1`的文档和其它与引用表达式相关的函数. 151 | 152 | 在本教程中我们将编写自己的第一个宏,让我们进入下一章吧. 153 | -------------------------------------------------------------------------------- /2_Macros.markdown: -------------------------------------------------------------------------------- 1 | #宏 2 | 3 | 1. 前言 4 | 2. 我们的第一个宏 5 | 3. 宏的隔离 6 | 4. 环境 7 | 5. 私有宏 8 | 6. 负责任地编写宏 9 | 10 | #前言 11 | 12 | 尽管Elixir已竭力为宏提供一个安全的环境,用宏编写干净代码的责任仍然落在了开发者身上.宏比传统的Elixir函数更难编写,而且在不必要的场合使用宏是不好的.所以请负责任地编写宏. 13 | 14 | Elixir已经提供了许多数据结构和函数,能够让你以简单可读的风格编写日常代码.宏应当是最后的选择.记住,**明显胜过含蓄.清晰的代码胜过简洁的代码.** 15 | 16 | #我们的第一个宏 17 | 18 | ELixir中使用`defmacro/2`来定义宏. 19 | 20 | > 本章,我们将使用文件来代替在IEx中运行样本代码.这是因为代码样本将跨越许多行,将它们全部输入IEx会适得其反.你应当将代码样本保存进`macro.exs`文件,并使用`elixir macros.exs`或`iex macro.exs`来运行. 21 | 22 | 为了更好地理解宏是如何运作的,让我们创建一个新的模块,在其中实现`unless`,它的作用与`if`相反.分别以函数和宏的形式: 23 | 24 | ``` 25 | defmodule Unless do 26 | def fun_unless(clause, expression) do 27 | if(!clause, do: expression) 28 | end 29 | 30 | defmacro macro_unless(clause, expression) do 31 | quote do 32 | if(!unquote(clause), do: unquote(expression)) 33 | end 34 | end 35 | end 36 | ``` 37 | 38 | 函数接收了参数,并传送给`if`.然而正如我们在前一章所学过的,宏会接收引用表达式,将它们注入引用,最后返回另一个引用表达式. 39 | 40 | 让我们用`iex`运行上面的模块: 41 | 42 | ``` 43 | $ iex macros.exs 44 | ``` 45 | 46 | 调戏一下那些定义: 47 | 48 | ``` 49 | iex> require Unless 50 | iex> Unless.macro_unless true, IO.puts "this should never be printed" 51 | nil 52 | iex> Unless.fun_unless true, IO.puts "this should never be printed" 53 | "this should never be printed" 54 | nil 55 | ``` 56 | 57 | 注意,在宏的实现中,句子没有被打印,然而在函数的实现中,句子被打印了.这是因为函数的参数会在调用函数之前被执行.而宏不会执行它们的参数.它们以引用表达式的形式接收参数,之后又将其变形为其它引用表达式.本例中,我们实际上是将`unless`宏重写成了一个`if`. 58 | 59 | 换句话说,当被这样调用时: 60 | 61 | ``` 62 | Unless.macro_unless true, IO.puts "this should never be printed" 63 | ``` 64 | 65 | 我们的`macro_unless`宏接收到了: 66 | 67 | ``` 68 | macro_unless(true, {{:., [], [{:aliases, [], [:IO]}, :puts]}, [], ["this should never be printed"]}) 69 | ``` 70 | 71 | 然后返回了一个引用表达式: 72 | 73 | ```Elixir 74 | {:if, [], 75 | [{:!, [], [true]}, 76 | [do: {{:., [], 77 | [{:__aliases__, 78 | [], [:IO]}, 79 | :puts]}, [], ["this should never be printed"]}]]} 80 | ``` 81 | 82 | 我们可以使用`Macro.expand_once/2`来验证它: 83 | 84 | ``` 85 | iex> expr = quote do: Unless.macro_unless(true, IO.puts "this should never be printed") 86 | iex> res = Macro.expand_once(expr, __ENV__) 87 | iex> IO.puts Macro.to_string(res) 88 | if(!true) do 89 | IO.puts("this should never be printed") 90 | end 91 | :ok 92 | ``` 93 | 94 | `Macro.expand_once/2`接收了引用表达式,并根据当前环境扩展了它.本例中,它扩展/调用了`Unless.macro_unless/2`宏,并返回了结果.之后我们将返回的引用表达式转换成一个字符串并打印出来(我们将在本章稍后的位置讨论`__ENV__`). 95 | 96 | 这就是宏.它们接收引用表达式并将其变形为别的东西.事实上,Elixir中的`unless/2`是作为宏来实现的: 97 | 98 | ``` 99 | defmacro unless(clause, options) do 100 | quote do 101 | if(!unquote(clause), do: unquote(options)) 102 | end 103 | end 104 | ``` 105 | 106 | 本教程中用到的许多纯Elixir实现的结构都是宏,例如`unless/2`,`defmacro/2`,`def/2`,`defprotocol/2`等等.这意味着,开发者可以用构建语言的结构来将语言扩展到它们工作的领域. 107 | 108 | 我们可以定义任何函数和宏,甚至覆盖Elixir中的原本定义.唯一的例外是Elixir特殊形式,它们不是由Elixir实现的,因此不能被覆盖,特殊形式的完整列表可以在`Kernel.SpecialForms`中找到. 109 | 110 | #宏的隔离(Macros hygiene) 111 | 112 | Elixir的宏有着低决定权.这保证了引用中的变量定义不会与宏被扩展到的语境中的变量定义相冲突.例如: 113 | 114 | ``` 115 | defmodule Hygiene do 116 | defmacro no_interference do 117 | quote do: a = 1 118 | end 119 | end 120 | 121 | defmodule HygieneTest do 122 | def go do 123 | require Hygiene 124 | a = 13 125 | Hygiene.no_interference 126 | a 127 | end 128 | end 129 | 130 | HygieneTest.go 131 | # => 13 132 | ``` 133 | 134 | 上述例子中,即使宏注入了`a = 1`,却没有影响到变量`a`在函数`go`中的定义.如果宏想要明确地影响语境,可以使用`var!`: 135 | 136 | ``` 137 | defmodule Hygiene do 138 | defmacro interference do 139 | quote do: var!(a) = 1 140 | end 141 | end 142 | 143 | defmodule HygieneTest do 144 | def go do 145 | require Hygiene 146 | a = 13 147 | Hygiene.interference 148 | a 149 | end 150 | end 151 | 152 | HygieneTest.go 153 | # => 1 154 | ``` 155 | 156 | 因为Elixir使用变量的语境来注解它,所以能够实现变量隔离.例如,一个模块的第三行定义的变量`x`可以被表示成: 157 | 158 | ``` 159 | {:x, [line: 3], nil} 160 | ``` 161 | 162 | 然而一个引用变量是这样表示的: 163 | 164 | ``` 165 | defmodule Sample do 166 | def quoted do 167 | quote do: x 168 | end 169 | end 170 | 171 | Sample.quoted #=> {:x, [line: 3], Sample} 172 | ``` 173 | 174 | 注意引用变量的第三个元素是原子`Sample`,而不是`nil`,它标记了变量是来自`Sample`模块的.因此,Elixir认为这两个变量来自不同语境,会分别处理它们. 175 | 176 | Elixir也为进口(imports)和别名(aliases)提供了相似的机制.这保证了宏的行为会与它源模块中的定义相同,而不是与宏所扩展到的目标模块相冲突.使用类似`var!/2`和`alias!/2`之类的宏可以突破隔离,但是它们必须小心使用,因为这直接改变了用户环境. 177 | 178 | 有时,变量名会被动态地创建.`Macro.var/2`可用于定义新变量: 179 | 180 | ``` 181 | defmodule Sample do 182 | defmacro initialize_to_char_count(variables) do 183 | Enum.map variables, fn(name) -> 184 | var = Macro.var(name, nil) 185 | length = name |> Atom.to_string |> String.length 186 | quote do 187 | unquote(var) = unquote(length) 188 | end 189 | end 190 | end 191 | 192 | def run do 193 | initialize_to_char_count [:red, :green, :yellow] 194 | [red, green, yellow] 195 | end 196 | end 197 | 198 | > Sample.run #=> [3, 5, 6] 199 | ``` 200 | 201 | 注意`Macro.var/2`的第二个变量.在下一节中我们将知道它是所使用的语境,而且能定义隔离. 202 | 203 | #环境 204 | 205 | 本章早些时候,我们调用`Macro.expand_once/2`时,使用了特殊形式`__ENV__`. 206 | 207 | `__ENV__`返回了一个`Macro.Env`结构的实例,它包含了编译环境的有用信息,包括当前模块,文件和行,所有定义在当前作用域中的变量,还有imports,requires等等. 208 | 209 | ``` 210 | iex> __ENV__.module 211 | nil 212 | iex> __ENV__.file 213 | "iex" 214 | iex> __ENV__.requires 215 | [IEx.Helpers, Kernel, Kernel.Typespec] 216 | iex> require Integer 217 | nil 218 | iex> __ENV__.requires 219 | [IEx.Helpers, Integer, Kernel, Kernel.Typespec] 220 | ``` 221 | 222 | `Macro`模块中的许多函数都期望一个环境.你可以在`Macro`模块中找到关于这些函数,以及在`Macro.Env`的文档中找到关于编译环境的更多信息. 223 | 224 | #私有宏 225 | 226 | Elixir也支持私有宏,使用`defmacrop`来定义.和私有函数一样,这些宏只能在它的定义模块中使用,而且只在编译时. 227 | 228 | 很重要的一点是,宏在使用之前定义.没有在调用一个宏之前定义它,将会在运行时抛出一个错误,因为宏不会被扩展,而且将会被转化成函数调用: 229 | 230 | ``` 231 | iex> defmodule Sample do 232 | ...> def four, do: two + two 233 | ...> defmacrop two, do: 2 234 | ...> end 235 | ** (CompileError) iex:2: function two/0 undefined 236 | ``` 237 | 238 | #负责任地编写宏 239 | 240 | 宏是很强大的结构,Elixir提供了许多机制来确保它们被负责任地使用. 241 | 242 | \- 宏是隔离的: 定义在宏内的变量默认是不会影响用户代码的.而且,宏语境中的函数调用和别名是不会泄露到用户语境中的. 243 | 244 | \- 宏具有词典性质: 不可能全局地注入代码或宏.为了使用宏,你需要明确地`require`或`import`定义了宏的模块. 245 | 246 | \- 宏是明确的: 宏不可能在没有明确被导入的情况下运行.例如,一些语言允许开发者在内部完全重写函数,通常是通过语义转换或一些反射机制.在Elixir中,编译时,宏必须在调用者中被明确导入. 247 | 248 | \- 宏的语言是清晰的: 许多语言为`quote`和`unquote`提供了语法捷径.在Elixir中,我们更愿意它们被明确地拼写出来,以便清楚地划出宏定义与它的引用表达式间的界限. 249 | 250 | 即使有这些保障,开发者仍在负责任地编写宏这件事中扮演重要角色.如果你确信你需要使用宏,记住宏不是你的API.你的宏定义要保持简短,包括它们的引用内容.例如,与其像这样编写宏: 251 | 252 | ``` 253 | defmodule MyModule do 254 | defmacro my_macro(a, b, c) do 255 | quote do 256 | do_this(unquote(a)) 257 | ... 258 | do_that(unquote(b)) 259 | ... 260 | and_that(unquote(c)) 261 | end 262 | end 263 | end 264 | ``` 265 | 266 | 不如这样: 267 | 268 | ``` 269 | defmodule MyModule do 270 | defmacro my_macro(a, b, c) do 271 | quote do 272 | # Keep what you need to do here to a minimum 273 | # and move everything else to a function 274 | do_this_that_and_that(unquote(a), unquote(b), unquote(c)) 275 | end 276 | end 277 | 278 | def do_this_that_and_that(a, b, c) do 279 | do_this(a) 280 | ... 281 | do_that(b) 282 | ... 283 | and_that(c) 284 | end 285 | end 286 | ``` 287 | 288 | 这使得你的代码更清晰,也更容易测试和维护,因为你可以直接调用和测试`do_this_that_and_that/3`.这也有助于你为那些不愿意依赖宏的开发者来设计一个实际的API. 289 | 290 | 现在,我们结束了对宏的介绍.下一章我们将简短得讨论DSL,展示如何混合宏和模块属性,来注释和扩展模块与函数. 291 | -------------------------------------------------------------------------------- /3_Domain_Specific_Languages.markdown: -------------------------------------------------------------------------------- 1 | #领域特定语言 2 | 3 | 1. 前言 4 | 2. 构建我们的测试案例 5 | 3. `test`宏 6 | 4. 用属性存储信息 7 | 8 | #前言 9 | 10 | 领域特定语言(DSL)允许开发者修改他们的应用以适应特定的领域.制作DSL可以不需要用到宏:你在你的模块中定义的每个数据结构和每个函数都是你的领域特定语言的一部分. 11 | 12 | 比如,假设你想要实现一个提供了数据验证领域特定语言的验证模块.你可以使用数据结构,函数或宏来实现它.让我们来看看这些不同的DSL: 13 | 14 | ``` 15 | # 1. data structures 16 | import Validator 17 | validate user, name: [length: 1..100], 18 | email: [matches: ~r/@/] 19 | 20 | # 2. functions 21 | import Validator 22 | user 23 | |> validate_length(:name, 1..100) 24 | |> validate_matches(:email, ~r/@/) 25 | 26 | # 3. macros + modules 27 | defmodule MyValidator do 28 | use Validator 29 | validate_length :name, 1..100 30 | validate_matches :email, ~r/@/ 31 | end 32 | 33 | MyValidator.validate(user) 34 | ``` 35 | 36 | 上述方法中,第一个是最有弹性的.如果我们的领域规则可以使用数据结构来编码,它们组成和实现起来会非常简单,因为Elixir的标准库中有很多用于操作不同数据类型的函数. 37 | 38 | 第二个方法使用函数调用,它能更好地适应更复杂的API(例如,需要传递很多选项)而且归功于Elixir中的管道操作符,它更易读. 39 | 40 | 第三个方法,使用宏,无疑是最复杂的.需要更多行的代码来实现,测试它是很难且很昂贵的(与测试简单的函数相比),而且它也限制了用户使用库的方法,因为所有验证需要在一个模块中定义. 41 | 42 | 为了更好地理解,假设你想要只在某种情况下验证某个特定属性.我们可以简单地使用第一个方法操作相应的数据结构来实现,或者使用第二个方法在调用函数前使用条件语句(if/else).然而不可能使用宏来完成,除非该DSL已经增强过了. 43 | 44 | 也就是说: 45 | 46 | ``` 47 | data > functions > macros 48 | ``` 49 | 50 | 仍然有一些时候,由宏和模块来构建领域特定语言是很管用的.因为我们已经在入门教程中探索过了数据结构和函数定义,本章将会探索如何使用宏和模块属性来处理更复杂的DSL. 51 | 52 | #构建我们自己的测试案例 53 | 54 | 本章的目标是构建一个名为`TestCase`的模块,它允许我们这样编写: 55 | 56 | ``` 57 | defmodule MyTest do 58 | use TestCase 59 | 60 | test "arithmetic operations" do 61 | 4 = 2 + 2 62 | end 63 | 64 | test "list operations" do 65 | [1, 2, 3] = [1, 2] ++ [3] 66 | end 67 | end 68 | 69 | MyTest.run 70 | ``` 71 | 72 | 在上面的例子中,使用`TestCase`,我们可以用`test`宏来编写测试,它定义了一个名为`run`的函数来自动为我们运行所有测试.我们的原型将简单地依赖匹配操作符(`=`)作为一个断言机制. 73 | 74 | #`test`宏 75 | 76 | 让我们创建一个模块,它在被使用时简单地定义并进口了`test`宏: 77 | 78 | ``` 79 | defmodule TestCase do 80 | # Callback invoked by `use`. 81 | # 82 | # For now it simply returns a quoted expression that 83 | # imports the module itself into the user code. 84 | @doc false 85 | defmacro __using__(_opts) do 86 | quote do 87 | import TestCase 88 | end 89 | end 90 | 91 | @doc """ 92 | Defines a test case with the given description. 93 | 94 | ## Examples 95 | 96 | test "arithmetic operations" do 97 | 4 = 2 + 2 98 | end 99 | 100 | """ 101 | defmacro test(description, do: block) do 102 | function_name = String.to_atom("test " <> description) 103 | quote do 104 | def unquote(function_name)(), do: unquote(block) 105 | end 106 | end 107 | end 108 | ``` 109 | 110 | 假设我们在一个名为`tests.exs`的文件中定义了`TestCase`,我们可以通过运行`iex tests.exs`打开它,并定义我们的第一个测试: 111 | 112 | ``` 113 | iex> defmodule MyTest do 114 | ...> use TestCase 115 | ...> 116 | ...> test "hello" do 117 | ...> "hello" = "world" 118 | ...> end 119 | ...> end 120 | ``` 121 | 122 | 现在我们还没有一个运行测试的机制,但是我们知道有一个名为`test hello`的函数已经在幕后被定义了.当调用它时,他应该会失败: 123 | 124 | ``` 125 | iex> MyTest."test hello"() 126 | ** (MatchError) no match of right hand side value: "world" 127 | ``` 128 | 129 | #使用属性存储信息 130 | 131 | 为了完整实现我们的`TestCase`,我们需要能够访问所有已经定义的测试案例.我们可以通过在运行时使用`__MODULE__.__info__(:functions)`来检索测试,它会返回一个包含给定模块中所有函数的列表.然而,考虑到我们可能需要存储更多关于每个测试的信息,这就要求要有一个更灵活地方法. 132 | 133 | 当在前几章中讨论模块属性时,我们提到了它们是如何被用作临时存储的.那就是我们在本节中将应用的特性. 134 | 135 | 在`__using__/1`的实现中,我们将会把一个名为`@tests`的模块属性初始化成一个空列表,然后存储每个已定义测试的名字到该属性中,这样测试就可以从`run`函数调用. 136 | 137 | 这是`TestCase`模块更新后的代码: 138 | 139 | ``` 140 | defmodule TestCase do 141 | @doc false 142 | defmacro __using__(_opts) do 143 | quote do 144 | import TestCase 145 | 146 | # Initialize @tests to an empty list 147 | @tests [] 148 | 149 | # Invoke TestCase.__before_compile__/1 before the module is compiled 150 | @before_compile TestCase 151 | end 152 | end 153 | 154 | @doc """ 155 | Defines a test case with the given description. 156 | 157 | ## Examples 158 | 159 | test "arithmetic operations" do 160 | 4 = 2 + 2 161 | end 162 | 163 | """ 164 | defmacro test(description, do: block) do 165 | function_name = String.to_atom("test " <> description) 166 | quote do 167 | # Prepend the newly defined test to the list of tests 168 | @tests [unquote(function_name) | @tests] 169 | def unquote(function_name)(), do: unquote(block) 170 | end 171 | end 172 | 173 | # This will be invoked right before the target module is compiled 174 | # giving us the perfect opportunity to inject the `run/0` function 175 | @doc false 176 | defmacro __before_compile__(env) do 177 | quote do 178 | def run do 179 | Enum.each @tests, fn name -> 180 | IO.puts "Running #{name}" 181 | apply(__MODULE__, name, []) 182 | end 183 | end 184 | end 185 | end 186 | end 187 | ``` 188 | 189 | 通过启动一个新的IEx会话,我们现在可以定义并运行我们的测试: 190 | 191 | ``` 192 | iex> defmodule MyTest do 193 | ...> use TestCase 194 | ...> 195 | ...> test "hello" do 196 | ...> "hello" = "world" 197 | ...> end 198 | ...> end 199 | iex> MyTest.run 200 | Running test hello 201 | ** (MatchError) no match of right hand side value: "world" 202 | ``` 203 | 204 | 尽管我们跳过了一些细节,但这就是在Elixir中创建领域特定模块的主要思想.宏使得我们能够返回在调用者中执行了的引用表达式,我们可以用它来变形代码并通过模块属性来在目标模块中存储相关信息.最后,`@before_compile`这样的回调能让我们将代码注入到已定义完成的模块中. 205 | 206 | 除了`@before_compile`,还有其它有用的模块属性,例如`@on_definition`和`@after_compile`,你可以在`Module`模块的文档中获取有关它们的信息.你也可以在`Macro`模块和`Macro.Env`的文档中找到关于宏和编译环境的有用信息. 207 | --------------------------------------------------------------------------------