やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

【第14回清流elixir勉強会】Elixirのマクロを完全に理解する会

トピック

f:id:takamizawa46:20191116231318p:plain
elixir-sr.connpass.com

第14回目となる清流elixirの勉強会を開催しました
今回からは本格的にリモート参加枠を用意した。しかし、これが思っていたよりも何倍も難しい。会場のホットな温度感や議論が始まるとリモート参加者にはその現場感を伝えることが出来ない。トライアンドエラーでよりリモート参加者の方に価値ある時間が提供出来るように創意工夫していきたい

また今回は普段より非常にお世話になっている、福岡を拠点に広く活動されているfukuoka.exさんと合同での勉強会を開催しました
今回で合同の勉強会は2回目となりました。いつも知識を共有して下さり、感謝しかありません...

fukuokaex.connpass.com

最近は普段使用している会議室がどこかしらの団体とバッティングしているようで、本当に隔週開催のペースで活動したいものの、中々、上手くいっていない 。金曜日に固定で19:30~使える会場を抑えたいが、何かに依存したくないな〜とも思いつつ、頭を悩ませている

それでも清流elixirの名前が着実に広まってきており、多くの方からお声掛けを頂いている。本当にありがたい限りです
弱い人でも勉強会は開催できる、参加できるってことを広く伝えていきたい。やるかやらないかだけの問題だと思うので...

