この記事は、Elixir Advent Calendar 2023 シリーズ13 の13日目です
piacere です、ご覧いただいてありがとございます
Rubyは、下記のように「後置 if
」が書けます
irb> name = "hoge"
irb> id = :id001
irb> name = "piacere" if id == :id001
irb> name
"piacere"
irb> id = :id002
irb> name = "piacere" if id == :id001
irb> name
nil
これをElixirでも追加してみます
なお、こうしたDSL(Domain Specific Language:ドメイン固有言語)は、マクロを使って実装するのが通例なので、書籍「プログラミングElixir」 のP303(第一版だとP243)以降を参照することをオススメします
あと、このコラムが、面白かったり、役に立ったら、 をお願いします
既存の if
式
まず、Elixirの if
式のおさらいです
Elixirの if
式は、戻り値を返すので、name
の再束縛(他言語だと代入に相当だが、厳密にはElixirはイミュータブルなので異なる)は if
の左に書く必要があります
iex> name = "hoge"
iex> id = :id001
iex> name = if id == :id001, do: "piacere"
iex> name
"piacere"
iex> id = :id002
iex> name = if id == :id001, do: "piacere"
iex> name
nil
これは、後置 if
であっても同様です
マクロでオレオレ if
式を実装
「プログラミングElixir」 にも解説ありますが、Elixirのマクロは、
「記載されたコードを条件次第で実行したり、しなかったりを切り替える」
「記載されたコードの構造次第では、全く異なる挙動に入れ替える」
という機能です
早速、オレオレ if
式を実装してみましょう(書籍よりも短縮記載し、どの条件を通ったかをデバッグするように改造しています)
defmodule My do
defmacro if(cond, cls) do
do_cl = cls[:do]
else_cl = cls[:else]
quote do
case unquote(cond) do
v when v in [false, nil] ->
IO.puts("### I'm original if! - false ###")
unquote(else_cl)
_ ->
IO.puts("### I'm original if! - true ###")
unquote(do_cl)
end
end
end
end
iex> name = "hoge"
iex> id = :id001
iex> require My
iex> name = My.if id == :id001, do: "piacere"
### I'm original if! - true ###
iex> name
"piacere"
iex> id = :id002
iex> name = My.if id == :id001, do: "piacere"
### I'm original if! - false ###
iex> name
nil
後置 if
式を実装する
後置 if
式は、一種の「二項演算子」です
なので、まず、「プログラミングElixir」 のP315(第一版だとP243)にある演算子の上書きから、二項演算子の書き換えを試します
足し算を文字列連結に書き換えるマクロと、その実行結果は、こうです(書籍よりも短縮記載してます)
defmodule Op, do: defmacro a + b, do: quote do: "#{unquote(a)}#{unquote(b)}"
iex> 123 + 456
579
iex> import Kernel, except: [+: 2]
iex> import Op
iex> 123 + 456
"123456"
同じことを、if
で定義可能か試してみます
iex> defmodule Ruby, do: defmacro a if b, do: quote do: "#{unquote(a)}#{unquote(b)}"
warning: missing parentheses for expression following "do:" keyword. Parentheses are required to solve ambiguity inside keywords.
This error happens when you have function calls without parentheses inside keywords. For example:
function(arg, one: nested_call a, b, c)
function(arg, one: if expr, do: :this, else: :that)
In the examples above, we don't know if the arguments "b" and "c" apply to the function "function" or "nested_call". Or if the keywords "do" and "else" apply to the function "function" or "if". You can solve this by explicitly adding parentheses:
function(arg, one: if(expr, do: :this, else: :that))
function(arg, one: nested_call(a, b, c))
Ambiguity found at:
└─ iex:38
…
むむ、カッコが曖昧でエラーになりました …
単数行記載が問題あるかもなので、複数行にバラしてみます
iex> defmodule Ruby do
...> defmacro a if b do
...> quote do
...> "#{unquote(a)}#{unquote(b)}"
...> end
...> end
...> end
error: cannot find or invoke local if/1 inside match. Only macros can be invoked in a match and they must be defined before their invocation. Called as: if(b)
└─ iex:39: Ruby.a/1
error: undefined variable "a"
└─ iex:40: Ruby.a/1
** (CompileError) iex: cannot compile module Ruby (errors have been logged)
(elixir 1.16.0) src/elixir_module.erl:185: anonymous fn/9 in :elixir_module.compile/7
iex:38: (file)
どうやら、if
で二項演算子を組むと、a
の部分が読まれず、エラーとなっているようです
「プログラミングElixir 第一版」 のP255(第二版からは記載そのものが消されている)にある下記で拾える演算子群が、定義可能な二項演算子かも知れません … が、最新Elixirでは関数そのものが無くなっていた …
iex> require Macro
iex> Macro.binary_ops
** (UndefinedFunctionError) function Macro.binary_ops/0 is undefined or private
(elixir 1.16.0) Macro.binary_ops()
iex:2: (file)
iex>
古いElixirソースコードから拾ってみたところ、if
は無さそうなので、代わりに in
を書き換えてみようと思います
in
での定義は、ちゃんと通りました
defmodule Ruby do
defmacro a in b do
quote do
"#{unquote(a)}#{unquote(b)}"
end
end
end
上手く行きました
iex> import Kernel, except: [in: 2]
iex> import Ruby
iex> name = 123 in 456
iex> name
"123456"
それでは、後置 if
ならぬ、後置 in
を定義してみましょう
なお、in
を再定義してしまうので、マクロ内でも in
を使うのは避けます
defmodule Ruby do
defmacro cls in cond do
quote do
case unquote(cond) do
false ->
IO.puts("### I'm original in! - false ###")
nil
nil ->
IO.puts("### I'm original in! - nil ###")
nil
_ ->
IO.puts("### I'm original in! - true ###")
unquote(cls)
end
end
end
end
実行してみます
iex> name = "hoge"
iex> id = :id001
iex> import Kernel, except: [in: 2]
iex> import Ruby
iex> name = "piacere" in id == :id001
### I'm original if! - true ###
false
おや? … これはもしかして、in
の後が id
で途切れてしまっている?
iex> name = "piacere" in (id == :id001)
### I'm original if! - true ###
"piacere"
iex> name
"piacere"
ビンゴッ
終わりに
Elixirで、Rubyのような「後置 if
」ならぬ「後置 in
」をマクロで定義してみました
このように、Elixirは独自の言語仕様をマクロで追加することが出来ます(ただし、二項演算子は使える対象が限られてはいますが)
これを応用すれば、Elixir言語内でのDSL(Domain Specific Language:ドメイン固有言語)も実装できます
たとえば、Ecto
の schema ~ do
や ExUnit
の test ~ do
、Mock
の with_mock ~ do
なんかが代表的な一例で、いずれも実用的で便利なマクロです
今回は、パロディ的な実践的で無いマクロでしたが、業務にフィットしたDSLなんかを今年は披露したいと思います