トピック
第14回目となる清流elixirの勉強会を開催しました
今回からは本格的にリモート参加枠を用意した。しかし、これが思っていたよりも何倍も難しい。会場のホットな温度感や議論が始まるとリモート参加者にはその現場感を伝えることが出来ない。トライアンドエラーでよりリモート参加者の方に価値ある時間が提供出来るように創意工夫していきたい
また今回は普段より非常にお世話になっている、福岡を拠点に広く活動されているfukuoka.exさんと合同での勉強会を開催しました
今回で合同の勉強会は2回目となりました。いつも知識を共有して下さり、感謝しかありません...
最近は普段使用している会議室がどこかしらの団体とバッティングしているようで、本当に隔週開催のペースで活動したいものの、中々、上手くいっていない 。金曜日に固定で19:30~使える会場を抑えたいが、何かに依存したくないな〜とも思いつつ、頭を悩ませている
それでも清流elixirの名前が着実に広まってきており、多くの方からお声掛けを頂いている。本当にありがたい限りです
弱い人でも勉強会は開催できる、参加できるってことを広く伝えていきたい。やるかやらないかだけの問題だと思うので...
というか、コミュニテイ人数が一気に増えてて笑うしかない(圧倒的僥倖...!
そろそろ一度は地元、岐阜でElixirの勉強会をやってもいいのかなと思っている
清流elixir-infomation
開催場所: 丸の内(愛知)
参加人数: 8 -> 12 update!!
コミュニティ参加人数 : 21 -> 36 update!
2019/11/16現在
第14回の勉強会について
今回は第13回の勉強会で涙を流すことになったElixirのマクロについて完全に理解するために、この勉強会を開催した
書籍プログラミングElixirの第20章をベースにElixirのマクロについて理解を進めていくことにした。全体の流れは私が以前、予習的にまとめた以下の記事を参照して頂ければと思う
当日も同じように
- ElixirにおけるASTについて
- quoteとunquoteについて
- defmacroについて
- macroを使ってmyifを作る
という流れで理解を進めた。しかしながら、人に解説をすることで自分の理解していない粗の部分が分かった。今回は勉強会を通して自身が深く理解をした点について列挙していこうと思う
評価される順番がマクロは異なる
以前からマクロは遅延評価
でっせという話は聞いていたし、何となく理解していた。なので引数からIO.puts("nice")
のような値を渡しても、その瞬間にnice
が標準出力されることはない
即時に評価されるのであれば、以下の処理がnice
がkey
がtrue
でもfalse
でも出力されるはず
iex(1)> defmodule Sample do ...(1)> defmacro exec(arg1, key) do ...(1)> quote do ...(1)> if unquote(key) do ...(1)> unquote(arg1) ...(1)> end ...(1)> end ...(1)> end ...(1)> end {:module, Sample, <<70, 79, 82, 49, 0, 0, 4, 172, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 137, 0, 0, 0, 15, 13, 69, 108, 105, 120, 105, 114, 46, 83, 97, 109, 112, 108, 101, 8, 95, 95, 105, 110, 102, 111, 95, 95, ...>>, {:exec, 2}} iex(2)> require Sample Sample
key=trueの時
iex(3)> Sample.exec(IO.puts("nice"), true) nice :ok
key=falseの時
iex(4)> Sample.exec(IO.puts("nice"), false) nil
この結果がマクロは即時評価ではなく、遅延評価であることを表している。これが通常の関数定義に用いるdef
であれば、以下のように出力されるからだ
iex(1)> defmodule Sample do ...(1)> def exec(arg1, key) do ...(1)> if key do ...(1)> arg1 ...(1)> end ...(1)> end ...(1)> end {:module, Sample, <<70, 79, 82, 49, 0, 0, 4, 208, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 135, 0, 0, 0, 15, 13, 69, 108, 105, 120, 105, 114, 46, 83, 97, 109, 112, 108, 101, 8, 95, 95, 105, 110, 102, 111, 95, 95, ...>>, {:exec, 2}}
iex(2)> Sample.exec(IO.puts("nice"), true) nice :ok iex(3)> Sample.exec(IO.puts("nice"), false) nice nil
あ、完全に理解した(何も分かってない
さらに評価される順序は異なる
マクロが遅延評価であることは分かったが、さらにマクロは細かく評価される時系列が異なるようだ。それを表しているのが以下のマクロ記述。これは書籍プログラミングElixirの第20章から引っ張ってきたものだ
defmodule MyMacro do defmacro myif(condition, clauses) do do_cal = Keyword.get(clauses, :do, nil) else_cal = Keyword.get(clauses, :else, nil) quote do case unquote(condition) do val when val in [false, nil] -> unquote(else_cal) _ -> unquote(do_cal) end end end end
そもそも、気になっていた事があって、なんでdo_cal
とelse_cal
がquote do
のブロック外にいるのかがよく理解出来ていなかった。quote do
の内部で変数が扱いたいのならunquote
を使えばいいじゃん〜って思うわけだが...
これにはマクロの中(defmacro)でもさらに評価される順番が異なるのではないかという仮説と結果によって、なぜquote doの外に記述しているのかを理解することが出来た
以下の処理の結果を見てみると非常に興味深い。なぜか、nice
は一度のみ出力されている
defmodule MyMacro do defmacro myif(condition, clauses) do do_cal = Keyword.get(clauses, :do, nil) else_cal = Keyword.get(clauses, :else, nil) IO.puts("nice") quote do case unquote(condition) do val when val in [false, nil] -> unquote(else_cal) _ -> unquote(do_cal) end end end end defmodule Foo do require MyMacro def main() do MyMacro.myif(1==2, do: IO.puts("nice"), else: IO.puts("boat")) end end Foo.main() Foo.main() # nice # boat # boat
何故なんだ...しかし、この現象はquote do
の外部(上部)に記述されているコードがquote do
の部分が評価される前に一度だけ、評価されていると考えると自然な結果だと理解することが出来る
一応、偶然かもしれないのでFoo.main()
をめっちゃ呼び出しておく
Foo.main() Foo.main() Foo.main() Foo.main() Foo.main() Foo.main() # nice # boat # boat # boat # boat # boat # boat
つまりdefmacro
の内部でも評価される順番は存在しており、現状の知識理解だと
- quote doの外部
- qiote doの内部
という順になっており、同じdefmacro
の内部であっても時系列が異なることが確認出来た。user
という構文や__before_compile__
のような指定がマクロに指定できる事は、同じマクロであっても評価される時系列が異なるためと考えると存在している理由が分かる
またdo_c
などをquote do
の外部に記述することで、 何が嬉しいかというと
- unquote()をめっちゃ使う必要がなくなる(Keyword.get()の戻り値を何度も使用しようとすると、codeが長くなって可読性が落ちる)
def :nice() do
のような関数定義をことを行うことがquote do
内部で出来ないため、quote do
の外部で宣言しておく必要がある
defmodule Sample do defmacro hoge(func_name) do to_atom = :"func_name" quote do def unquote(to_atom)(num1, num2) do num1 * num2 end end end end
quote do
の内部にあるものは、マクロ実行時に都度、評価されるためパフォーマンスが悪くなるのではないかと考えられる(複雑な計算処理は事前に行なっておくなど)
後日談とマクロは健全
これは参加者の方から頂いた受け売りの知識だが、C言語の場合にはマクロはスコープが切られておらず、副作用を及ぼす可能性が高い(同名の変数を使用してしまうとスコープ内となるため、簡単に上書きが出来てしまう)
しかしElixirの場合はマクロがdefmacro
、quote do
の内部に独自のスコープを持っており、お互いに影響を及ぼすことが無く、影響範囲さえ理解していれば安全に使うことが出来るようだ
闇の深淵を覗いてしまった...
今回の勉強会を通じて、自身のプログラミングの評価順序?のような概念がぶっ壊れた
pythonやjsぐらいしかまともにやってきたことがないので、コードは書かれている順に評価されていくものだと思い込んでいた
しかし、ながらElixirのマクロでは部分的に先に評価されたり、マクロの中でも評価される順序があったりと、頭がおかしくなりそうだ
非常に勉強になった。もう何も怖くない
参考文献
- プログラミングElixir