注:本章内容来自 Metaprogramming Elixir 一书,写的很是好,强烈推荐。内容不是原文照翻,部分文字采起意译,主要内容都基本保留,加上本身的一些理解描述。为更好理解,建议参考原文。javascript
Elixir中如何处理Unicodehtml
UnicodeData.txt 文件包含了27000行描述Unicode代码的映射,内容以下:java
00C7;LATIN CAPITAL LETTER C WITH CEDILLA;Lu;0;L;0043 0327;... 00C8;LATIN CAPITAL LETTER E WITH GRAVE;Lu;0;L;0045 0300;... 00C9;LATIN CAPITAL LETTER E WITH ACUTE;Lu;0;L;0045 0301;... 00CA;LATIN CAPITAL LETTER E WITH CIRCUMFLEX;Lu;0;L;0045 0302;... 00CB;LATIN CAPITAL LETTER E WITH DIAERESIS;Lu;0;L;0045 0308;... ...
String.Unicode模块在编译时读取这个文件,将每一行描述转化成函数定义。最终将包含全部的大小写转换以及其余一些字符转换函数。git
咱们看下这个模块是如何处理大写转换的:github
defmodule String.Unicode do ... def upcase(string), do: do_upcase(string) |> IO.iodata_to_binary ... defp do_upcase("é" <> rest) do :binary.bin_to_list("É") ++ do_upcase(rest) end defp do_upcase(" ć " <> rest) do :binary.bin_to_list(" Ć ") ++ do_upcase(rest) end defp do_upcase("ü" <> rest) do :binary.bin_to_list("Ü") ++ do_upcase(rest) end ... defp do_upcase(char <> rest) do :binary.bin_to_list(char) ++ do_upcase(rest) end ... end
编译后的 String.Unicode 模块将包含上万个这种函数定义。 当咱们须要转换 "Thanks José!" 成大写时,经过模式匹配,调用函数 do_update("T" <> rest),而后递归调用 do_upcase(rest),直到最后一个字符。 整个架构简单干脆,咱们接下来看看将这种技术用于 MIME 类型解析。web
在 web 程序中,咱们常常须要校验和转换 MIME 类型,而后将其对应到合适的文件扩展名。好比当咱们请求 application/javascript 时,咱们须要知道如何处理这种 MIME 类型,而后正确的渲染 .js 模板。大多数语言中咱们采用的方案是存储全部的 MIME 数据映射,而后构建一个 MIME-type 转换的关键字存储。面对庞大的数据类型,咱们不得不手工编写代码进行格式,这项工做索然乏味。在 Elixir 中这可简单多了。正则表达式
咱们采用的方法很是简洁。咱们获取公开的 MIME-type 数据集,而后自动生成转换函数。全部代码仅需十多行,代码快速易维护。express
咱们首先获取一份 MIMIE-type 的数据集,在网上很容易找到。内容以下:编程
文件:advanced_code_gen/mimes.txt --------------------------- application/javascript .js application/json .json image/jpeg .jpeg, .jpg video/jpeg .jpgv
完整的 mimes.txt 包含 685 行,内容都是 MIME 类型到文件名的映射。每一行两个字段,中间用 tab 分隔,若是对应多个扩展名,则再用逗号分隔。咱们先建立一个 Mime 模块。json
文件:advanced_code_gen/mime.exs -------------------------------- defmodule Mime do for line <- File.stream!(Path.join([__DIR__, "mimes.txt"]), [], :line) do [type, rest] = line |> String.split("\t") |> Enum.map(&String.strip(&1)) extensions = String.split(rest, ~r/,\s?/) def exts_from_type(unquote(type)), do: unquote(extensions) def type_from_ext(ext) when ext in unquote(extensions), do: unquote(type) end def exts_from_type(_type), do: [] def type_from_ext(_ext), do: nil def valid_type?(type), do: exts_from_type(type) |> Enum.any? end
就经过这十多行代码,咱们建立了一个完整的 MIME-type 转换跟校验模块。
首先逐行读取 mimes.txt 文件,将每行拆分红两部分,对于扩展名再用逗号继续拆分。而后基于解析定义两个函数,一个用于从 MIME 类型到扩展名映射,一个用于从扩展名到 MIME 类型映射。咱们使用标准的 def 宏定义函数,使用 unquote 注入 MIME 跟扩展名。最后,咱们还须要定义能捕获全部参数的两个函数 exts_from_type 、type_from_ext,这两个函数是最后的守门员,用来捕捉漏网之鱼。最后还有一个 valid_type? 函数,他简单的利用前面定义的函数进行校验。下面在 iex 测试一下:
iex> c "mime.exs" Line 1 [Mime] iex> Mime.exts_from_type("image/jpeg") [".jpeg", ".jpg"] iex> Mime.type_from_ext(".jpg") "image/jpeg" iex> Mime.valid_type?("text/html") true iex> Mime.valid_type?("text/emoji") false
功能完美。代码中须要注意的一点,你可能会很诧异咱们为何能在 quote 代码块外面调用 unquote。Elixir 支持 unquote 片断,使用 unquote 片断咱们能够动态地定义函数,正如前面的代码所示:
def exts_from_type(unquote(type)), do: unquote(extensions) def type_from_ext(ext) when ext in unquote(extensions), do: unquote(type)
咱们使用 unquote 片断定义了 exts_from_type 和 type_from_ext 函数的多个子句,咱们还能够动态定义函数名哦。示例以下:
iex> defmodule Fragments do ...> for {name, val} <- [one: 1, two: 2, three: 3] do ...> def unquote(name)(), do: unquote(val) ...> end ...> end {:module, Fragments, ... iex> Fragments.one 1 iex> Fragments.two 2
使用 unquote 片断,咱们能够将任意 atom 传给 def,用它作函数名来动态的定义一个函数。在本章余下的内容中,咱们还会大量的使用 unquote 片断。
不少美妙的解决方案每每让你一眼看不穿。使用大量的小块代码,咱们能够更快速地构建任意 web 服务,代码维护性还很好。如要要增长更多的 MIME-type ,咱们只须要简单的编辑一下 mimes.txt 文件。代码越短,bug越少。定义多个函数头,把繁重的工做丢给 VM 去匹配解析 ,咱们偷着乐吧。
接下来,咱们经过构建一个国际化语言的 lib 来进一步学习。 首先咱们先交代一下另一个背景知识。
咱们的 Mime 模块工做地很好,可是一旦 mimes.txt 文件发生变化,咱们的模块是不会经过 mix 自动编译的。这是由于程序源码并无发生变化。Elixir 提供了一个模块属性 @external_resource 用来处理这种状况,一旦资源变化,模块将自动编译。咱们在 Mime 里面注册一个 @external_resource:
文件: advanced_code_gen/external_resource.exs --------------------------------------- defmodule Mime do @external_resource mimes_path = Path.join([__DIR__, "mimes.txt"]) for line <- File.stream!(mimes_path, [], :line) do end
如今只要 mimes.txt 修改了,mix 会自动从新编译 Mime 模块。@external_resource 是一个累加属性(accumulated attribute),它会把屡次调用的参数不断累积,都汇聚到一块儿。若是你的代码须要依赖一个非代码的资源文件,就在模块的body内使用他。这样一旦有须要代码就会自动从新编译,这会帮上大忙,节约咱们不少的时间。
几乎全部用户友好的程序都须要支持语言国际化,为世界上不一样国家的人提供不一样的语言界面。让咱们用比你想象中少的多的代码来实现这个功能。
咱们要构建一个 Translator 程序,咱们先琢磨下怎么设计 macro 的接口API。咱们称之为说明书驱动开发。其实这有利于咱们梳理目标,规划 macro 的实现。咱们的目标是实现以下 API。文件存为 i18n.exs
文件:advanced_code_gen/i18n.exs ------------------------------ defmodule I18n do use Translator locale "en", flash: [ hello: "Hello %{first} %{last}!", bye: "Bye, %{name}!" ], users: [ title: "Users", ] locale "fr", flash: [ hello: "Salut %{first} %{last}!", bye: "Au revoir, %{name}!" ], users: [ title: "Utilisateurs", ] end
最终代码调用格式以下:
iex> I18n.t("en", "flash.hello", first: "Chris", last: "McCord") "Hello Chris Mccord!" iex> I18n.t("fr", "flash.hello", first: "Chris", last: "McCord") "Salut Chris McCord!" iex> I18n.t("en", "users.title") "Users"
任何模块只要 use Translator
后,就能够包含一个翻译字典和一个 t/3 函数。那么咱们确定须要定义一个 __using__
宏,用来 import 模块,以及包含一些属性,而后还须要一个 locale 宏用来处理 locale 注册。回到键盘上,让咱们开干。
实现一个 Translator 骨架,须要定义 using,before_compile,和 locale 宏。这个库只是简单地设置编译时 hooks 以及注册模块属性,至于生成代码部分稍后再作。首先定义一个元编程骨架是一种很好的思惟模式,咱们先把复杂的代码生成丢到脑后,单纯地来思考模块组织。这有利于咱们保持代码的清晰和可复用。
建立 translator.exs 文件,加入骨架 API:
advanced_code_gen/translator_step2.exs ---------------------------------------- defmodule Translator do defmacro __using__(_options) do quote do Module.register_attribute __MODULE__, :locales, accumulate: true, persist: false import unquote(__MODULE__), only: [locale: 2] @before_compile unquote(__MODULE__) end end defmacro __before_compile__(env) do compile(Module.get_attribute(env.module, :locales)) end defmacro locale(name, mappings) do quote bind_quoted: [ name: name, mappings: mappings ] do @locales {name, mappings} end end def compile(translations) do # TBD: Return AST for all translation function definitions end end
在前面几章的 Assertion 模块中咱们定义过一个累加属性 @tests,这里咱们也定义了一个累加属性 @locales。而后咱们在 Translator.using 宏中激活 before_compile hook。这里我暂时放个 compile 空函数占位,此函数将根据 locale 注册信息进行代码生成,内容随后填充。最后咱们再定义 locale 宏,用来注册locale ,以及文本翻译对照表,这些东西随后在 before_compile hook 调用的compile 中会用到。
咱们注册的累加属性激活后,咱们就有了足够的信息来生成 t/3 函数的 AST 结构。若是你喜欢递归,哈哈正逢其时。不喜欢,那就要下点功夫,咱们细细讲。
咱们开始填充 compile 函数,实现将 locale 注册信息转换成函数定义。咱们最终要实现将全部的翻译条目映射到一堆庞大的 t/3 函数的 AST 中。咱们须要添加一个 catch-all 子句做为守夜人,用来处理全部未被翻译收纳的内容,它将返回 {:error,:no_translation}。
修改 compile/1 函数内容以下:
advanced_code_gen/translator_step3.exs ---------------------------------------- def compile(translations) do translations_ast = for {locale, mappings} <- translations do deftranslations(locale, "", mappings) end quote do def t(locale, path, bindings \\ []) unquote(translations_ast) def t(_locale, _path, _bindings), do: {:error, :no_translation} end end defp deftranslations(locales, current_path, mappings) do # TBD: Return an AST of the t/3 function defs for the given locale end
compile 函数用来处理 locale 代码生成。使用 for 语句读取 locale,生成函数的 AST 定义,结果存入 translations_ast,此参数随后用于代码注入。这里咱们先放个 deftranslations 占位,此函数用来实现 t/3 函数的定义。最后6-10行结合 translations_ast 参数,为调用者 生成AST,以及定义一个 catch-all 函数。
在最终实现 deftranslations 前,咱们在iex 查看下:
iex> c "translator.exs" [Translator] iex> c "i18n.exs" [I18n] iex> I18n.t("en", "flash.hello", first: "Chris", last: "McCord") {:error, :no_translation} iex> I18n.t("en", "flash.hello") {:error, :no_translation}
一切都如预期。任何 I18n.t 的调用都将返回 {:error, :no_translation},由于咱们如今尚未为 locale 生成对应函数。咱们只是验证了 catch-all t/3 定义是工做正常的。让咱们开始实现 deftranslations ,递归遍历 locales,而后生成翻译函数。
修改 deftranslations 以下:
advanced_code_gen/translator_step4.exs
defp deftranslations(locale, current_path, mappings) do for {key, val} <- mappings do path = append_path(current_path, key) if Keyword.keyword?(val) do deftranslations(locale, path, val) else quote do def t(unquote(locale), unquote(path), bindings) do unquote(interpolate(val)) end end end end end defp interpolate(string) do string # TBD interpolate bindings within string end defp append_path("", next), do: to_string(next) defp append_path(current, next), do: "#{current}.#{next}"
咱们首先将 mappings 中的键值对取出,而后检查 value 是否是一个 keyword list。由于咱们的翻译内容多是一个嵌套的列表结构,正如咱们在以前原始的高阶 API 设计中所见。
flash: [ hello: "Hello %{first} %{last}!", bye: "Bye, %{name}!" ],
关键字 :flash 指向一个嵌套的 keyword list。处理办法,咱们将 "flash" 追加到累加变量 current_path 里面,这个变量在最后两行的 append_path 辅助函数中会用到。而后咱们继续递归调用 deftranslations, 直到最终解析到一个字符串文本。咱们使用 quote 为每个字符串生成 t/3 函数定义,而后使用 unquote 将对应的 current_path(好比"flash.hello") 注入到函数子句中。t/3 函数体调用一个占位函数 interpolate,这个函数随后实现。
代码只有寥寥数行,不过递归部分略微烧脑。咱们能够在 iex 里作下调试:
iex> c "translator.exs" [Translator] iex> c "i18n.exs" [I18n] iex> I18n.t("en", "flash.hello", first: "Chris", last: "McCord") "Hello %{first} %{last}!"
咱们确实作到了。咱们的 t/3 函数正确生成了,咱们只不过作了些简单的变量插值就完成了这个库。你可能会琢磨咱们程序生成的代码又如何跟踪呢,不用担忧,Elixir为咱们想好了。当你生成了一大堆代码时,通常来讲咱们只须要关心最终生成的代码,咱们可使用 Macro.to_string。
Macro.to_string 读取 AST,而后生成 Elixir 源码文本。这个工具在调试生成的 AST 时很是的强大,尤为在建立大批量的函数头时很是有用,就像咱们上面的 Translator 模块。让咱们观察一下 compile 函数生成的代码。
修改 Translator 模块:
advanced_code_gen/macro_to_string.exs
def compile(translations) do translations_ast = for {locale, mappings} <- translations do deftranslations(locale, "", mappings) end final_ast = quote do def t(locale, path, binding \\ []) unquote(translations_ast) def t(_locale, _path, _bindings), do: {:error, :no_translation} end IO.puts Macro.to_string(final_ast) final_ast end
第6行,咱们将生成的结果 AST 存入 final_ast 绑定。第12行,使用 Macro.to_string 将 AST 展开成源码文本后输出。最后将 final_ast 返回。启用 iex 调试:
iex> c "translator.exs" [Translator] iex> c "i18n.exs" ( def(t(locale, path, bindings \\ [])) [[[def(t("fr", "flash.hello", bindings)) do "Salut %{first} %{last}!" end, def(t("fr", "flash.bye", bindings)) do "Au revoir, %{name}!" end], [def(t("fr", "users.title", bindings)) do "Utilisateurs" end]], [[def(t("en", "flash.hello", bindings)) do "Hello %{first} %{last}!" end, def(t("en", "flash.bye", bindings)) do "Bye, %{name}!" end], [def(t("en", "users.title", bindings)) do "Users" end]]] def(t(_locale, _path, _bindings)) do {:error, :no_translation} end ) [I18n] iex>
第一眼看上去,返回结果彷佛没啥用,由于 t/3 定义包裹在一个嵌套列表中。咱们看到 def 语句都嵌入在 list 中,由于前面咱们用 for 语句返回了全部的 deftranslations AST。咱们能够 flatten (扁平化)列表,而后将其切片,提取最终的 AST,但对于 Elixir 是无所谓的,所以咱们保持原样,经过 unquote 列表片断引用代码就好了。
在你生成 AST 是最好时不时地使用 Macro.to_string 来调试。你能够看到最终展开的代码如何注入到 caller 中,能够检查生成的参数列表,是否符合模板匹配。固然编写测试代码也是必不可少的。
最后一步工做是,让 Translator 模块实现对占位符进行插值替换,好比 %{name}。固然咱们能够在运行时生成正则表达式进行求值,这里咱们换个思路,尝试一下进行编译时优化。咱们能够生成一个函数定义,用来在进行插值替换时完成字符拼接功能。这样在运行时性能会急剧提高。咱们实现一个 interpolate 函数,它用来生成一段 AST 注入到 t/3中 ,当 t/3 函数须要插值替换时进行引用。
advanced_code_gen/translator_final.exs
defp deftranslations(locale, current_path, mappings) do for {key, val} <- mappings do path = append_path(current_path, key) if Keyword.keyword?(val) do deftranslations(locale, path, val) else quote do def t(unquote(locale), unquote(path), bindings) do unquote(interpolate(val)) end end end end end defp interpolate(string) do ~r/(?<head>)%{[^}]+}(?<tail>)/ |> Regex.split(string, on: [:head, :tail]) |> Enum.reduce "", fn <<"%{" <> rest>>, acc -> key = String.to_atom(String.rstrip(rest, ?})) quote do unquote(acc) <> to_string(Dict.fetch!(bindings, unquote(key))) end segment, acc -> quote do: (unquote(acc) <> unquote(segment)) end end
从16行开始,咱们使用 %{varname} 模板来拆分翻译字符串。%{开头就意味着碰到了一个 segment,咱们搜索字符串,不断的变量替换,不断的缩减引用,最后 Regex.split 被转换成一个简单的字符串拼接的 AST。咱们使用 Dict.fetch!来处理绑定变量,以确保 caller 提供全部的内插值。对于普通字符串部分,咱们就直接将其追加到这个不断累加的 AST 中。咱们使用 Macro.to_string 来调试下:
iex> c "translator.exs" [Translator] iex> c "i18n.exs" ( def(t(locale, path, binding \\ [])) [[[def(t("fr", "flash.hello", bindings)) do (((("" <> "Salut ") <> to_string(Dict.fetch!(bindings, :first))) <> " ") <> to_string(Dict.fetch!(bindings, :last))) <> "!" end, def(t("fr", "flash.bye", bindings)) do (("" <> "Au revoir, ") <> to_string(Dict.fetch!(bindings, :name))) <> "!" end], [def(t("fr", "users.title", bindings)) do "" <> "Utilisateurs" end]], [[def(t("en", "flash.hello", bindings)) do (((("" <> "Hello ") <> to_string(Dict.fetch!(bindings, :first))) <> " ") <> to_string(Dict.fetch!(bindings, :last))) <> "!" end, def(t("en", "flash.bye", bindings)) do (("" <> "Bye, ") <> to_string(Dict.fetch!(bindings, :name))) <> "!" end], [def(t("en", "users.title", bindings)) do "" <> "Users" end]]] def(t(_locale, _path, _bindings)) do {:error, :no_translation} end ) [I18n] iex> I18n.t("en", "flash.hello", first: "Chris", last: "McCord") "Hello Chris Mccord!" iex> I18n.t("fr", "flash.hello", first: "Chris", last: "McCord") "Salut Chris McCord!" iex> I18n.t("en", "users.title") "Users"
Macro.to_string 观察了编译时优化的 t/3 函数内部。咱们看到全部的内插 AST 都正确的展开成简单的字符串拼接操做。这种方式的性能优化是绝大多数语言作不到的,相比于运行时的正则表达式匹配,性能提高那是至关大的。
你可能会好奇咱们是如何在不使用 var! 宏的状况下直接在插值时引用绑定变量的。这里咱们彻底不用考虑宏卫生的问题,由于全部 quote block 都是位于同一个模块当中,所以他们共享同一个个上下文。让咱们暂时存疑,好好欣赏下咱们完成的工做吧。
让我好好看看完整版本的程序,看下各部分是如何有机地结合在一块儿的。浏览代码时,思考下元编程中的每一步决定,咱们是如何让说明文档驱动咱们的实现的。
defmodule Translator do defmacro __using__(_options) do quote do Module.register_attribute __MODULE__, :locales, accumulate: true, persist: false import unquote(__MODULE__), only: [locale: 2] @before_compile unquote(__MODULE__) end end defmacro __before_compile__(env) do compile(Module.get_attribute(env.module, :locales)) end defmacro locale(name, mappings) do quote bind_quoted: [name: name, mappings: mappings] do @locales {name, mappings} end end def compile(translations) do translations_ast = for {locale, source} <- translations do deftranslations(locale, "", source) end quote do def t(locale, path, binding \\ []) unquote(translations_ast) def t(_locale, _path, _bindings), do: {:error, :no_translation} end end defp deftranslations(locale, current_path, translations) do for {key, val} <- translations do path = append_path(current_path, key) if Keyword.keyword?(val) do deftranslations(locale, path, val) else quote do def t(unquote(locale), unquote(path), bindings) do unquote(interpolate(val)) end end end end end defp interpolate(string) do ~r/(?<head>)%{[^}]+}(?<tail>)/ |> Regex.split(string, on: [:head, :tail]) |> Enum.reduce "", fn <<"%{" <> rest>>, acc -> key = String.to_atom(String.rstrip(rest, ?})) quote do unquote(acc) <> to_string(Dict.fetch!(bindings, unquote(key))) end segment, acc -> quote do: (unquote(acc) <> unquote(segment)) end end defp append_path("", next), do: to_string(next) defp append_path(current, next), do: "#{current}.#{next}" end
仅65行代码,咱们就编写了一个鲁棒性很是强的国际化语言库,并且作了编译时性能优化。为每个翻译条目映射生成函数头,也确保了 VM 可以快速检索。有了更多的翻译内容,也只须要简单的更新 locales就好了。
经过这一系列的联系你的元编程技能有进阶了,Elixir武器库又多了几样宝贝。如今让咱们尝试在真实生产环境下探索 Elixir 的扩展性。前面咱们没有限制是根据纯文本仍是 Elixir 数据结构来构建代码。让咱们建立一个 Hub mix project,经过 GitHub 的公开 API 来定义咱们的模块功能。咱们会生成一个模块,包含咱们的 public repositories 的嵌入信息,要可以函数调用启动一个 web 浏览器直接跳转到咱们的 project。
建立一个工程项目
$ mix new hub --bare $ cd hub
添加 Poison 和 HTTPotion 到项目依赖,一个用于 JSON 编码,一个用于处理 HTTP 请求。
编辑 hub/mix.exs
defmodule Hub.Mixfile do use Mix.Project def project do [app: :hub, version: "0.0.1", elixir: "~> 1.0.0", deps: deps] end def application do [applications: [:logger]] end defp deps do [{:ibrowse, github: "cmullaparthi/ibrowse", tag: "v4.1.0"}, {:poison, "~> 1.3.0"}, {:httpotion, "~> 1.0.0"}] end end
下载依赖包
$ mix deps.get
编辑主模块 hub.ex ,从远程 API 生成代码。咱们会访问 GitHub 的公开 API,提取咱们 GitHub 帐号下全部的 repositories,而后将返回的 JSON 数据中的 body 解码后存入一个 Elixir map。而后基于每条结果记录生成一个函数,函数名就是 repository 名,函数体就是该 repository 下的 GitHub proects 的全部相关数据。最后定义一个 go 函数,接受 repository name 做为参数,启动一个 web 浏览器跳转到该 URL。
编辑 lib/hub.ex 文件,输入下列代码。若是你有本身的 GitHub 帐号,那么把 "chrismccord" 改为你本身的帐号。
defmodule Hub do HTTPotion.start @username "chrismccord" "https://api.github.com/users/#{@username}/repos" |> HTTPotion.get(["User-Agent": "Elixir"]) |> Map.get(:body) |> Poison.decode! |> Enum.each fn repo -> def unquote(String.to_atom(repo["name"]))() do unquote(Macro.escape(repo)) end end def go(repo) do url = apply(__MODULE__, repo, [])["html_url"] IO.puts "Launching browser to #{url}..." System.cmd("open", [url]) end end
在第5行,咱们使用管道用来将 JSON URL 转化成一系列的函数定义。咱们获取原始的 response body,解码成 JSON,而后将每个 JSON repository 映射成函数定义。基于每一个 repository 生成一个函数,函数名就是 repo name;函数体只是简单的包含 repo 信息。第15行,定义了一个 go 函数,能够快速启动一个浏览器,跳转到给定 repository 的 URL。在 iex 测试下:
$ iex -S mix iex> Hub. atlas/0 bclose.vim/0 calliope/0 chrismccord.com/0 dot_vim/0 elixir/0 elixir_express/0 ex_copter/0 genserver_stack_example/0 gitit/0 go/1 haml-coffee/0 historian/0 jazz/0 jellybeans.vim/0 labrador/0 linguist/0 phoenix_chat_example/0 plug/0 phoenix_haml/0 phoenix_render_example/0 phoenix_vs_rails_showdown/0 iex > Hub.linguist %{"description" => "Elixir Internationalization library", "full_name" => "chrismccord/linguist", "git_url" => "git://github.com/chrismccord/linguist.git", "open_issues" => 4, "open_issues_count" => 4, "pushed_at" => "2014-08-04T13:28:30Z", "watchers" => 33, ... } iex> Hub.linguist["description"] "Elixir Internationalization library" iex> Hub.linguist["watchers"] 33 iex> Hub.go :linguist Launching browser to https://github.com/chrismccord/linguist...
仅20行代码,让咱们陶醉下。咱们在互联网上发出一个 JSON API 调用,而后直接将返回数据转换成模块函数。只有模块编译时产生了一次 API 调用。在运行时,咱们至关于已经直接将 GitHub 数据缓存为函数调用了。这个例子只是为了好玩,它向咱们展现了 Elixir 是如何的易于扩展。这里咱们第一次接触了 Macro.escape 。
Macro.escape 用来将一个 Elixir 字面量递归地(由于有嵌套的数据结构)转义成 AST 表达式(译注:由于 Elixir 的字面语法并不是是 AST语法,因此须要转义。彷佛只有 Lisp 系列语言才是直接操纵 AST 的)。
它主要用在当你须要将一个 Elixir value(而这个 value 是 Elixir 字面量语法,不是 AST 字面量语法) 插入到一个已经 quoted 的表达式中。
对于 Hub 模块,咱们须要将 JSON map 注入到函数体重,可是 def 宏已经 quote 了接收到的代码块(译注:def 是个宏,所以其参数会自动 quoted,而 def func,do: block 的格式中,block 不过是个参数而已)。所以咱们须要对 repo escape 转义,而后在 quoted block 中,才能经过 unquote 对其引用。
在 iex 中咱们作些演示:
iex> Macro.escape(123) 123 iex> Macro.escape([1, 2, 3]) [1, 2, 3] # %{watchers: 33, name: "linguist"} 是Elixir字面量表示 # {:%{}, [], [name: "linguist", watchers: 33]}是 AST 字面量表示 iex> Macro.escape(%{watchers: 33, name: "linguist"}) {:%{}, [], [name: "linguist", watchers: 33]} iex> defmodule MyModule do ...> map = %{name: "Elixir"} # map 是 Elixir 字面量 ...> def value do ...> unquote(map) # 因此这里引用 Elixir 字面量是有问题的 ...> end ...> end ** (CompileError) iex: invalid quoted expression: %{name: "Elixir"} iex> defmodule MyModule do ...> map = Macro.escape %{name: "Elixir"} # 转换成 AST 字面量 ...> def value do ...> unquote(map) ...> end ...> end {:module, MyModule, ...} iex> MyModule.value %{name: "Elixir"}
在这个 MyModule 例子当中,CompileError 报错是由于 map 不是一个 quoted 过的表达式。咱们使用 Macro.escape 将其转义为一个可注入的 AST,就解决问题了。不管什么时候只要你遇到一个 invalid quoted expression 错误,停下来好好想想,你是要把 values 注入到一个 quoted 表达式。若是表达式已经 quoted 成了一个 AST,那你就须要 Macro.escape 了。
咱们已经把生成代码带到了一个新高度,咱们的代码高度可维护,性能也很棒,彻底能够用于生产服务中。咱们已经见识过了高阶代码生成技术的优势,也感觉到了从远程 API 派生代码的乐趣。若是你试图探索这些技术的更多可能性,之后再说吧。咱们这先思考下这些问题,扩充下你的脑洞。
defmodule MimeMapper do use Mime, "text/emoji": [".emj"], "text/elixir": [".exs"] end iex> MimeMapper.exts_for_type("text/elixir") [".exs"] iex> MimeMapper.exts_for_type("text/html") [".html"]
iex> I18n.t("en", "title.users", count: 1) "user" iex> I18n.t("en", "title.users", count: 2) "users"