15
5

この記事は、Elixir Advent Calendar 2023 シリーズ13 の13日目です


piacere です、ご覧いただいてありがとございます :bow:

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)以降を参照することをオススメします

image.png

image.png

あと、このコラムが、面白かったり、役に立ったら、image.png をお願いします :bow:

既存の 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

むむ、カッコが曖昧でエラーになりました …

単数行記載が問題あるかもなので、複数行にバラしてみます

lib/basic.ex
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 を書き換えてみようと思います :stuck_out_tongue_winking_eye:

image.png

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"

ビンゴッ :stuck_out_tongue_closed_eyes: :tada:

終わりに

Elixirで、Rubyのような「後置 if」ならぬ「後置 in」をマクロで定義してみました

このように、Elixirは独自の言語仕様をマクロで追加することが出来ます(ただし、二項演算子は使える対象が限られてはいますが)

これを応用すれば、Elixir言語内でのDSL(Domain Specific Language:ドメイン固有言語)も実装できます

たとえば、Ectoschema ~ doExUnittest ~ doMockwith_mock ~ do なんかが代表的な一例で、いずれも実用的で便利なマクロです

今回は、パロディ的な実践的で無いマクロでしたが、業務にフィットしたDSLなんかを今年は披露したいと思います

p.s.このコラムが、面白かったり、役に立ったら…

image.png にて、どうぞ応援よろしくお願いします :bow:

15
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
5