/var/log/messages

Feb 18, 2019 - 4 minute read - Comments - programming

Metaprogramming Elixir (7)

Advanced Compile-Time Code Generation の Chapter 3 です。最初に出てくるのは UnicodeData.txt を例にした外部データがコード生成手続きを呼び出す例。

一つには以下な情報 (一部のみ) を行単位で読みこんで

application/javascript  .js
application/json        .json
image/jpeg              .jpeg, .jpg
video/jpeg              .jpgv

リストにしてそれに手続きを割り当てる、という事をしてます。これはこれで便利。そしてこのテキストファイルを更新した時点でコンパイルも動かしてしまえ、という属性があるとのこと。これは凄い。

とりあえず

例示されている 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), 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

そういえば

昨日、作業を終えてからの昼呑み帰りにふと思い出したのが

  • quote はソースコードから AST への変換
  • unquote はその逆 (AST からソースコードへの変換)

というナニでした。

Recompiling Modules when External Resources Change

@external_resource という module attribute を使う、とのことです。以下なカンジで書きなさい、とのことです (未実装)。

defmodule Mime do
  @external_resource mimes_path = Path.join([__DIR__, "mimes.txt"])
  for line <- File.stream!(mimes_path, [], :line) do

これはこれでなかなか。そしてここまではオマケ的な話で以降が本題らしく。

Building an Internationalization Library

この節、ステップごとのコードを引用してみようと思います。まずは README Driven Development ということで以下なガワがでっちあげられています。

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 macro を定義しなければなりません
  • locale マクロで登録がおこなえる

ようになっていなければ、なのかどうか。

Step 2

標題が Implement a Skelton Module with Metaprogramming Hooks とのこと。Translator の skelton を作りましょう、なのかな。

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
  • locales という attribute を定義している
  • before_compile macro が定義されている (locales という attribute を compile に渡している)
  • locale という macro の定義 (attribute に引数を設定?)
  • compile は未実装

次は t/3 を定義、なのかどうか。

step3

Generate Code from Your Accumulated Module Attributes という題になっています。t/3 手続きの large AST に translations を map するってどゆ意味なのか。とりあえず例示されている以下を実装しています。

  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

それにしてもこの記述、凄いな。iex でコンパイルして実行してみました。テキストと同じ挙動です。

iex(3)> c "translator_step3.exs"
warning: redefining module Translator (current version defined in memory)
  translator_step3.exs:1

warning: variable "current_path" is unused
  translator_step3.exs:31

warning: variable "locales" is unused
  translator_step3.exs:31

warning: variable "mappings" is unused
  translator_step3.exs:31

[Translator]
iex(4)> c "i18n.exs"            
warning: redefining module I18n (current version defined in memory)
  i18n.exs:1

[I18n]
iex(5)> I18n.t("en", "flash.hello", first: "Chris", last: "McCord")
{:error, :no_translation}
iex(6)> I18n.t("en", "flash.hello")
{:error, :no_translation}
  • まず Translator というモジュールがコンパイルされ
  • i18n.exs コンパイル時にまず using なマクロが展開
  • locales という属性と locale というマクロが定義される
  • 翻訳完了時に before_compile 展開してね状態になる
  • locale マクロで @locales に “en” とか “fr” が設定される (accumulate: true だとリストになるのかどうか、別途確認)
  • 翻訳終了したら before_compile が展開
  • compile が呼び出される
  • @locales の要素ごとに deftranslations が呼び出されて translation_ast に戻りを格納 (なので translation_ast もリスト、という認識で良いのかどうか)
  • リストの要素ごとに quote されたナニが設定 (ということで良いのかどうか)

なんですが、この時点の実装では t/3 は {:error, :no_translation} を戻すのみ、になっていますね。

step 4

deftranslations を実装、とあります。以下らしい。

  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}"

ええと、compile の始点? が以下な形になっているので

  def compile(translations) do
    translations_ast = for {locale, mappings} <- translations do
      deftranslations(locale, "", mappings)
    end

current_path は最初は “” という状態のはず。mappings は

  flash: [hello: "Hello %{first} %{last}!", bye: "Bye, %{name}!"],
  users: [title: "Users",]

なので当初の、という意味では

  • key : flash:
  • val : [hello: “Hello %{first} %{last}!”, bye: “Bye, %{name}!”]

になるはず。で、val は Keyword なので再帰しますね。

      if Keyword.keyword?(val) do
        deftranslations(locale, path, val)

この時点で

  • key : hello:
  • val : “Hello %{first} %{last}!”

になるので path (“flash.hello” になる模様) に t が定義される、ということになるのかどうか。これはなかなか凄いな。そして interpolate は val が渡されて val が戻る形になっているので “flash.hello” が渡されると上の val がそのまま戻る形になるんですね。

中断

現時点で書籍の半分くらいまで読めているらしい。ちょっとキリが無い状態になってきてるので一旦離脱するとともにエントリもここで切ります。

Metaprogramming Elixir (6) Metaprogramming Elixir (8)

comments powered by Disqus