エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

黒魔術で Elixir に Ruby を召喚する

まず始めに。

ネタ記事です。

ひさびさのネタ記事です。

ネタプログラミングです。

とはいえ。

黒魔術を使うことはなくても、このようなしくみを知っていると、その知識が役に立つときが来るかもしれません(来ないかもしれません)。

Level 1, Medium: Port.open/2 で Ruby を起動する

まずは定石の範囲で。

Port.open/2 を使って Ruby のプロセスを起動します。

外部プロセスの起動には System.cmd/3 という関数もありますが、 Port.open/2 の方が制御を細かくできるのでこちらを利用しすることにします。 ちなみに System.cmd/3 も内部では Port.open/2 を利用していました。

path = System.find_executable("ruby")
port = Port.open({:spawn_executable, path},
         [:binary, args: ["-e", "puts('Hello')"]])

receive do
  {^port, {:data, data}} -> data
end
# => "Hello\n"

Port.open/2 でプロセスを起動すると、そのプロセスの出力をメッセージで送信します。 メッセージはポートの値とデータのタプル、データは :data とデータ本体の値のタプル、という形式になっています。

メッセージの形式の詳細はドキュメントを参照してください。

Port.open/2 の戻り値を送り先に指定すると、起動したプロセスにメッセージを送ることができます。

path = System.find_executable("ruby")

# 1 行読み込んで 1 行出力する
port = Port.open({:spawn_executable, path}, [:binary, args: ["-e", ~S|puts("Hello #{gets.strip}!")|]])

# 1 行分の文字列を送る
send(port, {self(), {:command, "world\n"}})

receive do
  {^port, {:data, data}} -> data
end
# => "Hello world!\n"

Level 2, Seer: irb を起動して対話的に式を評価する

ruby コマンドでは、ループ処理を自分で描かない限り、ワンショットの実行になってしまいます。 そこで代わりに irb を起動して一つのプロセスで何度でも式を評価できるようにしてみます。

path = System.find_executable("irb")
port = Port.open({:spawn_executable, path}, [:binary])

ここで。irb を起動しするとどのような挙動になるか IEx で flush/0 を使って確認してみます。

iex> path = System.find_executable("irb")
iex> port = Port.open({:spawn_executable, path}, [:binary])
#Port<0.5>