というか、コミュニテイ人数が一気に増えてて笑うしかない(圧倒的僥倖...!
そろそろ一度は地元、岐阜でElixirの勉強会をやってもいいのかなと思っている

清流elixir-infomation
開催場所: 丸の内(愛知)
参加人数: 8 -> 12 update!!
コミュニティ参加人数 : 21 -> 36 update!
2019/11/16現在

第14回の勉強会について

今回は第13回の勉強会で涙を流すことになったElixirのマクロについて完全に理解するために、この勉強会を開催した
書籍プログラミングElixirの第20章をベースにElixirのマクロについて理解を進めていくことにした。全体の流れは私が以前、予習的にまとめた以下の記事を参照して頂ければと思う

www.okb-shelf.work

当日も同じように

  • ElixirにおけるASTについて
  • quoteとunquoteについて
  • defmacroについて
  • macroを使ってmyifを作る

という流れで理解を進めた。しかしながら、人に解説をすることで自分の理解していない粗の部分が分かった。今回は勉強会を通して自身が深く理解をした点について列挙していこうと思う

評価される順番がマクロは異なる

以前からマクロは遅延評価でっせという話は聞いていたし、何となく理解していた。なので引数からIO.puts("nice")のような値を渡しても、その瞬間にniceが標準出力されることはない
即時に評価されるのであれば、以下の処理がnicekeytrueでも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_calelse_calquote 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の内部でも評価される順番は存在しており、現状の知識理解だと

  1. quote doの外部
  2. 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の場合はマクロがdefmacroquote doの内部に独自のスコープを持っており、お互いに影響を及ぼすことが無く、影響範囲さえ理解していれば安全に使うことが出来るようだ

闇の深淵を覗いてしまった...

今回の勉強会を通じて、自身のプログラミングの評価順序?のような概念がぶっ壊れた
pythonやjsぐらいしかまともにやってきたことがないので、コードは書かれている順に評価されていくものだと思い込んでいた

しかし、ながらElixirのマクロでは部分的に先に評価されたり、マクロの中でも評価される順序があったりと、頭がおかしくなりそうだ
非常に勉強になった。もう何も怖くない

参考文献

  • プログラミングElixir

【メタプログラミング入門】Elixirのマクロに非黒魔術士が挑む

マクロは黒魔術

以前からメタプログラミングという単語を耳にしたことはあり、存在は認知していた。マクロはメタプログラミングを行うための機能、言語という意味で捉えれば良い(マクロ in メタプログラミングという階層になっている)
要するに言語拡張のために使う言語に用意された機能というわけだ。しかし、言語の仕様を気軽に書き換える・追加してしまうことが可能なため仕様には注意を払わねばならない
拡張をしすぎた結果、誰も仕様を追えなくなってしまうということが起こり得る。これがマクロが黒魔術と呼ばれる理由である

それを理解した上で、言語拡張を行いたい場合やOSSソースコードを読もうと思う時にマクロの知識が必要になってくる(勉強会でOSSのコードを読んで撃沈した。マクロ難し杉内)

マクロに惨敗した勉強会のレポートはこちらです

www.okb-shelf.work

今回はgumiの幾田さんのqiita記事を参考にしながら、プログラミングElixirのマクロの章(20章)を読み進めていく形にした。要は情報を組み合わせただけですが、惨敗した勉強会の当日の自分でも分かるようにまとめたつもりだ

プログラミングElixir読んだけど、Elixir Schoolのメタプログラミング読んだけど、よく分からなかった方を自身含めて少しでも救済できればと思います

マクロの仕組み

Elixirはマクロを用いてAST(抽象構文木)というデータ構造をいじるという操作を行う。ASTという単語をコンピューターサイエンスを集中的に勉強した時にお目にかかったことがある。一言でいえば、ASTというのは記述したコードをパースして生成される処理をまとめたデータ構造だ
ElixirにおけるASTは3つの要素をもつタプルとして表現されている

{関数名, メタデータ, 引数} -> {atom, keyword list, list}

先出しとなるが、quoteを使うことでASTとなったコード表現を確認することが出来る。ためしにIO.puts("nice")のASTを確認してみよう
確かに3つの要素で構成されたタプルとなっている

iex(2)> quote do IO.puts("nice") end
{{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [], ["nice"]}

もう少しだけ、複雑な処理のASTを確認してみる。Enum.mapで10までの数値を倍にしてみる

iex(3)> quote do Enum.map(1..10, fn num -> num * 2 end) end
{{:., [], [{:__aliases__, [alias: false], [:Enum]}, :map]}, [],
 [
   {:.., [context: Elixir, import: Kernel], [1, 10]},
   {:fn, [],
    [
      {:->, [],
       [
         [{:num, [], Elixir}],
         {:*, [context: Elixir, import: Kernel], [{:num, [], Elixir}, 2]}
       ]}
    ]}
 ]}

何かたくさん出てきた。しかし、よく見てみると、上記の処理に使用した演算子を所々に確認することが出来る
つまりは、このコード表現をゴチャゴチャといじってやれば処理の内容を変更できるということになる(例えば*+に変えるとかね)
そのためにはquoteunquoteの知識が不可欠になる

quoteとunquoteについて

quoteについて

先ほどチラりとquoteが何をするものか見せたが、改めて言葉で書き出すと、ElixirのコードをASTの形に変換するための構文になる じゃあ、このquoteをいつ使うのという話になるが、現状の理解で処理の通り、コード表現をASTに変換する必要がある時だ

その前にdefmacroについて触れておく。defmacrodefmodule内でのみ使用することが可能で、引数で受け取った値をASTの形式として解釈をして内部のASTをElixirでの実行結果として返す。以下の処理の違いをみれば、どんな値がreturnされているのかが目に見て分かる
まずは受け取っている値から確認する

iex(8)> defmodule Sample do
...(8)>   defmacro arg_val(val) do
...(8)>     IO.inspect(val)
...(8)>     quote do
...(8)>       unquote(val)
...(8)>     end
...(8)>   end
...(8)> end

iex(9)> require Sample
Sample
:
:
iex(12)> Sample.arg_val(%{"key" => "nice"})
{:%{}, [line: 12], [{"key", "nice"}]}
%{"key" => "nice"}

次にどんな値を返すのか見てみよう

iex(1)> defmodule Sample do
...(1)>   defmacro in_quote() do
...(1)>     quote do
...(1)>       Enum.map(1..10, fn num -> num * 2 end)
...(1)>     end
...(1)>   end
...(1)>   defmacro not_in_quote() do
...(1)>     Enum.map(1..10, fn num -> num * 2 end)
...(1)>   end
...(1)>   def normal() do
...(1)>     quote do
...(1)>       Enum.map(1..10, fn num -> num * 2 end)
...(1)>     end
...(1)>   end
...(1)> end
{:module, Sample,
 <<70, 79, 82, 49, 0, 0, 6, 156, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 201,
   0, 0, 0, 18, 13, 69, 108, 105, 120, 105, 114, 46, 83, 97, 109, 112, 108, 101,
   8, 95, 95, 105, 110, 102, 111, 95, 95, ...>>, {:not_in_quote, 0}}

# macroを使用するためにrequireを使う
iex(2)> require Sample
Sample

defmacro内でquoteを使用した場合

iex(3)> Sample.in_quote()
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

defmacro内でquoteを使用していない場合

iex(4)> Sample.not_in_quote
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

def内でquoteを使用した場合

iex(5)> Sample.normal
{{:., [], [{:__aliases__, [alias: false], [:Enum]}, :map]}, [],
 [
   {:.., [context: Sample, import: Kernel], [1, 10]},
   {:fn, [],
    [
      {:->, [],
       [
         [{:num, [], Sample}],
         {:*, [context: Sample, import: Kernel], [{:num, [], Sample}, 2]}
       ]}
    ]}
 ]}

この場合にだけ、ASTの状態で値がreturnされた。なるほど... 何となく違いが分かってきたような気がする

unquoteについて

先ほど使用したquoteのブロック内部に変数を使用しようとすると以下のような結果になってしまう

iex(1)> word = "nice"
"nice"
iex(2)> quote do: word
{:word, [], Elixir}

ありゃ、変数がquoteのブロック内部では上手く値が読み込まれない模様。どうすれば...
こんな場合に使用するのがunquoteの構文だ

iex(3)> quote do: unquote(word)
"nice"

こんな感じでquoteとの組み合わせでそれっぽいことが出来た
つまりは用途としてはマクロ作成時にElixirのsyntaxを使う中で変数を扱いたい時(正しくはコードを差し込みたい)にquoteと組み合わせて使えば良い

iex(81)> quote do: IO.puts(unquote(word))
{{:., [],
  [{:__aliases__, [alias: false], [:IO]}, :puts]}, [],
 ["nice"]}

黒魔術(マクロ)入門

プログラミングElixirを参考にmyifの作成をしてみよう。基礎知識を身につけたはずなので後はコードを理解するだけ
do: __, else: __の値を渡り方と取得については以下を参考にしてほしい

iex(17)> tmp = fn val -> Keyword.get(val, :do, nil) end
iex(18)> tmp.(do: "nice", else: "boat")
"nice"

さっそくマクロを書いてみる(マルパクリ)

defmodule MyMacro do
  # 全ての引数がASTとして受けられる
  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はunquoteすることなく文字列として扱い実行時には変数名になる)
        val when val in [false, nil] -> unquote(else_cal) # false時の処理
        _ -> unquote(do_cal) #true時の処理
      end
    end
  end
end

defmodule Main do
  require MyMacro
  def main() do
    MyMacro.myif(1==2, do: IO.puts("nice"), else: IO.puts("boat"))
  end
end

では、実行してみる

iex()> Main.main()
boat

おお、いいですね。それっぽいことが出来た。ちなみに今更だが、defmacroの引数として受け取っている値は受け取り時に評価されずにマクロの実行時に評価される。そのためIO.puts("nice")IO.puts("boat")の値が受け取り時に評価されていない(標準出力されていない)ということになる
普通に引数にIO.puts()何かを渡すと以下のように動作してしまう

iex(20)> normal = fn _, exec -> Keyword.get(exec, :do, nil) end
#Function<12.128620087/2 in :erl_eval.expr/5>
iex(21)> normal.(1==2, do: IO.puts("nice"), else: IO.puts("boat")) |> IO.inspect()
nice
boat
:ok
:ok

プログラミングElixirの練習問題に挑む

程よいレベル感の問題が付属されていたので、チャレンジしてみた。関数として扱いたい場合はアトムにするという点で若干詰まった
20分程で理解出来たが、こんなことが出来ていいの?という驚きは隠せない。マクロは用法用量を守って正しく使うべきだ

defmodule Times do
  defmacro times_n(num) do
    str_num = Integer.to_string(num)
    func_name = :"times_#{str_num}"
    quote do
      def unquote(func_name)(val) do
        unquote(num) * val
      end
    end
  end
end

defmodule Test do
  require Times
  Times.times_n(3)
  Times.times_n(10)
end

実行結果

iex(25)> Test.times_
times_10/1    times_3/1

iex(25)> Test.times_3(3)
9
iex(26)> Test.times_10(10)
100

参考文献

人生で初めてLTしてきたので後日談【よわよわな新卒が勉強会を始めた話】

後日談とは(1週間以上経ってる

f:id:takamizawa46:20191109202024p:plain:w450

いつもお世話になっているからpiacereさんから声を掛けて頂き、人生で初めての自身のネタでのLTかつリモートLTをしました
fukuokaex.connpass.com

今回話したネタは「清流elixirのこれまで -東海にElixir界の異端児が生まれた話」と称して、なぜプログラミング経歴が2年にも満たないよわよわな新卒のキャリアも学もない僕が東海地方で勉強会を開催しているのかという話をした(しかもElixirの)

とりわけ、特別なことをやっているわけではないので経験談を語るだけの形になりましたが、たくさんお褒めの言葉を頂けて光栄の限りです
今後も可能な限り、このペースを保って勉強会を開催していきたい

当日に使用したスライドはこちらから閲覧できます

www.slideshare.net

初LTの率直な感想

セミナーという形で何度か今まで業務の中で登壇させて頂いたことはあるが、いずれにしろテーマというものが事前に決まっているので、今回自身で初めてLTをするということになりテーマを決めて、内容の下地を考えるのにかなり時間を費やした

「これまで」という便利な単語でタイトルを決めたところで、ただただ経緯を話すだけだと、恐らく全然面白くないものが出来上がる。サプライズとなる場面をいかにして切り出そうかと意識をして作ったつもり。個人的に大きかったのはやはり、「全く知識のない人が勉強会をはじめたよ」という話を聞いたということだ

研究室時代の名残もあり、個人的なルールとしてLTやプレゼンの練習は極力行わないようにしている。これは当日、記憶喪失になってもプレゼンができるように資料を作り込むという意識を高めるためにやると、お世話になった研究室で教わった
パワポの極意的なものを偉そうにまとめたことがあるので、「なんだこいつ」と思ってみて頂けると嬉しい

www.okb-shelf.work

ただ、緊張というものはどうしてもしてしまう。さらにリモートLTなんていう形だったので、どういう顔をすればいいか分からず綾波もビックリの引き笑いをしてしまっていたかもしれない

話したかったことは話せたのでヨシ!としておく

あなたがLTをするべきかどうかについて

したほうがいい。情報は拡散して頂けるし、情報というものは意識的に集める人と、情報を自身で提供できる人の所に集まってくる
以下は引用。これは尊敬する「まつもとゆきひろ」先生のお言葉です

インプットは必要、でも差別化要因にならない
しかし、
アウトプットすることで差別化になる

自身の言葉で言い換えると、「インプットは割とだれでも出来るけどアウトプットできる人は一握りなので差別化要因になるで〜」といった感じ
こうやって褒めて頂けると一層やる気が出る...もっと褒めてくれ(福田

なので苦労をしてでも、どんなレベルのものであっても、まずはアウトプットをしてみることは非常に重要。恥をかいた分だけ強くなれるはず

ある程度をアウトプットを続けて得られた恩恵

これすごくないですか。僕がフリー素材で30分ぐらいでXD使って作ったバイアスかかりまくりの清流elixirのロゴが日本のElixirコミュニティとして掲載されています

どんな形であれ、何でも続けてアウトプットするのが大切なんやなーって再度、実感
もっと強くなりたいのでアウトプット頑張らねば...

参考文献

【まつもとゆきひろ】20代のためのプログラマー勉強方法を聞いてきた。

【レポート】第13回清流elixir勉強会を開催しました【Elixirで作成されたOSSのコードを読んでみる会】

トピック

elixir-sr.connpass.com

第13回目の清流elixirの勉強会を開催しました
今回はOSSのコードを読んでElixir力をあげるという目的かつ、弊コミュニティ初の試みとしてリモート(Zoom)での参加にも対応しました
これで全国からのジョインが可能に!! 勉強会の当日は東海と九州が繋がり、事実上の中部から西日本を制覇したことになった。すごい

初のリモート開催のためZoomに手馴れておらず、バタついてしまいました。申し訳ありません。次はもっとスマートに出来るかと思いますので多めに見て頂きたいですm( )m

清流elixir-infomation
開催場所: 丸の内(愛知)
参加人数: 5 -> 8 update!! コミュニティ参加人数 : 16 -> 21 update!
20191026現在

第13回の勉強会の内容について

コードを早く、美しく形に出来るようにするためには、コードを書く経験を積むことも重要だが、達人のコードを読むという経験も非常に重要だと考えている。Elixirの基礎的な文法に関してはある程度、習得が完了したはずなので、Elixirを使って書かれたOSSのコードで実際にどのような記述が現れてくるのかを、1人だと辛いので、集まって読んでみるかという流れに

時間の都合もあるので、コード量が多すぎず、コード量が少なすぎず、かつOSSの内容が比較的分かりやすいものを用いたいということで、私の独断と偏見でTrotというphoenixとは異なるmicro web frameworkのOSSを選択しました

github.com

まず先に得られた結果・知見から

...

俺の知っているElixirと違う...

なんじゃこれ。知らないことばっかりなんですが、それは..
syntaxに関しては概ね把握しているつもりであったし、Enumでパイプ使って脳汁ブシャーッってのも気持ちよく出来るレベルなんで、まぁ読めるやろと思ってたけど、自分のレベルを思い知らされた

特に苦労したのがmacroに関しての理解。存在は知っているし、プログラミングElixirでも扱われているトピックだったので、うっすら頭に知識があるはずだが実際のところ黒魔術的要素が強いのでユースケースまでが想定できていなかった

Trotの内部の至る所にmacroがふんだんかつ、おしゃれに使われており、頭が????となった
最初に遭遇した頭を悩ませた問題のコードは以下になる

./trot/lib/trot/trot.exより抜粋

# module変数 -> 許可するmethod
  @http_methods [:get, :post, :put, :patch, :delete, :options]

  @doc """
  Returns a boolean indicating whether the passed in atom is a valid HTTP method.
  """
  defmacro is_http_method(thing) do
    quote do
      # unquote(module.func) |> is_atom()
      # get, post, patch, delete
      # unquote -> thing -> 実体化(http_methodsのどれかであれば)
      # return -> boolean
      # Q: macroの使うタイミングむずすぎ問題 -> if(macro!!), for(macro!!), def(macro), defmacro(macro) ?????
      is_atom(unquote(thing)) and unquote(thing) in unquote(@http_methods)
    end
  end

このis_http_method(thing)というものがboolの値を返していることは何となく分かるが、なぜここでmacroなのかという疑問は残る
普通にis_atom()の判定と@http_methodsに引数の値が含まれているかどうかが分かれば良いだけではないのか?
なので以下のように書き直しても問題ないはずだが、macroが実行時評価になるという話が関係するのだろうか、いずれにしろ現段階では弱すぎて理解が出来ていないのでレベル上げをしないといけない

defmodule Sample do
  @http_methods [:get, :post, :put, :patch, :delete, :options]
  def is_http_method(thing) do
    is_atom(thing) and thing in @http_methods
  end
end

Sample.is_http_method(:get) |> IO.puts()
# true

このSampleを書いて、TrotのREAD.MEにあるエンドポイントのサンプルを見てみて1つ思ったことは、このget "/hello", do: "hello"というものがmacroでsyntaxを定義しており、この記述が解釈されてAST(構文抽象木)に変換(elixirの場合は3つの値をもつタプルだった気がする)されるみたいな話しが絡んでくるんだろうなぁと。勉強しよう

ちなみに先ほどのdefmacroはこんな感じで呼び出せた

defmodule Trot do
  @http_methods [:get, :post, :put, :patch, :delete, :options]

  @doc """
  Returns a boolean indicating whether the passed in atom is a valid HTTP method.
  """
  defmacro is_http_method(thing) do
    quote do
      # unquote(module.func) |> is_atom()
      # get, post, patch, delete...
      # unquote -> thing -> 実体化(http_methodsのどれかであれば)
      # return -> boolean
      # Q: macroの使うタイミングむずすぎ問題 -> if(macro!!), for(macro!!), def(macro), defmacro(macro) ?????
      # IO.puts(thing)
      IO.puts(unquote(thing))
      is_atom(unquote(thing)) and unquote(thing) in unquote(@http_methods)
    end
  end
end

defmodule Call do
  import Trot
  def exec(), do: is_http_method(:get)
end

Call.exec() |> IO.puts()
# true

知らない機能がたくさんある

Elixirってこんな風になってんの、そんなことができるのって機能が多すぎて驚いた
iexを立ち上げた状態でh Enum(hのあとはモジュール名とかsyntaxとか)としてやるとEnumに関するhelpを確認することが出来る

                                      Enum                                      

Provides a set of algorithms to work with enumerables.

In Elixir, an enumerable is any data type that implements the Enumerable
protocol. Lists ([1, 2, 3]), Maps (%{foo: 1, bar: 2}) and Ranges (1..3) are
common data types used as enumerables:

    iex> Enum.map([1, 2, 3], fn x -> x * 2 end)
    [2, 4, 6]
    
    iex> Enum.sum([1, 2, 3])
    6
    
    iex> Enum.map(1..3, fn x -> x * 2 end)
    [2, 4, 6]
    
    iex> Enum.sum(1..3)
    6
    
    iex> map = %{"a" => 1, "b" => 2}
    iex> Enum.map(map, fn {k, v} -> {k, v * 2} end)
    [{"a", 2}, {"b", 4}]

同じようにh defってやってみるとdefってのがmacroで作られていることが分かる

 defmacro def(call, expr \\ nil)                         

Defines a function with the given name and body.

さらに驚いたのがh defmacroと記述した時。defmacroがdefmacroによって作られている。なんだこれは笑

defmacro defmacro(call, expr \\ nil)  

macroでめちゃ盛り上がる様子

つまりは、Elixirのコードのどこかに本物のdefmacroがいるということになり、今日一上がった。defmacrodefmacroを作っている。うーん。厨二病、素晴らしい...!!

Elixirのsyntaxは多くがmacroで記述されており、ElixirのKernel.exというファイルを見れば確認することが可能だ

github.com

IExとEEx

今まで知ることも触れることもなかったものだ
IExはElixirのiexに関する関数が扱えるようで、IEx.started?とするとiexが立ち上がっているのかどうかがbool値で判定することが出来る
特に感動したのがEExの方で、このEExを使えば色々と面白いことが出来そうだ

EEx.eval_file(ex_file_path)とすると.exに記述されているコードを文字列として取得することが出来る

iex> EEx.eval_file("mix.exs")
"defmodule Trot.Mixfile do\n 
:
:
\"https://github.com/hexedpackets/trot\"},\n     files: ~w(mix.exs README.md LICENSE lib VERSION)]\n  end\nend\n"

当然、文字列なので|>を使ってStringモジュールの関数を使用することが出来る

iex> EEx.eval_file("mix.exs") |> String.at(3)
"m"

あと1つ文字列からコードを実行する関数があったが、何ていう名前だったのかど忘れしてしまった...

福岡のエンジニア強すぎるって

fukuoka.exの発足人のpiacereさんがElixirに関して知らないことがなくて、もう笑うしかない
twitter.com

東海(広すぎるので本当は岐阜と愛知)にElixirの知識をリモートで届けてくださり感謝しかありません
また今回リモートで参加してくださった方は皆さん、福岡の方(間違いありましたら申し訳ありません)で、いかに福岡でElixirに対して注目が集まってるのかが分かるし、議論している話のレベルも高くて驚くばかりだ...

良い感じで悔しい気持ちになれたのでホットなまま、維持していきたい。やる気がめっちゃ出た

総評

コードを一緒に読むのいいですね。自分の弱さ、何が分かっていないかが分かったので良しとする。まだまだ知らないことがあるんやなと高ぶった
こういう思いが出来るのも清流elixirを開催しある程度、継続してきたからだと身にしみる...

なんと10/30(水)にfukuoka.exさんが開催される秋のLT大会にリモートLTという形で登壇させて頂くことになりました!!
リモートLTって凄いな。初めて聞きました笑
半ば強引に枠に入れて頂けたようで恐縮です、ありがとうございます

fukuokaex.connpass.com

「清流elixirのこれまで。なぜ東海でelixirをやるのか」みたいなタイトルでLTします。資料はどこかにアップしようと思うので、ごひいきに〜

Elixirでプロセスをspawnで生成してErlangの:random.uniformを呼び出すと全て同じ値になる

何が起こったのか

まず、プロセスを複数立ち上げて並行で処理をしようと考えた。その処理の過程の中でErlang:random.uniform()(ElixirからErlangのモジュール関数を呼び出す際には:をつける)を呼び出して実行してみると、何と全て同じ値になっているではないか!!

ランダムに生成したidをPOST経由で送信した際にuserが1人しか生成されていないので不思議に思って、デバッグをしている際に気づいた
以下は、問題を再現するために記述したコードになる

Enum.map(1..10, fn num ->
  spawn(fn ->
    num = :random.uniform()
    IO.puts(num)
  end)
end)

このコードをiex起動して実行してみると、全て同じ値が出力されることが確認できる

[#PID<0.722.0>, #PID<0.723.0>, #PID<0.724.0>,
 #PID<0.725.0>, #PID<0.726.0>, #PID<0.727.0>,
 #PID<0.728.0>, #PID<0.729.0>, #PID<0.730.0>,
 #PID<0.731.0>]
0.4435846174457203
0.4435846174457203
:
:
0.4435846174457203
0.4435846174457203

なぜ〜

Erlangの公式ドキュメントをふと読んでみることに

答えは全てそこにあった。公式 is GOD
以下は引用になります

Data Types
ran() = {integer(), integer(), integer()}
The state.

uniform() -> float()
Returns a random float uniformly distributed between 0.0 and 1.0, updating the state in the process dictionary.

ここから分かることは、ランダム値の生成にprocess dictionaryに保存されているData Typesの値を利用しているということだ
つまり、このstateの値(seed値)が全てのプロセスで重複してしまっているために、同じ値が生成されているのではないかと考えられる

さらにページ下部から決定的な記述を発見したので引用

Some of the functions use the process dictionary variable random_seed to remember the current seed.
If a process calls uniform/0 or uniform/1 without setting a seed first, seed/0 is called automatically.

ふむふむ、要は:random.uniform()を呼び出した際に、最初にseed値を設定するために、seed/0を内部的に呼び出しているようで、その際にseed値をセットしているもしくは、process dictionaryからseed値を読みこんでいると考えることが出来る

そうであれば、同じ値が生成されるという現象を説明することが出来る
Erlangのコードは読み慣れていないが、該当する部分のコードを発見できたので確認しておこう

-spec uniform() -> float().

uniform() ->
    {A1, A2, A3} = case get(random_seed) of
               undefined -> seed0();  %% ここでseed0()を呼び出している!! 
               Tuple -> Tuple
           end,
    B1 = (A1*171) rem ?PRIME1,
    B2 = (A2*172) rem ?PRIME2,
    B3 = (A3*170) rem ?PRIME3,
    put(random_seed, {B1,B2,B3}),
    R = B1/?PRIME1 + B2/?PRIME2 + B3/?PRIME3,
    R - trunc(R).

なるほど、やはりそういうことだね
ということは別プロセスを立ち上げた際にこのseed値を変えてあげれば良さそう

清流elixir常連参加のりきくんにも助けて貰いました。圧倒的感謝ッ!!


問題を解決したコード

ということで以下のコードに変更したところ、無事にそれぞれのプロセスで異なる値が生成されるのを確認できた seed値の設定値については気にしてはいない。まずは結果が変化することを確認することが優先だ

base_num = :random.uniform(1000)
Enum.map(1..10, fn num ->
  spawn(fn ->
    :random.seed(num+base_num, num+base_num, num+base_num)
    num = :random.uniform()
    IO.puts(num)
  end)
end)
[#PID<0.766.0>, #PID<0.767.0>, #PID<0.768.0>,
 #PID<0.769.0>, #PID<0.770.0>, #PID<0.771.0>,
 #PID<0.772.0>, #PID<0.773.0>, #PID<0.774.0>,
 #PID<0.775.0>]
0.7457427656808528
0.7626736718805098
:
:
0.8811900152781076
0.8981209214777643

ヨシ!

おまけ。Erlangの関数を使用せずにランダム値を生成する

面倒なのでEnum.random()を使って乱数を擬似的に作成してあげれば良い
基本形は以下の通り。rangeもenumerableなので記述するのが楽だ

iex> Enum.random(1..100)
47

少数にしたい場合は任意の10n(希望の桁数)で割ってやれば良い感じになる

iex> Enum.random(1000..1000000) / 1000000
0.534401