注:本章內容來自 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"