iex> flush
{#Port<0.5>, {:data, "Switch to inspect mode.\n"}}

起動しただけでなにやらメッセージを受信しています。

メッセージとして Ruby の式を送ってみます。 評価は行単位で行われるので末尾の改行( "\n" )を忘れないようにしてください。

# 式展開で書くと Elixir の文脈で展開してしまうので文字列連結 ++ で書いています
iex> send(port, {self(), {:command, "puts('Hello ' ++ (gets.strip) ++ '!')\n"}})

# gets が読み込む文字列を送る
iex> send(port, {self(), {:command, "world\n"}})
{#PID<0.108.0>, {:command, "world\n"}}

iex> flush
{#Port<0.5>, {:data, "puts('Hello ' ++ (gets.strip) ++ '!')\n"}}
{#Port<0.5>, {:data, "Hello world!\n"}}
{#Port<0.5>, {:data, "nil\n"}}

入力した評価対象の文字列( "puts('Hello ' ++ (gets.strip) ++ '!')\n" )と、puts の出力結果、そして puts の戻り値の値である nil が送り返されました。

出力した内容( "Hello irb!\n" )だけが得られればよいのですが、入力した評価対象の内容も送られてきてしまっています。

明示的に出力した内容以外の出力を抑制するオプションをつけて irb を起動するようにします。

iex> path = System.find_executable("irb")
# 評価する式の出力を抑える --noverbose と 評価した値の出力を抑える --noecho を指定する
iex> port = Port.open({:spawn_executable, path}, [:binary, args: ["--noverbose", "--noecho"]])

iex> send(port, {self(), {:command, "puts('Hello ' ++ (gets.strip) ++ '!')\n"}})
iex> send(port, {self(), {:command, "world\n"}})

iex> flush
{#Port<0.5>, {:data, "Hello world!\n"}}

これで評価する式と評価した値、加えて起動時のメッセージの出力を抑制することができました。

Level 3, Conjuror: サーバにする

これらを踏まえて。 Ruby の式を評価して結果を返すサーバを書いてみます。

defmodule ExIRB do
  use GenServer

  @name __MODULE__

  def start_link(opts) do
    name = Keyword.get(opts, :name, @name)
    GenServer.start_link(__MODULE__, opts, name: name)
  end

  def eva_ruby(server \\ @name, expression) do
    GenServer.call(server, {:eva_ruby, expression})
  end

  def init(_opts) do
    path = System.find_executable("irb")
    port = Port.open({:spawn_executable, path}, [:binary, args: ["--noverbose", "--noecho"]])

    {:ok, %{port: port, froms: []}}
  end

  def handle_call({:eva_ruby, expression}, from, %{port: port, froms: froms} = state) do
    send(port, {self(), {:command, expression}})

    {:noreply, %{state | froms: [from | froms]}}
  end

  def handle_info({port, {:data, data}}, %{port: port, froms: [from | froms]} = state) do
    GenServer.reply(from, data)

    {:noreply, %{state | froms: froms}}
  end
end

実行。

iex> ExIRB.start_link([])
iex> ExIRB.eva_ruby("puts('Hello ExIRB!')\n")
"Hello ExIRB!\n"

ここで注意点として。この実装では式を評価したら必ずメッセージが返されることを期待しています。 値を出力しない式を与えるとタイムアウトが発生します。

値が返ることを期待しない式を評価したい場合は、次のような関数とハンドラを追加することで実現できます。

  def cast_ruby(server \\ @name, expression) do
    GenServer.cast(server, {:cast_ruby, expression})
  end
  def handle_cast({:cast_ruby, expression}, %{port: port} = state) do
    send(port, {self(), {:command, expression}})

    {:noreply, state}
  end
# irb は gets の入力待ちになる
iex> ExIRB.cast_ruby("puts('Hello ' ++ (gets.strip) ++ '!')\n")

# 文字列を送ると gets が値を返し、結果として上の式が評価された値が返される
iex> ExIRB.eva_ruby("world\n")
"Hello world!\n"

ここで puts を使っていると末尾に改行コードが付いてしまいます。 これは print に変更することでこれを回避できます。

iex> ExIRB.eva_ruby("print('Hello ExIRB!')\n")                
"Hello ExIRB!"

Level 4, Magician: マーシャリングした値を利用する

irb のプロセスを起動し評価したい Ruby の式を渡すことで値を得ることができるようになりました。 しかし文字列という形式でしかやりとりができません。 その文字列を Elixir 上でパースすることもできますが面倒です。

シリアライズした Ruby の値を Elixir でデシリアライズできれば文字列をパースするよりもミスなく値を受け渡すことができるはずです。

と、いうわけで。

Ruby の値を Marshal で出力し、それを Elixir で読み込むことにします。

Format of marshaling

フォーマットの仕様はドキュメントにまとめられています。

フォーマットのバージョンは Ruby 1.8.0 以降は 4.8 なので、先頭の 2 バイトは 0x040x08 です。

niltruefalse はそれぞれ "0""T", "F" という形式になるので、マーシャリングされると

Ruby の値 マーシャリングされた値
nil "\x04\x080"
true "\x04\x08T"
false "\x04\x08F"

irb で確認してみます。

irb> Marshal.load("\x04\x080")
=> nil
irb> Marshal.load("\x04\x08T")
=> true
irb> Marshal.load("\x04\x08F")
=> false

Level 3 で作成した ExIRB で同じように評価してみます。

iex> ExIRB.eva_ruby("print(Marshal.dump(nil))\n")
<<4, 8, 48>>
iex> ExIRB.eva_ruby("print(Marshal.dump(true))\n")
<<4, 8, 84>>
iex> ExIRB.eva_ruby("print(Marshal.dump(false))\n")
<<4, 8, 70>>

文字として表示できない値が含まれるためバイト列として表示されます。 フォーマットが規定されているバイト列のパースは文字列のパースよりも容易ですし、なにより Elixir はバイト列のパースが得意です。

そんなわけで。

マーシャリングされた Ruby の値を Elixir の値に変換するパッケージを がっ! と書いてみました。

mix.exsdepsex_marshal を追加します。

# mix.exs

  defp deps do
    [
      {:ex_marshal, github: "mattsan/ex_marshal"}
    ]
  end

iex を起動しなおし、マーシャリングした値を ExMarshal.load/1 で読み込んでみます。

iex> ExIRB.start_link([])
iex> ExIRB.eva_ruby("print(Marshal.dump(nil))\n") |> ExMarshal.load()
nil
iex> ExIRB.eva_ruby("print(Marshal.dump(false))\n") |> ExMarshal.load()
false
iex> ExIRB.eva_ruby("print(Marshal.dump(true))\n") |> ExMarshal.load()
true
iex> ExIRB.eva_ruby("print(Marshal.dump(123))\n") |> ExMarshal.load()
123
iex> ExIRB.eva_ruby("print(Marshal.dump([1,2,3]))\n") |> ExMarshal.load()
[1, 2, 3]
iex> ExIRB.eva_ruby("print(Marshal.dump({a: 1, b: '2', c: :three}))\n") |> ExMarshal.load()
%{a: 1, b: "2", c: :three}

扱える値はドキュメント( lib/ex_marshal.ex の moduledoc )を参照してください。

Ruby のコードを外部の関数として扱うと割り切ればマーシャリングの処理をハンドラの中に隠すことができます。 思い切って書き換えてみます。

  def handle_call({:eva_ruby, expression}, from, %{port: port, froms: froms} = state) do
    send(port, {self(), {:command, "print(Marshal.dump(#{expression}))\n"}})

    {:noreply, %{state | froms: [from | froms]}}
  end

  def handle_info({port, {:data, data}}, %{port: port, froms: [from | froms]} = state) do
    GenServer.reply(from, ExMarshal.load(data))

    {:noreply, %{state | froms: froms}}
  end

修正したコードの内容を反映して Ruby の式を評価してみます。

iex> ExIRB.eva_ruby("nil")
nil
iex> ExIRB.eva_ruby("true")
true
iex> ExIRB.eva_ruby("true")
true
iex> ExIRB.eva_ruby("false")
false
iex> ExIRB.eva_ruby("123")
123
iex> ExIRB.eva_ruby("[1,2,3]")
[1, 2, 3]
iex> ExIRB.eva_ruby("{a: 1, b: '2', c: :three}")
%{a: 1, b: "2", c: :three}

文字列の中は Ruby の式として評価されるのでメソッドも記述できます。 結果は ExMarshal.load/1 で変換しているので Elixir の値として返されます。

iex> ExIRB.eva_ruby("(1..10).map {|i| i * i }")
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

Level 5, Enchanter: ライブラリを読み込む

irbコマンドラインオプション -r で起動時にライブラリを読み込むことができます。

ここでは ExIRB.start_link/1 の引数で指定できるようにしてみます。

ExIRB.init/1 を次のように書き換えます。

  def init(opts) do
    path = System.find_executable("irb")
    requires =
      opts
      |> Keyword.get(:require, [])
      |> Enum.map(&"-r#{&1}")
    port = Port.open({:spawn_executable, path}, [:binary, args: ["--noverbose", "--noecho" | requires]])

    {:ok, %{port: port, froms: []}}
  end

オプションで :require で指定された値をライブラリ名のリストとして -r をつけて irb のオプションの形式にします。 Port.open/2 の起動時の引数にそれらを渡します。

ExIRB.start_link/1 でライブラリを指定します。

iex> ExIRB.start_link(require: ["yaml"])

ここでは yaml を指定しています。 これで YAML のメソッドを利用することができるようになりました。

iex> ExIRB.eva_ruby("YAML.load('---\n- 1\n- 2\n')")
[1, 2]

混乱しそうですが、こんなこともできます。

iex> yaml = """
...> ---
...> - 1
...> - 2
...> - a:
...>   - 3
...>   - 4
...> """
"---\n- 1\n- 2\n- a:\n  - 3\n  - 4\n"
iex> ExIRB.eva_ruby("YAML.load('#{yaml}')")
[1, 2, %{"a" => [3, 4]}]

Level 6, Warlock: apply(module, function, arguments)

Ruby の式を文字列で記述する代わりにモジュール名とメソッド名と引数列を評価できれば Elixir の値を渡すことができます。 ちょうど Kernel.apply/3 のような記述です。

このためにはまず引数列を文字列に変換する必要がありますが、これは個々の値を Kernel.inspect/2 で評価してコンマで連結すれば良さそうです。

iex> args = [1, "2", :three] |> Enum.map(&inspect/1) |> Enum.join(",")
"1,\"2\",:three"

iex> "Foo.bar(#{args})"
"Foo.bar(1,\"2\",:three)"

よさそうなのですが、いくつか落とし穴があるので注意しなければなりません。

長い値は省略される

要素の数が多く表現が長くなるばあい、途中から省略されてしまいます。

iex> (1..55) |> Enum.to_list() |> inspect()
"[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, ...]"

すべての要素を表示させるためには :limit オプションに :infinity を指定します。

iex> (1..55) |> Enum.to_list() |> inspect(limit: :infinity)
"[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55]"

表示できる文字コードのリストは charlist として表示される

Erlang や Elixir の「クセ」の部分。 整数値のリストなのに、その整数値が文字コードとして表示できるばあい、文字のリスト (charlist) として表現されてしまいます。

iex> (65..90) |> Enum.to_list() |> inspect(limit: :infinity)
"'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"

整数値のリストとして表示させるためには :charlists オプションに false を指定します。

iex> (65..90) |> Enum.to_list() |> inspect(limit: :infinity, charlists: false)
"[65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90]"

マップはには % が付く

マップを inspect すると、%{} で囲まれた文字列になります。 これは Elixir の仕様ですから、もうどうしようもありません。

iex> %{a: 1} |> inspect(limit: :infinity, charlits: false)
"%{a: 1}"

…と、あきらめかけていたのですが。 バージョン 1.9.0 から :inspect_fun オプションに適切な変換を行う関数を渡すことで任意の表示をさせることができるようになりました。

たとえば次のような関数 MapToHash.inspect_fun/2 を用意します。

defmodule MapToHash do
  import Inspect.Algebra

  def inspect_fun(term, opts) when is_map(term) do
    arrow = string("=>")
    comma = string(",")

    docs =
      term
      |> Enum.reduce([string("}")], fn {k, v}, acc ->
        key = inspect_fun(k, opts)
        value = inspect_fun(v, opts)
        [comma, key, arrow, value | acc]
      end)

    docs =
      case docs do
        [^comma | docs] -> docs
        docs -> docs
      end

    concat([string("{") | docs])
  end

  def inspect_fun(term, opts) do
    Inspect.inspect(term, opts)
  end
end

この関数を :inspect_fun オプションに指定します。

iex> inspect(%{a: 1}, limit: :infinity, charlists: false, inspect_fun: &MapToHash.inspect_fun/2)
"{:a=>1}"

入れ子になったマップでも空のマップでも Ruby のハッシュの形式の文字列で出力できます。

iex> inspect(%{a: 1, b: %{c: %{d: :e}}}, limit: :infinity, charlists: false, inspect_fun: &MapToHash.inspect_fun/2)
"{:b=>{:c=>{:d=>:e}},:a=>1}"
iex> inspect(%{}, limit: :infinity, charlists: false, inspect_fun: &MapToHash.inspect_fun/2)
"{}"

Elixir のモジュール名には Elixir が付いている

あとはモジュール名と関数名とを連結して先に書いた関数で Ruby にメッセージを送ればよさそうです。

まず期待する文字列を組み立てられるか、次のような関数を書いて確かめてみます。

  def apply(module, function, args) do
    args_str =
      args
      |> Enum.map(fn arg ->
        arg
        |> inspect(limit: :infinity, charlists: false, inspect_fun: &MapToHash.inspect_fun/2)
      end)
      |> Enum.join(",")

    "#{module}.#{function}(#{args_str})"
  end

モジュール名と関数名を文字列で指定してみます。

iex> ExIRB.apply("YAML", "load", ["---\n- 1\n - 2\n"])
"YAML.load(\"---\\n- 1\\n - 2\\n\")"

よさそうです。

次に関数名をアトムで指定してみます。

iex> ExIRB.apply("YAML", :load, ["---\n- 1\n - 2\n"])
"YAML.load(\"---\\n- 1\\n - 2\\n\")"

これもよさそうです。

最後に kernel.apply/3 と同じようにモジュール名で指定してみます。

iex> ExIRB.apply(YAML, :load, ["---\n- 1\n - 2\n"])
"Elixir.YAML.load(\"---\\n- 1\\n - 2\\n\")"

Elixir の文字列がつきました。

Elixir のモジュール名の実態はアトムです。

iex> is_atom(YAML)
true

ですのでモジュールが定義されていなくてもモジュール名は自由に使うことができます。 ただし注意する点がああります。たとえば YAML と書いたばあい内部的には :"Elixir.YAML" という値になっているという点です。 これは IEx でも簡単に確認することができます。

iex> :"Elixir.YAML"
YAML

iex> :"Elixir.YAML" == YAML
true

モジュールは Module.split/1 を使うと安全に文字列として分割できるので今回はこれを使うことにします。

iex> [module_name] = Module.split(YAML)
["YAML"]
iex> module_name
"YAML"

上に書いた apply 関数を修正して、組み立てた文字列を Ruby で評価する関数にします。

  def apply(server \\ @name, module, function, args) do
    [module_name] = Module.split(module)
    args_str =
      args
      |> Enum.map(fn arg ->
        arg
        |> inspect(limit: :infinity, charlists: false, inspect_fun: &MapToHash.inspect_fun/2)
      end)
      |> Enum.join(",")

    GenServer.call(server, {:eval_ruby, "#{module_name}.#{function}(#{args_str})"})
  end
iex> ExIRB.start_link(require: ["yaml"])
iex> ExIRB.apply(YAML, :load, ["---\n- 1\n- 2\n"])
[1, 2]
iex> ExIRB.apply(YAML, :dump, [%{a: 1}])
"---\n:a: 1\n"

これで Kernel.apply/3 と同じ書式で Ruby のメソッドを呼び出せるようになりました。

Level 7, Sorcerer: 関数を定義する

apply できるようになりましたが、やはり本来の関数名で呼び出したいものです。

関数を定義してみましょう。

defmodule ExYAML do
  def load(str), do: ExIRB.apply(YAML, :load, [str])
  def dump(term), do: ExIRB.apply(YAML, :dump, [term])
end
iex> ExIRB.start_link(require: ["yaml"])
iex> ExYAML.dump(%{a: 1})
"---\n:a: 1\n"
iex(3)> ExYAML.load("---\n- 1\n- 2\n")
[1, 2]

呼び出しは便利になりましたが関数を定義するのが面倒です。

そんなときは。 そう、マクロです。

関数を定義するマクロを定義します。

defmodule Importer do
  defmacro import_ruby(module, function) do
    quote do
      def unquote(function)(args) do
        ExIRB.apply(unquote(module), unquote(function), args)
      end
    end
  end
end

関数を定義したいモジュールでマクロを展開します。

defmodule ExYAML do
  require Importer
  import Importer

  import_ruby YAML, :load
  import_ruby YAML, :dump
end

これだけで関数が使えるようになります。

iex> ExIRB.start_link(require: ["yaml"])
iex> ExYAML.dump([%{a: 1}])
"---\n:a: 1\n"
iex(3)> ExYAML.load(["---\n- 1\n- 2\n"])
[1, 2]

しかし。このままでは一つの引数しか受け取れないので複数の引数を渡したいときにはリストにまとめて渡す必要があります。

引数の数も定義できる方法を考えます。

まず、quote された関数の構造を調べます。

iex> quote do
...>   def dump(term, opts) do
...>     ExIRB.apply(YAML, :dump, [term, opts])
...>   end
...> end
{:def, [context: Elixir, import: Kernel],
 [
   {:dump, [context: Elixir], [{:term, [], Elixir}, {:opts, [], Elixir}]},
   [
     do: {{:., [], [{:__aliases__, [alias: false], [:ExIRB]}, :apply]}, [],
      [
        {:__aliases__, [alias: false], [:YAML]},
        :dump,
        [{:term, [], Elixir}, {:opts, [], Elixir}]
      ]}
   ]
 ]}

二つの引数 (term, opts) は、[{:term, [], Elixir}, {:opts, [], Elixir}] という構造に展開されていることがわかります。 また ExIRB.apply/3 の三つ目の引数 [term, opts] も同じ構造になっています。

つまり必要な引数の数だけこの構造を並べたものをマクロの値として返すようにすれば、欲しい引数の数の関数を得ることができるはずです。

引数の構造を組み立てます。 26 個より多い引数を持つ関数はないだろうという大雑把な考えのもと、Stream で個々の引数のタプルを組み立て Enum.take/2 で切り出します。

args =
  (?a..?z)
  |> Stream.map(&{String.to_atom(<<&1>>), [], __MODULE__})
  |> Enum.take(arity)

これを関数の定義に組み込みます。 全体では、このようになります。

defmodule Importer do
  defmacro import_ruby(module, function, arity) do
    args =
      (?a..?z)
      |> Stream.map(&{String.to_atom(<<&1>>), [], __MODULE__})
      |> Enum.take(arity)

    {
      :def, [context: __MODULE__, import: Kernel],
      [
        {
          function,
          [context: __MODULE__],
          args
        },
        [do: {
          {:., [], [ExIRB, :apply]},
          [],
          [
            module,
            function,
            args
          ]
        }]
      ]
    }
  end
end

マクロを展開します。 追加した第三引数に引数の個数を指定する必要がありますが、これを利用することで引数の個数違いの関数を定義することができるようになりました。

defmodule ExYAML do
  require Importer
  import Importer

  import_ruby YAML, :load, 1
  import_ruby YAML, :dump, 1
  import_ruby YAML, :dump, 2
end
iex> ExIRB.start_link(require: ["yaml"])
iex> ExYAML.dump([1, [2, [3, 4]]])
"---\n- 1\n- - 2\n  - - 3\n    - 4\n"
iex> ExYAML.dump([1, [2, [3, 4]]], %{indentation: 4})
"---\n- 1\n-   - 2\n    -   - 3\n        - 4\n"

これで扱いやすくなりました。

しかしまだ何度も import_ruby YAML と書かなければならない手間があります。

Kernel.SpecialForms.import/2 のように、関数名と引数の個数をペアにしたキーワードリストでまとめて定義できるようにしてみます。

マクロを修正します。

defmodule Importer do
  defmacro import_ruby(module, functions) do
    functions
    |> Enum.map(fn {function, arity} ->
      args =
        (?a..?z)
        |> Stream.map(&{String.to_atom(<<&1>>), [], __MODULE__})
        |> Enum.take(arity)

      {
        :def, [context: __MODULE__, import: Kernel],
        [
          {
            function,
            [context: __MODULE__],
            args
          },
          [do: {
            {:., [], [ExIRB, :apply]},
            [],
            [
              module,
              function,
              args
            ]
          }]
        ]
      }
    end)
  end
end

利用する側も修正します。

defmodule ExYAML do
  require Importer
  import Importer

  import_ruby YAML, load: 1, dump: 1, dump: 2
end

利用しやすくなりました。

Level 8, Necromancer: モジュールを定義する

しかしまだ問題があります。

defmodule ExYAMLandJSON do
  require Importer
  import Importer

  import_ruby YAML, load: 1, dump: 1
  import_ruby JSON, load: 1, dump: 1
end

同じ名前、同じ引数の数の関数を定義すると、先に定義した関数にすべてのパタンがマッチしてしまい後から定義した関数は呼ばれません。

iex> ExYAMLandJSON.dump(%{a: 1})
"---\n:a: 1\n"

名前空間をわけたいところです。 Ruby のモジュール名を渡しているので、これを名前空間にできないか考えます。

こんどは quote されたモジュールの構造を調べます。

iex> quote do
...>   defmodule Yaml do
...>     def dump(term) do
...>       ExIRB.apply(YAML, :dump, [term])
...>     end
...>
...>     def load(str) do
...>       ExIRB.apply(YAML, :dump, [str])
...>     end
...>   end
...> end
{:defmodule, [context: Elixir, import: Kernel],
 [
   {:__aliases__, [alias: false], [:Yaml]},
   [
     do: {:__block__, [],
      [
        {:def, [context: Elixir, import: Kernel],
         [
           {:dump, [context: Elixir], [{:term, [], Elixir}]},
           [
             do: {{:., [], [{:__aliases__, [alias: false], [:ExIRB]}, :apply]},
              [],
              [
                {:__aliases__, [alias: false], [:YAML]},
                :dump,
                [{:term, [], Elixir}]
              ]}
           ]
         ]},
        {:def, [context: Elixir, import: Kernel],
         [
           {:load, [context: Elixir], [{:str, [], Elixir}]},
           [
             do: {{:., [], [{:__aliases__, [alias: false], [:ExIRB]}, :apply]},
              [],
              [
                {:__aliases__, [alias: false], [:YAML]},
                :dump,
                [{:str, [], Elixir}]
              ]}
           ]
         ]}
      ]}
   ]
 ]}

関数を定義するタプル {:def, ...} の部分を除くと思いの外シンプルです。

また、マクロを利用するモジュールの名前空間の中でモジュールを定義したいわけですが、そのモジュール名は Kernel.SpecialForms.__CALLER__/0 で取得することができます。

これらをすべて適用します。

defmodule Requirer do
  defmacro require_ruby(module, functions) do
    ruby_module =
      case module do
        {:__aliases__, _, [ruby_module]} -> ruby_module
        module -> module
      end

    quoted_functions =
      functions
      |> Enum.map(fn {function, arity} ->
        args =
          (?a..?z)
          |> Stream.map(&{String.to_atom(<<&1>>), [], __MODULE__})
          |> Enum.take(arity)

        {
          :def, [import: Kernel],
          [
            {function, [], args},
            [do: {
              {:., [], [ExIRB, :apply]},
              [],
              [module, function, args]
            }]
          ]
        }
      end)

    {
      :defmodule,
      [import: Kernel],
      [
        Module.concat(__CALLER__.module, ruby_module),
        [do: {:__block__, [], quoted_functions}]
      ]
    }
  end
end

マクロを利用するモジュールを書きます。

defmodule ExYAMLandJSON do
  require Requirer
  import Requirer

  require_ruby YAML, load: 1, dump: 1
  require_ruby JSON, load: 1, dump: 1
end

使ってみます。

iex> ExIRB.start_link(require: ["yaml", "json"])
iex> ExYAMLandJSON.JSON.dump(%{a: 1, b: [1, 2, 3]})
"{\"b\":[1,2,3],\"a\":1}"
iex> ExYAMLandJSON.YAML.dump(%{a: 1, b: [1, 2, 3]})
"---\n:b:\n- 1\n- 2\n- 3\n:a: 1\n"

マクロを利用するモジュールの名前に require_ruby したモジュールの名前を連結した新しいモジュールが定義され、その各々に関数が定義されました。

Level 9, Wizard: ぜんぶまとめると

これらのコードを整理して GitHub に push しました

使ってみましょう。

プロジェクトを作成する

新しいプロジェクトを作成します。プロセスを supervisor で管理したいので --sup オプションを指定します。

$ mix new sample --sup
$ cd sample

依存パッケージを設定する

mix.exs を編集して依存パッケージに ex_irb を指定します。

defmodule Sample.MixProject do
  use Mix.Project

  def project do
    [
      app: :sample,
      version: "0.1.0",
      elixir: "~> 1.9",
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end

  def application do
    [
      extra_applications: [:logger],
      mod: {Sample.Application, []}
    ]
  end

  defp deps do
    [
      {:ex_irb, github: "mattsan/ex_irb"}
    ]
  end
end

パッケージを取得します。

$ mix deps.get

ExIRB プロセスの起動を設定する

lib/sample/application.ex を編集して ExIRB のプロセスを起動する設定を記述します。

defmodule Sample.Application do
  @moduledoc false

  use Application

  def start(_type, _args) do
    children = [
      {ExIRB, require: ["yaml", "json"]}
    ]

    opts = [strategy: :one_for_one, name: Sample.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

召喚するモジュールを記述する

ExIRB.Requirer を使って Ruby のモジュールを Elixir のモジュールに召喚します。

defmodule Sample do
  @moduledoc """
  Documentation for Sample.
  """

  require ExIRB.Requirer
  import ExIRB.Requirer

  require_ruby YAML, load: 1, dump: 1, dump: 2
  require_ruby JSON, load: 1, dump: 1
end

使う

iex を起動します。

$ iex -S mix

iex のプロンプトから召喚した Ruby のメソッドを呼びます。 アプリケーションが起動するときに ExIRB を起動しているのですぐに利用することができます。

iex> Sample.JSON.dump(%{a: 1})
"{\"a\":1}"
iex> Sample.YAML.dump(%{a: 1})
"---\n:a: 1\n"
iex> Sample.JSON.load(~S[{"a":1}])
%{"a" => 1}
iex> Sample.YAML.load("---\na: 1\n")
%{"a" => 1}

ちなみに。

終了処理をきちんと書いていないので、 iex を終了すると irb と接続しているポートが閉じて irb が例外を投げスタックトレイスを吐き出して停止します。

まぁ、黒魔術だから…。

結論

わかりやすく書こう。

----うますぎるプログラムはいけない

プログラム書法 第2版

いつか読むはずっと読まない:霊媒師、占い師、奇術師、…

ダンジョンズ&ドラゴンズ スターター・セット第5版

ダンジョンズ&ドラゴンズ スターター・セット第5版

Phoenixが入ったDocker containerをHerokuに配備する

de・ploy | dɪplɔ́ɪ |

他動詞

1 〘軍〙 〈部隊など〉を展開させる, 〈兵器など〉を配置[配備]する.

ウィズダム英和辞典


いまさらなのですが。

Phoenix のドキュメントに Docker container を利用したリリースの手順が追加されていることに気がつきました。 今夏のアップデートで追加されたようです。

また Heroku のドキュメントにも Docker container を利用したデプロイの手順が掲載されています。

従来から Phoenix アプリケーションを Heroku にデプロイするには buildpack を利用する手順がドキュメントに記載されていますが、これらのドキュメントに従えば Docker container を利用したリリースが可能になるはずです。

と、いうわけで。 Phoenix アプリケーションを new するところから Heroku にデプロイするまでの手順をまとめてみました。

いずれも Phoenix あるいは Heroku のドキュメントに記載されている内容ですので難しいことはないと思いますが、細かなところで情報が分散していてつまづくところがあったので何かの参考になればと思います。

{:elixir, "~> 1.9"}

なお Config モジュールと mix release コマンドを利用するので Elixir は 1.9 以上が必要です。

プロジェクトを用意する

プロジェクトを作成します。

$ mix phx.new hoge
$ cd hoge

Heroku はリポジトリの設定を .git/config に記録するので、Git のリポジトリを作成しておいてください。 設定が記録できればよいのでコードの変更を commit しておく必要はないのですが、一般的に作業の巻き戻しなどに備えて適宜 commit しながら進めてください。

$ git init
$ git add .
$ git commit -m 'initial commit'

Config を変更する

config ファイルを書き換えます。

書き換えについては Phoenix のドキュメント「Deploying with Releases」のセクション「Runtime configuration」に解説があります。

ファイル名の変更

ディレクトconfig/ にある prod.secret.exs のファイル名を releases.exs に変更します。

releases.exs の編集

releases.exs の内容を編集します。

まず、モジュール Config を利用するように変更します。

use Mix.Configimport Config に書き換えます。

--- a/config/prod.secret.exs
+++ b/config/releases.exs
@@ -2,7 +2,7 @@
 # from environment variables. You can also hardcode secrets,
 # although such is generally not recommended and you have to
 # remember to add this file to your .gitignore.
-use Mix.Config
+import Config

次に Endpoint の設定に server: true を追加します。

--- a/config/prod.secret.exs
+++ b/config/releases.exs
@@ -25,7 +25,8 @@ secret_key_base =
 
 config :hoge, HogeWeb.Endpoint,
   http: [:inet6, port: String.to_integer(System.get_env("PORT") || "4000")],
-  secret_key_base: secret_key_base
+  secret_key_base: secret_key_base,
+  server: true

これは Mix を利用せずに単独でサーバとして起動するための設定です。

  • :server - when true, starts the web server when the endpoint supervision tree starts. Defaults to false. The mix phx.server task automatically sets this to true

Runtime configuration / Phoenix.Endpoint

なお Heroku の制限として、HTTP のポートは Heroku が設定する環境変数 PORT の値を利用する必要があります。

  • The web process must listen for HTTP traffic on $PORT, which is set by Heroku.

Dockerfile commands and runtime / Container Registry & Runtime (Docker Deploys) | Heroku Dev Center

ただこれは自動生成される config に port: String.to_integer(System.get_env("PORT") || "4000") と最初から設定されていますので変更せずこのままで大丈夫です。

また config/releases.exs については、一つ前の記事に簡単にまとめましたので参考にしてみてください。

不要な設定の削除

prod.secret.exs が不要になったので prod.exs 内の prod.secret.exs を読み込んでいる行を削除します。

--- a/config/prod.exs
+++ b/config/prod.exs
@@ -52,4 +52,3 @@ config :logger, level: :info
 
 # Finally import the config/prod.secret.exs which loads secrets
 # and configuration from environment variables.
-import_config "prod.secret.exs"

Dockerfile を用意する

Dockerfile を用意します。 アプリケーション名が現れるのはリリース版のファイルをコピーする部分とサーバを起動する部分のみです。 この二つを書き換えるだけで他のプロジェクトでも基本的に同じ内容で対応できます。

########################################
# ビルド
########################################

FROM elixir:1.9-alpine as build

WORKDIR /app

# ビルドに必要なツールをインストールする
RUN apk add --update git build-base nodejs yarn npm

# Hex と rebar をインストールする
RUN mix local.hex --force && mix local.rebar

# ビルド時の環境変数を設定する
ENV MIX_ENV=prod

# 依存するパッケージをインストールする
COPY mix.exs ./
COPY mix.lock ./
COPY config ./config
RUN mix deps.get --only prod
RUN mix deps.compile

# assets をインストールする
COPY assets ./assets
RUN npm install --prefix ./assets && npm run deploy --prefix ./assets
RUN mix phx.digest

# ビルドする
COPY priv ./priv
COPY lib ./lib
RUN mix compile

# リリース版を構築する
#   リリースの設定を rel/ に用意しているばあいは、それのコピーもしておいてください
# COPY rel ./rel
RUN mix release

########################################
# リリースするイメージを構築する
########################################

FROM alpine:3.10 as web

WORKDIR /app

# 実行時に必要なツールをインストールする
RUN apk add --update bash openssl

# ビルドしたリリース版のファイルをコピーする
#   アプリケーション名の部分 ( `hoge` ) は作成したアプリケーションの名前に合わせてください
COPY --from=build /app/_build/prod/rel/hoge ./

# ユーザを設定する
RUN chown -R nobody: /app
USER nobody

# 実行時の環境変数を設定する
ENV HOME=/app

# サーバを起動する
#   アプリケーション名の部分 ( `hoge` ) は作成したアプリケーションの名前に合わせてください
CMD bin/hoge start

Heroku にデプロイする

Heroku アプリケーションの作成

まず Heroku のコンテナリポジトリにログインします。

$ heroku container:login

次に Heroku に新しいアプリケーションを作成します。 heroku create コマンドを使って、あるいは Heroku のコンソールを利用して作成します。

$ heroku create hoge-with-docker

環境変数の設定

環境変数を設定します。

SECRET_KEY_BASE を設定します。 ここでは mix phx.gen.secret コマンドで生成した値を設定しています。 heroku config:set コマンドを使って、あるいは Heroku のコンソールを利用して設定します。

$ heroku config:set SECRET_KEY_BASE=`mix phx.gen.secret`

データベースの設定

データベースを利用するばあい、データベースの設定をします。 Heroku のアドオンを利用するばあい、heroku addons:create コマンドを使って、あるいは Hroku コンソールを利用して設定します。

$ heroku addons:create heroku-postgresql

アドオンを利用したばあいは接続先の URL が環境変数DATABASE_URL に設定されます。 config/releases.exs の初期状態では DATABASE_URL から接続先の情報を取得するようになっているので、この状態でデプロイすればデータベースに接続できるようになります。

Heroku のアドオン以外のデータベースを利用するばあいは、そのデータベースのURLを DATABASE_URL に設定してください。

$ heroku config:set DATABASE_URL=postgres://USER:PASSWORD@DOMAIN:PORT/DATABASE

Push the image and release

Docker イメージをビルドして Heroku に push します。

$ heroku container:push web

push したイメージをリリースします。

$ heroku container:release web

すべての準備が整ったので、デプロイしたアプリケーションを開きます。 ブラウザでアプリケーションの URL を入力して開くか、あるいは heroku open コマンドでページを開きます。

$ heroku open

データベースのマイグレーション

データベースを利用するばあい、データベースのマイグレーションを実行する必要があります。 通常は mix ecto.migrate コマンドを利用しますがリリース版は Mix を含まないためこのコマンドを実行できません。

このようなばあい、自分でマイグレーションの処理を用意し実行する必要があります。

これについても Phoenix のドキュメントに記載されています。

lib/hoge/マイグレーションを実行する関数を記述した次のような内容のファイル release.ex を追加します。

ファイル名、ファイルの格納ディレクトリは、マイグレーションを実行できれば任意でかまわないのですが、ここではドキュメントに合わせた構成にしています。

# lib/hoge/release.ex

defmodule Hoge.Release do
  @app :hoge

  def migrate do
    for repo <- repos() do
      {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
    end
  end

  def rollback(repo, version) do
    {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
  end

  defp repos do
    Application.load(@app)
    Application.fetch_env!(@app, :ecto_repos)
  end
end

これをアプリケーションの eval コマンドで実行します。

Phoenix のドキュメントでは次のような実行例が記載されています。

$ _build/prod/rel/my_app/bin/my_app eval "MyApp.Release.migrate"

今回は Heroku にデプロイしているので Heroku 上でプロセスを実行するので heroku run コマンドを利用します。

またリリースは _build/prod/rel/hoge/ 以下の内容をデプロイしているので実行パスも bin/ 以降を指定すればよく bin/hoge eval という形になります。

全体として。次のようにマイグレーションを実行します。 アプリケーション名の部分 ( hoge ) 、モジュール名の部分 ( Hoge ) は作成されるアプリケーション、モジュールに置き換えて実行してください。

$ heroku run 'bin/hoge eval "Hoge.Release.migrate()"'

WebSocket を利用する

Phoenix.Channel や Phoenix.LiveView などで WebSocket を利用するばあい、 :check_origin を設定する必要があります。

  • :check_origin - configure transports to check origin header or not. May be false, true, a list of hosts that are allowed, or a function provided as MFA tuple. Hosts also support wildcards.

Runtime configuration/ Phoenix.Endpoint

config/releases.exs に :check_origin を追加します。 ここでは環境変数を利用した設定にしています。

# config/releases.exs

config :hoge, HogeWeb.Endpoint,
  http: [:inet6, port: String.to_integer(System.get_env("PORT") || "4000")],
  secret_key_base: secret_key_base,
  check_origin: ["//#{System.get_env("HOST")}"],
  server: true

アプリケーションの環境変数に値を追加します。

$ heroku config:set HOST=hoge-with-docker.herokuapp.com

これで WebSocket のリクエストを受け付けることができるようになりました。

いつか読むはずっと読まない:古典

自分にとって長らくプログラミングスタイルの拠り所になっていたのは間違いなく「プログラム書法」でした。 さすがに現在ではそぐわない内容も多くなりましたが、プログラミングに対する姿勢はこの本で学びました。

「達人プログラマー」(翻訳旧版)を読んだとき、「現代の『プログラム書法』だ」と思ったものでした。

その「達人プログラマー」も現在ではすっかり古典となったようです。 古典であるからこそ、立ち返ってその考えをふたび吸収したいと思います。

プログラム書法 第2版

プログラム書法 第2版

新装版 達人プログラマー 職人から名匠への道

新装版 達人プログラマー 職人から名匠への道

Elixirのreleases.exsの使いどころ

リリースするアプリケーションでは、実行環境に依存する情報をコード内に抱え込まないように、環境変数から設定を取得することが多いと思います。 Elixir では設定情報を config/config.exs に書くことが多いのですが、このファイルはビルド時に読み込まれるため、この中で環境変数の値を取り込んでも実行時に反映されません。

Elixir 1.9 では config の扱いが変わりました。

1.8 までは Mix.Config という Mix のモジュールを利用していましたが、1.9 からは Elixir が直接持つ Config というモジュールを利用するように変更されています。

また、実行環境に合わせた設定をアプリケーションが起動するタイミングでおこないたい、という動機で config/releases.exs が導入されたようです。

アプリケーションをリリースする段になって混乱しないように Elixr 1.9 の config の使い方を確認してみました。

config/config.exs

まず従来からある config/config.exs の挙動を確認してみます。

挙動を確認するためのプロジェクトを用意する

適当なプロジェクトを用意します。

$ mix new hoge
$ cd hoge

1.9 からは config の扱いが変更になったのにともない、 mix new コマンドはディレクトconfig および config ファイルを作成しなくなりました。

config が必要なばあいは自分でディレクトconfig を作成します。

$ mkdir config

config.exs を記述する

config/config.exs ファイルを作成して設定を記述します。

import Config

config :hoge,
  foo: System.get_env("FOO"),
  bar: System.get_env("BAR")

挙動を確認するための関数を用意する

lib/hoge.ex にアプリケーションの設定を表示する関数を追加します。

defmodule Hoge
  def show_env do
    IO.write("foo: ")
    IO.inspect(Application.fetch_env(:hoge, :foo))

    IO.write("bar: ")
    IO.inspect(Application.fetch_env(:hoge, :bar))
  end
end

またリリース版を作成するために lib/hoge/application.ex を用意します。

defmodule Hoge.Application do
  use Application

  def start(_, _) do
    # 環境変数を表示する関数を呼び出す
    Hoge.show_env()

    # 今回は実行を継続する必要がないので環境変数を表示したら終了する
    System.halt()
  end
end

Hoge.Application を callback module として設定します。

(最初の投稿時にこの記述が抜けていました。失礼しました)

defmodule Hoge.MixProject do

  # ...

  def application do
    [
      extra_applications: [:logger],
      mod: {Hoge.Application, []}
    ]
  end

  # ...

end

挙動を確認する

リリース版をビルドします。

$ mix release

実行します。

$ _build/dev/rel/hoge/bin/hoge start
foo: {:ok, nil}
bar: {:ok, nil}

環境変数を設定して実行します。

$ FOO=foo BAR=bar _build/dev/rel/hoge/bin/hoge start
foo: {:ok, nil}
bar: {:ok, nil}

実行時に環境変数を指定しても反映されません。

リリース版をビルドするときに環境変数を設定してみます。

$ FOO=foo BAR=bar mix release

実行します。

$ _build/dev/rel/hoge/bin/hoge start
foo: {:ok, "foo"}
bar: {:ok, "bar"}

環境変数を設定して実行します。

hoge $ FOO=hoge BAR=fuga _build/dev/rel/hoge/bin/hoge start
foo: {:ok, "foo"}
bar: {:ok, "bar"}

実行時の環境変数は影響を与えず、ビルド時に設定した値が表示されることがわかります。

config/releases.exs

次に config/releases.exs を作成して設定を記述します。

releases.exs を記述する

import Config

config :hoge,
  bar: System.get_env("BAR")

挙動を確認する

リリース版をビルドします。

$ mix release

実行します。

$ _build/dev/rel/hoge/bin/hoge start
foo: {:ok, nil}
bar: {:ok, nil}

環境変数を設定して実行します。

$ FOO=hoge BAR=fuga _build/dev/rel/hoge/bin/hoge start
foo: {:ok, nil}
bar: {:ok, "fuga"}

config/releases.exs環境変数を利用するようにした bar は実行時の環境変数の値が反映されることがわかります。

次にビルド時に環境変数を設定します。

$ FOO=foo BAR=bar mix release

実行します。

$ _build/dev/rel/hoge/bin/hoge start
foo: {:ok, "foo"}
bar: {:ok, nil}

ビルド時に環境変数 BAR にも値を設定しましたが、実行時に設定していないため bar の値は nil になっています。 またここから、 config.exsreleases.exs に同じ設定を記述したばあい、releases.exs の設定が優先されることがわかります。

環境変数を指定して実行します。

$ FOO=hoge BAR=fuga _build/dev/rel/hoge/bin/hoge start
foo: {:ok, "foo"}
bar: {:ok, "fuga"}

config/release.exs で設定していない foo はビルド時の環境変数の値になっています。 一方 config/releases.exs で設定した bar は慈光寺の環境変数の値になっています。

releases.exs の在り処

最後に。 releases.exs がどこで利用されているか確認します。

$ find . -name releases.exs
./config/releases.exs
./_build/dev/rel/hoge/releases/0.1.0/releases.exs

config/releases.exs はリリース版に同梱されていることがわかります。

またアプリケーションを起動するスクリプト _build/dev/rel/hoge/bin/hoge を読んでみると、_build/dev/rel/hoge/releases/0.1.0/sys.config のコピーを読み込んでいることがわかります。

このファイルの内容を確認すると次のようになっています。 実際のファイルではコメント行以外は一行で記述されていますが、ここでは読みやすいように改行とインデント、およびバイナリ部分にコメントを追加しました。

Erlang の内容に踏み込んでしまうので深追いはしませんが、同梱された releases.exsConfig.Reader で利用するように設定されていることがわかります。

%% coding: utf-8
%% RUNTIME_CONFIG=true
[
  {
    hoge,
    [
      {foo, nil},
      {bar, nil}
    ]
  },
  {
    elixir,
    [
      {
        config_providers,
        #{
          '__struct__' => 'Elixir.Config.Provider',
          config_path => {
            system,
            % "RELEASE_SYS_CONFIG"
            <<82,69,76,69,65,83,69,95,83,89,83,95,67,79,78,70,73,71>>,
            % ".config"
            <<46,99,111,110,102,105,103>>
          },
          extra_config => [
            {
              kernel,
              [
                {start_distribution, true}
              ]
            }
          ],
          providers => [
            {
              'Elixir.Config.Reader',
              {
                system,
                % "RELEASE_ROOT"
                <<82,69,76,69,65,83,69,95,82,79,79,84>>,
                % "/releases/0.1.0/releases.exs"
                <<47,114,101,108,101,97,115,101,115,47,48,46,49,46,48,47,114,101,108,101,97,115,101,115,46,101,120,115>>
              }
            }
          ],
          prune_after_boot => false
        }
      }
    ]
  },
  {
    kernel,
    [
      {start_distribution, false}
    ]
  }
].

いつか読むはずっと読まない:mismatch

昨年まで 3 年近く関わったプロジェクトではバックエンドの部分を担ったため、エンドユーザはあまり意識せずに開発をしていました。

今年から再びフロントエンドを含むウェブサービスの開発に参加することになりましたが、一番に戸惑ったのがフロントエンドの書き方、特にテストの書き方を結構忘れてしまっていたこと。 幸いすぐに勘を取り戻すことができましたが、そういったものに気を取られることがなくなるとアプリケーションが提供している操作そのものに意識が向くようになります。

ミスマッチ 見えないユーザーを排除しない「インクルーシブ」なデザインへ

ミスマッチ 見えないユーザーを排除しない「インクルーシブ」なデザインへ

  • 作者: キャット・ホームズ,ジョン・マエダ,大野千鶴
  • 出版社/メーカー: ビー・エヌ・エヌ新社
  • 発売日: 2019/03/15
  • メディア: 単行本
  • この商品を含むブログを見る

Phoenix.LiveView 事始め(subscribeとbroadcast)

前回の続きです。

前回、「バージョン 0.1 が公開され…」と書いたばかりなのに、今回の記事を書くまでの十日足らずの間にバージョン 0.2 が公開されてしまいました。

それはそれとして。

今回は Phoenix.LiveView の app で subscribe と broadcast を使って、複数のユーザ(ブラウザ)で通信させようという試みです。

使うサンプルはクリックしたセルの色が同期して変化する Phoenix app です。

モジュールを追加する手前まで進む

LiveView を利用できるように設定するところまでは同じですので前回の記事を参照しながら、「準備」「パッケージを追加する」「Endpoint を編集する」「ソケットのクライアントを設定する」「salt を設定する」まで進みます。

ここでは live_click という app 名で作成しています。

$ mix phx.new live_click --no-ecto

まずは単独の app を作成する

live モジュール

ディレクトlib/live_click_web/live を作成し、そこに click_live.ex を作成してモジュール LiveClickWeb.ClickLive を定義します。

defmodule LiveClickWeb.ClickLive do
  use Phoenix.LiveView

  def mount(_session, socket) do
    new_socket =
      socket
      |> assign(:effects, %{})
    {:ok, new_socket}
  end

  def render(assigns) do
    Phoenix.View.render(LiveClickWeb.ClickView, "index.html", assigns)
  end

  def handle_event("cell", %{"row" => row, "col" => col}, socket) do
    new_socket =
      socket
      |> assign_cell_color(String.to_integer(row), String.to_integer(col))
    {:noreply, new_socket}
  end

  def assign_cell_color(socket, row, col) do
    effects = socket.assigns.effects

    new_effect =
      case Map.get(effects, {row, col}) do
        :red -> :blue
        :blue -> :green
        :green -> :white
        _ -> :red
      end

    socket
    |> assign(:effects, Map.put(effects, {row, col}, new_effect))
  end
end

view モジュール

lib/live_click_web/views/click_view.ex を作成してモジュール LiveClickWeb.ClickView を定義します。

defmodule LiveClickWeb.ClickView do
  use LiveClickWeb, :view
end

テンプレートとスタイル

lib/live_click_web/templates/click/index.html.leex を作成してテンプレートを定義します。

<div>
  <%= Enum.map (0..9), fn row -> %>
    <div>
      <%= Enum.map (0..9), fn col -> %>
        <div class="cell <%= Map.get(@effects, {row, col}) %>" phx-click="cell" phx-value-row="<%= row %>" phx-value-col="<%= col %>">
          <%= row %><%= col %>
        </div>
      <% end %>
    </div>
  <% end %>
</div>

また assets/css/app.css を編集してスタイルを追加します。

@import "./phoenix.css";

/* ここから下を追加 */

.cell {
  display: inline-block;
  height: 50px;
  width: 50px;
  border: solid thin #ccc;
  text-align: center;
  line-height: 50px;
  margin-bottom: 5px;
}

.red {
  background-color: red;
}

.green {
  background-color: green;
}

.blue {
  background-color: blue;
}

.white {
  background-color: white;
}

ルーティングを変更する

lib/live_click_web/router.ex を編集します。

   scope "/", LiveClickWeb do
     pipe_through :browser
 
-    get "/", PageController, :index
+    live "/", ClickLive
   end

動作を確認する

iex コマンドあるいは mix コマンドで Phoenix app をローカルに起動し、http://localhost:4000 にアクセスして動作を確認します。

$ iex -S mix phx.server

セルをクリックすると、クリックしたセルの色がクリックのたびに 白 → 赤 → 青 → 緑 → 白 と変化すれば成功です。

subscribe と broadcast と handler を追加する。

Phoenix.LiveView 自体が WebSocket を利用しているため、ここから少しコードを追加するだけで他のブラウザにイベントを送ることができるようになります。

subscribe

イベントを subscribe するために LiveClickWeb.ClickLive.mount/2LiveClickWeb.Endpoint.subscribe(@topic) の一行を追加します。 これだけで broadcast されたイベントを受け取ることができるようになります。

  @topic "live_click:cells"

  def mount(_session, socket) do
    LiveClickWeb.Endpoint.subscribe(@topic)
    new_socket =
      socket
      |> assign(:effects, %{})
    {:ok, new_socket}
  end

関数の詳細はドキュメントを参照してください。

broadcast

イベントを broadcast するために LiveClickWeb.Endpoint.broadcast_from/4 の一行を追加します。 今回は LiveClickWeb.ClickLive. assign_cell_color/3 でローカルで発生したイベントを処理をしているのでここに追加します。

関数の第二引数には subscribe したトピックを文字列で、第三引数にはイベントを文字列で、第四引数には送信したいパラメータをマップで設定します。

  def assign_cell_color(socket, row, col) do
    effects = socket.assigns.effects

    new_effect =
      case Map.get(effects, {row, col}) do
        :red -> :blue
        :blue -> :green
        :green -> :white
        _ -> :red
      end

    LiveClickWeb.Endpoint.broadcast_from(self(), @topic, "click", %{key: {row, col}, value: new_effect})

    socket
    |> assign(:effects, Map.put(effects, {row, col}, new_effect))
  end

broadcast 元以外にイベントを送るために broadcast_from/4 を利用しましたが、全体に送るには broadcast/3 を利用します。

関数の詳細はドキュメントを参照してください。

handler

broadcast されたイベントはモジュール LiveClickWeb.ClickLive へのメッセージとして届きます。メッセージをハンドリングするため LiveClickWeb.ClickLive.handle_info/2 を記述します。

第一引数は broadcast の引数の内容を格納したマップになります。 :topic にトピックが、:event にイベントが、:payload にその他のパラメータが格納されます。

  def handle_info(%{topic: @topic, event: "click", payload: %{key: key, value: value}}, socket) do
    effects = socket.assigns.effects
    new_socket =
      socket
      |> assign(:effects, Map.put(effects, key, value))

    {:noreply, new_socket}
  end

関数の詳細はドキュメントを参照してください。

実行

app を起動し、ブラウザのウィンドウを二つ以上開きます。 一つのブラウザでクリックした内容が他のブラウザにも反映されることが確認できます。

一つ注意点。 このサンプルではセルの状態を永続化しているわけではないので、表示の内容のすべてが同期されるわけではありません。 あるセルをクリックしたことによるそのセルの状態の変化のみが伝わるようになっています。

いつか読むはずっと読まない:THE LONG WAY TO A ...

銀河共同体の中で、様々な星系出身の容姿も文化も価値観も違うクルーが一つの船で目的地に向かう。古典的なスペオペがわくわく感を誘います。

とはいえ。「古典的」とはあえて書いてみました。設定だけを切り出すと懐かしいスペオペの雰囲気があるのですが、書かれた時代背景が作品に現れるのはこの作品にも違いはなく、やはり現代の作品なのだな、と感じます。

銀河核へ 上 (創元SF文庫)

銀河核へ 上 (創元SF文庫)

銀河核へ 下 (創元SF文庫)

銀河核へ 下 (創元SF文庫)

Phoenix.LiveView 事始め

先月末にバージョン 0.1 が公開され、Hex からインストールできるようになりました。

これを機に Phoenix.LiveView に挑戦です。

この記事では、app 作成から LiveView で表示を動かすまでの作業を、ただ淡々と書いてゆきます。

準備

適当な Phoenix app を用意します。

以降の説明のために、ここでは DB (Ecto) を利用しない app を新規に作成します。

$ mix phx.new my_app --no-ecto
$ cd my_app

パッケージを追加する

mix.exs を編集して phoenix_live_view を追加します。

--- a/mix.exs
+++ b/mix.exs
@@ -38,7 +38,8 @@ defmodule MyApp.MixProject do
       {:phoenix_live_reload, "~> 1.2", only: :dev},
       {:gettext, "~> 0.11"},
       {:jason, "~> 1.0"},
-      {:plug_cowboy, "~> 2.0"}
+      {:plug_cowboy, "~> 2.0"},
+      {:phoenix_live_view, "~> 0.1"}
     ]
   end
 end
$ mix deps.get

assets/package.jsonJavaScript で利用するパッケージの依存情報を追加します。

--- a/assets/package.json
+++ b/assets/package.json
@@ -7,7 +7,8 @@
   },
   "dependencies": {
     "phoenix": "file:../deps/phoenix",
-    "phoenix_html": "file:../deps/phoenix_html"
+    "phoenix_html": "file:../deps/phoenix_html",
+    "phoenix_live_view": "file:../deps/phoenix_live_view"
   },
$ npm install --prefix assets

Endpoint を編集する

lib/my_app_web/endpoint.ex を編集し、 LiveView で使うソケットを設定します。

--- a/lib/my_app_web/endpoint.ex
+++ b/lib/my_app_web/endpoint.ex
@@ -5,6 +5,8 @@ defmodule MyAppWeb.Endpoint do
     websocket: true,
     longpoll: false
 
+  socket "/live", Phoenix.LiveView.Socket

ソケットのクライアントを設定する

assets/js/app.js を編集し、ソケットに接続するためのクライアントのコードを追加します。

--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -15,3 +15,8 @@ import "phoenix_html"
 //
 // Local files can be imported directly using relative paths, for example:
 // import socket from "./socket"
+
+import LiveSocket from "phoenix_live_view"
+
+let liveSocket = new LiveSocket("/live")
+liveSocket.connect()

salt を設定する

config/config.exs の endpoint の設定の項目に :live_view を追加し、キーワードリストに :signing_salt を設定します。

--- a/config/config.exs
+++ b/config/config.exs
@@ -10,11 +10,12 @@ use Mix.Config
 # Configures the endpoint
 config :my_app, MyAppWeb.Endpoint,
   url: [host: "localhost"],
   secret_key_base: "QshB2lH5dWIXtZFX9mewWHeY/Lt/EpyQv/ErFXjXMfDmstWH3eQ9dVqM2rfdGLBX",
   render_errors: [view: MyAppWeb.ErrorView, accepts: ~w(html json)],
-  pubsub: [name: MyApp.PubSub, adapter: Phoenix.PubSub.PG2]
+  pubsub: [name: MyApp.PubSub, adapter: Phoenix.PubSub.PG2],
+  live_view: [signing_salt: "some-secure-salt"]

モジュールを追加する

ディレクトlib/my_app_web/live を作成し LiveView のモジュール MyAppWeb.SampleLIveを定義したファイル lib/my_app_web/live/sample_live.ex を作成します。

defmodule MyAppWeb.SampleLive do
  use Phoenix.LiveView

  def render(assigns) do
    ~L"""
    Hello Phoenix.LiveView world!
    """
  end

  def mount(_session, socket) do
    {:ok, socket}
  end
end

router を設定する

lib/my_app_web/router.ex を編集し、MyAppWeb.SampleLIve を利用する routing を追加します。

--- a/lib/my_app_web/router.ex
+++ b/lib/my_app_web/router.ex
@@ -1,26 +1,28 @@
 defmodule MyAppWeb.Router do
   use MyAppWeb, :router
+  import Phoenix.LiveView.Router
 
   pipeline :browser do
     plug :accepts, ["html"]
     plug :fetch_session
     plug :fetch_flash
     plug :protect_from_forgery
     plug :put_secure_browser_headers
   end
 
   pipeline :api do
     plug :accepts, ["json"]
   end
 
   scope "/", MyAppWeb do
     pipe_through :browser
 
     get "/", PageController, :index
+    live "/sample", SampleLive
   end

最初の表示

app を起動し、http://localhost:4000/sample にアクセスします。

$ iex -S mix phx.server

MyAppWeb.SampleLIverender/1 に記述した内容がレンダリングされ表示されました。

view と template を追加する

LiveView のモジュール内に sigil ( ~L ) で記述する代わりに view と template で表示するように変更します。

新規に view のファイル lib/my_app_web/views/sample_view.ex を作成してモジュール MyAppWeb.SampleView を定義します。

defmodule MyAppWeb.SampleView do
  use MyAppWeb, :view
end

同じく新規に template のファイル lib/my_app_web/templates/sample/show.html.leex を作成してテンプレートを記述します。LiveView のテンプレートは拡張子が .leex になります。

<h1><%= @message %></h1>

モジュール MyAppWeb.SampleLiverender/1 の記述を上で追加した view と template を使うように変更します。

  def render(assigns) do
    Phoenix.View.render(MyAppWeb.SampleView, "show.html", message: "Hello Phoenix.LiveView world!")
  end

:message で与えた文字列が、テンプレートに記述したタグ <h1> で装飾されて表示されることが確認できます。

click をハンドリングする

lib/my_app_web/templates/sample/show.html.leex を次のように変更します。

<h1><%= @message %></h1>
<button phx-click="add" phx-value-char="A">A</button>
<button phx-click="add" phx-value-char="B">B</button>
<button phx-click="add" phx-value-char="C">C</button>
<button phx-click="clear">clear</button>
<div>
  > <%= @str %>
</div>

変更すると右のようにボタンが表示されます。

タグ button に指定した phx-click が click のイベントとして LiveView のモジュールで定義するハンドラ handle_event/3 に渡されます。 また phx-value-* で指定した値がイベントの値として一緒にハンドラに渡されます。

MyAppWeb.SampleLivehandle_event/3 を実際に追加します。

defmodule MyAppWeb.SampleLive do
  use Phoenix.LiveView

  def render(assigns) do
    Phoenix.View.render(MyAppWeb.SampleView, "show.html", message: "Hello Phoenix.LiveView world!", str: assigns.str)
  end

  def mount(_session, socket) do
    {:ok, assign(socket, :str, "")}
  end

  def handle_event("add", %{"char" => char}, socket) do
    str = socket.assigns.str
    new_socket =
      socket
      |> assign(:str, str <> char)
    {:noreply, new_socket}
  end

  def handle_event("clear", _, socket) do
    {:noreply, assign(socket, :str, "")}
  end
end

A, B, C の各ボタンで文字列に文字が追加され、clear ボタンで文字列がクリアされます。

フォームのイベントをハンドリングする

lib/my_app_web/templates/sample/show.html.leex にフォームを追加します。また入力結果を表示するための要素も追加します。 フォームで変更が発生すると phx-change で指定されたイベントが発生します。また submit した場合は phx-submit で指定したイベントが発生します。

<h1><%= @message %></h1>
<button phx-click="add" phx-value-char="A">A</button>
<button phx-click="add" phx-value-char="B">B</button>
<button phx-click="add" phx-value-char="C">C</button>
<button phx-click="clear">clear</button>
<div>
  > <%= @str %>
</div>

<hr />

<form phx-change="update" phx-submit="submit">
  <input type="text" name="text">
</form>
<div>
  > <%= @text %>
</div>
<ul>
  <%= Enum.map(@texts, fn text -> %>
    <li><%= text %></li>
  <% end) %>
</ul>

LiveView のモジュールにハンドラを追加します。

イベント update が発生すると input タグの name で指定した値をパラメータとしてハンドラが呼び出されます。

ここでは input タグに文字列の入力があると、それを反転した文字列を表示し、submit されるとその反転された文字列をリストに追加する処理を追加しました。

defmodule MyAppWeb.SampleLive do
  use Phoenix.LiveView

  def render(assigns) do
    params = [
      message: "Hello Phoenix.LiveView world!",
      str: assigns.str,
      text: assigns.text,
      texts: assigns.texts
    ]
    Phoenix.View.render(MyAppWeb.SampleView, "show.html", params)
  end

  def mount(_session, socket) do
    new_socket =
      socket
      |> assign(:str, "")
      |> assign(:text, "")
      |> assign(:texts, [])
    {:ok, new_socket}
  end

  def handle_event("add", %{"char" => char}, socket) do
    str = socket.assigns.str
    new_socket =
      socket
      |> assign(:str, str <> char)
    {:noreply, new_socket}
  end

  def handle_event("clear", _, socket) do
    {:noreply, assign(socket, :str, "")}
  end

  def handle_event("update", %{"text" => text}, socket) do
    new_socket =
      socket
      |> assign(:text, String.reverse(text))
    {:noreply, new_socket}
  end

  def handle_event("submit", _, socket) do
    new_socket =
      socket
      |> assign(:text, "")
      |> assign(:texts, [socket.assigns.text | socket.assigns.texts])
    {:noreply, new_socket}
  end
end

実行結果です。

ここでは form タグを直接記述したので phx-change, phx-submit という形で記述しましたが、Phoenix.HTML.Form.form_for/3 を利用する場合はドキュメントにあるように、オプションに :phx_change, :phx_submit とハイフンをアンダスコアに置き換えたキーで指定します。

いつか読むはずっと読まない:博物館の後ろ側、あるいは本当の顔

研究者から見た大英自然史博物館の様子を語った一冊。

博物館には一般の来場者の目の届かないところに膨大な資料が保存され様々な人々の営みがあることに目を向けさせられます。

乾燥標本収蔵1号室―大英自然史博物館 迷宮への招待

乾燥標本収蔵1号室―大英自然史博物館 迷宮への招待

へんなものみっけ! (4) (ビッグコミックス)

へんなものみっけ! (4) (ビッグコミックス)

Elixir でビット列を展開する覚書

例えば右のように

11001010(2) = CA(16)

1111000011001100(2) = F0CC(16)

に Elixir で展開するための覚書です。

Elixir の内包表記は for

Elixir にも内包表記があります。他の関数型言語とくらべて内包表記っぽくない表記と感じましたが、意味するとろは確かに内包表記です。

iex> for n <- [1,2,3,4,5], do: n * n
[1, 4, 9, 16, 25]
iex> for x <- [1,3,5], y <- [2,4,6], do: {x, y}
[{1, 2}, {1, 4}, {1, 6}, {3, 2}, {3, 4}, {3, 6}, {5, 2}, {5, 4}, {5, 6}]

ちなみに for/1 はモジュール Kernel.SpecialForms で定義されています。

Elixir (と Erlang)の特徴はバイナリにも内包表記が使える点です。

iex> for <<c <- <<1, 2, 3, 4, 5>> >>, do: c * c                
[1, 4, 9, 16, 25]

個々の演算の値をバイナリにして、:into オプションを利用すれば、結果もバイナリで得ることができます。

iex> for <<c <- <<1, 2, 3, 4, 5>> >>, into: <<>>, do: <<c * c>>
<<1, 4, 9, 16, 25>>

また個々の演算の値がバイナリになっていればよいので、元のバイナリに対して長さの異なる結果を得ることができます。

iex> for <<c <- <<1, 2, 3, 4, 5>> >>, into: <<>>, do: <<c, c>> 
<<1, 1, 2, 2, 3, 3, 4, 4, 5, 5>>

加えて任意のサイズのビットで値を取り出すことができます。

iex> for <<bit::1 <- <<128>> >>, into: <<>>, do: <<bit>>
<<1, 0, 0, 0, 0, 0, 0, 0>>

バイナリはビット数を指定しない場合は 8 ビットで扱われるため、この <<bit>> は 8 ビットの値と解釈されます。 もちろんここでもビット数を指定することができるので、例えば次のようにすると元のバイナリが得られます。

iex> for <<bit::1 <- <<128>> >>, into: <<>>, do: <<bit::1>>
<<128>>

ビット列を展開する

必要な情報がそろったので、これらを踏まえて。

iex> x = 0xCA
202
iex> <<y::16>> = for <<bit::1 <- <<x::8>> >>, into: <<>>, do: << <<bit::1>>, <<bit::1>> >>
<<240, 204>>
iex> Integer.to_string(y, 16)                                                             
"F0CC"

CA(16) から F0CC(16) を得ることができました。

ビット列を文字列に展開する

Elixir の文字列はビット列です。

iex> "" == <<>>
true
iex> <<65>> == "A"
true

つまり文字列に対するいろいろな加工を内包表記を使って書くことができます。

iex> for <<c <- "hello">>, do: Integer.to_string(c, 16)
["68", "65", "6C", "6C", "6F"]
iex> for c <- [0x68, 0x65, 0x6C, 0x6C, 0x6F], into: "", do: <<c>>
"hello"

この仕組みを利用すればビット列を任意の文字列に展開することもできます。

iex> for <<bit::1 <- <<0x5A>> >>, into: "", do: if bit == 1, do: "@", else: "_"
"_@_@@_@_"

これを踏まえて。 フォントデータをキャラクターで表示してみます。

iex> [0x10, 0x28, 0x44, 0x82, 0xfe, 0x82, 0x82, 0x00] |> Enum.each(&IO.puts(for <<bit::1 <- <<&1>> >>, into: "", do: if bit == 1, do: "[]", else: "  "))
      []        
    []  []      
  []      []    
[]          []  
[][][][][][][]  
[]          []  
[]          []  

キャラクタ以外にも、たとえば 0<<0::24>>1<<0xffffff::24>> と展開すれば 2 値のデータから 24 ビットカラーのデータを作成することができます。

補足:Erlang の内包表記

Erlang にもリストとバイナリ両方の内包表記があります。こちらの方がよく見る内包表記の形式をしています。

> [X * 2 || X <- [1,2,3,4,5]].
[2,4,6,8,10]
> << <<(C* 2)>> || <<C>> <= <<1,2,3,4,5>> >>.
<<2,4,6,8,10>>
> [C * 2 || <<C>> <= <<1,2,3,4,5>>].         
[2,4,6,8,10]
> << <<(X * 2)>> || X <- [1,2,3,4,5]>>.
<<2,4,6,8,10>>

いつか読むはずっと読まない:失われたものを復元するという偉業

とうとう新種として判明し命名されました。

2019年9月現在、全身実物化石と全身復元骨格を間近で見ることができます。これはぜひ見て欲しい。

恐竜・古生物ビフォーアフター

恐竜・古生物ビフォーアフター

恐竜の魅せ方 展示の舞台裏を知ればもっと楽しい

恐竜の魅せ方 展示の舞台裏を知ればもっと楽しい

Timex.format のフォーマットの書式覚書

Elixir で日時を便利に操作する定番のパッケージ timex

そこで定義されている Timex.format/2 をいつも忘れてしまいます。

~N[2019-08-09 01:02:03.456789]
|> Timex.to_datetime("Asia/Tokyo")
|> Timex.format("{YYYY}/{0M}/{0D} {h24}:{m}:{s} {Zabbr}") 
# =>{:ok, "2019/08/09 01:02:03 JST"}

と、いうわけで。覚書として一覧にしてみました。

そして。一覧を作成中に Timex.format/3 で formatter に :strftime を指定すると strftime の書式で指定できるということを知りました。

~N[2019-08-09 01:02:03.456789]
|> Timex.to_datetime("Asia/Tokyo")
|> Timex.format("%Y/%m/%d %H:%M:%S %Z", :strftime) 
# => {:ok, "2019/08/09 01:02:03 Asia/Tokyo"}

一つ賢くなった。結果オーライ。

なお。timex には format/2, format/3 の他に、locale を指定できる lformat/3, lformat/4 という関数も用意されています。 こちらの出力も合わせて一覧にしました。

 ~N[2019-08-09 01:02:03.456789]
|> Timex.to_datetime("Asia/Tokyo")
|> Timex.lformat("{YYYY}/{0M}/{0D} {WDfull} {AM} {0h12}:{m}", "ja")     
# => {:ok, "2019/08/09 金曜日 午前 01:02"}
format string example of format example of lformat
YYYY 2019 2019
YY 19 19
C 20 20
WYYYY 2019 2019
WYY 19 19
M 8 8
0M 08 08
Mfull August 8月
Mshort Aug 8月
D 9 9
0D 09 09
Dord 221 221
Wiso 32 32
Wmon 31 31
Wsun 31 31
WDmon 5 5
WDsun 5 5
WDshort Fri
WDfull Friday 金曜日
h24 01 01
h12 1 1
0h12 01 01
m 02 02
s 03 03
s-epoch 1565280123 1565280123
ss .456789 .456789
am am 午前
AM AM 午前
Zname Asia/Tokyo Asia/Tokyo
Zabbr JST JST
Z +0900 +0900
Z: +09:00 +09:00
Z:: +09:00:00 +09:00:00
ISO:Extended 2019-08-09T01:02:03.456789+09:00 2019-08-09T01:02:03.456789+09:00
ISO:Extended:Z 2019-08-08T16:02:03.456789Z 2019-08-08T16:02:03.456789Z
ISO:Basic 20190809T010203.456789+0900 20190809T010203.456789+0900
ISO:Basic:Z 20190808T160203.456789Z 20190808T160203.456789Z
ISOdate 2019-08-09 2019-08-09
ISOtime 01:02:03.456789 01:02:03.456789
ISOweek 2019-W32 2019-W32
ISOweek-day 2019-W32-5 2019-W32-5
ISOord 2019-221 2019-221
RFC822 Fri, 09 Aug 19 01:02:03 +0900 金, 09 8月 19 01:02:03 +0900
RFC822z Thu, 08 Aug 19 16:02:03 Z 木, 08 8月 19 16:02:03 Z
RFC1123 Fri, 09 Aug 2019 01:02:03 +0900 金, 09 8月 2019 01:02:03 +0900
RFC1123z Thu, 08 Aug 2019 16:02:03 Z 木, 08 8月 2019 16:02:03 Z
RFC3339 2019-08-09T01:02:03.456789+09:00 2019-08-09T01:02:03.456789+09:00
RFC3339z 2019-08-08T16:02:03.456789Z 2019-08-08T16:02:03.456789Z
ANSIC Fri Aug 9 01:02:03 2019 金 8月 9 01:02:03 2019
UNIX Fri Aug 9 01:02:03 JST 2019 金 8月 9 01:02:03 JST 2019
ASN1:UTCtime 190808160203Z 190808160203Z
ASN1:GeneralizedTime 20190809010203 20190809010203
ASN1:GeneralizedTime:Z 20190808160203Z 20190808160203Z
ASN1:GeneralizedTime:TZ 20190809010203+0900 20190809010203+0900
kitchen 1:02AM 1:02午前

書式の解釈は、パッケージのコード上でそれぞれ Timex.Parse.DateTime.Tokenizers.Default , Timex.Parse.DateTime.Tokenizers.Strftime というモジュールの map_directive/2 という関数で実装されています。

Timex.Parse.DateTime.Tokenizers.Default.map_directive/2 Timex.Parse.DateTime.Tokenizers.Strftime.map_directive/2