やわらかテック

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

Elixrのプロセスとは一体何なのか。探索の旅に出てた

今更だけど

Node間通信のチャットサーバーを作っている時にふとなぜか、「Elixirでspawnとかした後に返ってくるPIDって一体何なんだ?」と思い立った
なぜsend(target_pid, message)とするだけで対象のプロセスにdataを送ることが可能なのだろか。Node間通信をしている最終に気になってしまったので調べた結果を個人的メモとしてまとめておく

実際にPIDを見てみよう

PIDとは「Process ID」の略語でプロセスを認識するための固有IDなのかなと間違えてしまいそうだが、実際には「Process Identifier」の略語であり、プロセスの識別子、つまりはプロセスを識別するために割り振りされた値という捉え方をすることが出来る

以下のコードをiexを立ち上げて実行することでPIDの存在を確認することが出来る

iex(1)> self()
#PID<0.105.0>

この値がPID。ここに表示されたPIDは現在、立ち上がっているiexに割り振られたPIDになる。すなわち、iexもプロセスとして動いているということになる。次は新規に生成したプロセスのPIDを確認してみる

iex(3)> spawn(fn -> IO.puts("nice") end)
nice
#PID<0.109.0>
iex(4)> self()
#PID<0.105.0>

spawnを使って生成したプロセスのPIDself()で確認したPIDが異なっていることが確認できる。すなわち、新たなる別のプロセスの生成に成功しているということだ。では、実際のこのプロセスが何者なのかを追っていく

PIDを追う旅の始まり

僕がPIDに関して疑問を持ったのはNode.self()の戻り値がPIDだったからだが、先ほどself()PIDが確認出来たので、そこから追っていこう

h spawn/1としてhelpを確認したところ、spawnProcessというモジュールに実装されているそうだ
こちらがspawn/1に該当するコード。defdelegateって何じゃい。しかも、引数が2つある。どういうことだ...

@spec spawn((() -> any), spawn_opts) :: pid | {pid, reference}
defdelegate spawn(fun, opts), to: :erlang, as: :spawn_opt

defdelegateについて

まずはdefdelegateから調べてみよう。ぐぐったらElixirKerneldefdelegateを発見。見た感じ、別のモジュールに用意されている関数を自身で定義しているモジュールの関数として振る舞わせることが可能なようだ

無事にEnumに実装されているrandomの関数に処理が割り当てられているようだ。Erlangの関数も無事に呼び出せるところを見ると、先ほどのspawnに割り当てられている処理はErlangspawn_optをcallしているはずだ

defmodule Sample do
  defdelegate random(list), to: Enum
  defdelegate my_random(list), to: Enum, as: :random
  defdelegate my_sleep(time), to: :timer, as: :sleep
end

Sample.random([1,2,3]) |> IO.puts()
Sample.my_random([1,2,3]) |> IO.puts()
Sample.my_sleep(1000) |> IO.puts()

# 2
# 1
# ok

spawn_opt

次にErlangドキュメントのspawn_optを見に行こう。Erlangのドキュメントの目次のproc_libの項目からspawn_optを発見出来た

  • spawn_opt/2 -> spawn_opt(Fun, SpawnOpts) -> pid()
  • spawn_opt/3 -> spawn_opt(Node, Function, SpawnOpts) -> pid()
  • spawn_opt/4 -> spawn_opt(Module, Function, Args, SpawnOpts) -> pid()
  • spawn_opt/5 -> spawn_opt(Node, Module, Function, Args, SpawnOpts) -> pid()

ドキュメントの文言通り、optionを用いて新たなプロセスを生成するで〜とのこと。ふむふむ、この関数で間違いないだろう

Spawns a new process and initializes it as described in the beginning of this manual page. The process is spawned using the spawn_opt BIFs.

この関数はどれもpid()という値を返している。次はこのpid()を探しに行こう

Erlangのドキュメントを見たところData Typesの項目にPidの項目があるのだが、実際に内部のデータがどうなっているかという記述はなく、詳しいことはProcessを見て下さいとある。しかし、ProcessのページでもPIDの内部に触れている部分はなかった

pid()はいずこに

探しに探し、このpdfファイルの引用からヒントを得て、対象のファイルを見つけることが出来た
Erlang and Elixir for Imperative Programmers

https://github.com/erlang/otp/blob/maint/erts/emulator/beam/erl_term.h#571

現在はErlangディレクトリ構造やファイル内容が更新されていて、パスは変わっている。こちらが現在、erl_term.hが格納されているディレクトリのパスになる

github.com

そこに記述されているのがこのコメント。どうやらbinaryでデータを持たせているようだ

/*
 *  PID layout (internal pids):
 *
 *   |3 3 2 2 2 2 2 2|2 2 2 2 1 1 1 1|1 1 1 1 1 1    |               |
 *   |1 0 9 8 7 6 5 4|3 2 1 0 9 8 7 6|5 4 3 2 1 0 9 8|7 6 5 4 3 2 1 0|
 *   |               |               |               |               |
 *   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *   |n n n n n n n n n n n n n n n n n n n n n n n n n n n n|0 0|1 1|
 *   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *
 *  n : number
 *
 *  Very old pid layout:
 *
 *   |3 3 2 2 2 2 2 2|2 2 2 2 1 1 1 1|1 1 1 1 1 1    |               |
 *   |1 0 9 8 7 6 5 4|3 2 1 0 9 8 7 6|5 4 3 2 1 0 9 8|7 6 5 4 3 2 1 0|
 *   |               |               |               |               |
 *   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *   |s s s|n n n n n n n n n n n n n n n|N N N N N N N N|c c|0 0|1 1|
 *   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *
 *  s : serial
 *  n : number
 *  c : creation
 *  N : node number
 *
 */

何だこれ。全くわからん。websocketの実装をRFCで覗いたことがあるが、その時にもpayloadの内訳がこんな感じで定義されていた。電話線でのやり取りで扱われるデータも同様だ。つまりは、最大32bitの固定長のbinaryの枠に何かしらの情報を格納したものをPIDとして扱っているということだろう。この内訳が何なのかを調べていたところ、以下の質問をstackoverflowにて発見した(古い質問なので情報が古い可能性が高い)

stackoverflow.com

この回答から推測するにPID layoutは28bitから32bitのエミュレーターとなっており、そのエミュレーター<A.B.C>という、まさにElixirPIDを確認した時のように(PID<0.109.0>)構成されているということ
確かにPID layoutは最大32bitsの枠を用意している。次にA, B, Cが何を指しているのかを確認してみる。回答を翻訳すると

  • A -> 0はローカルのノード。もしくは任意のリモートノード番号
  • B(15bits) -> プロセス番号の最初の15bit
  • C(16-18bits) -> MAXPROCSに達するたびに増加するシリアル

とのこと。うーん、よく分からない。そういう風にErlangで枠を定義しているってことは理解した。つまりは、電話線やプロトコルのようにbinaryの枠にデータを定義してプロセス同士、もしくはErlang自体がやり取りをしているということだろうか?
この説明を見ただけではしっくりこなかったので流浪していたところ、Erlangの公式ドキュメントにて以下のページを見つけた

erlang.org

ページトップの文言を引用して、翻訳してみると

The external term format is mainly used in the distribution mechanism of Erlang
external term formatは主にErlangの配布機構(おそらくmessage passingのことを指す)にて使用されています

このページでビンゴだろう。それにPIDに該当するであろうNEW_PID_EXTというformatを発見することが出来た

1 N 4 4 4
88 Node ID Serial Creation

それぞれの項目に対する説明を軽く翻訳してみた

  • Node -> ATOM_UTF8_EXT, SMALL_ATOM_UTF8_EXT or ATOM_CACHE_REFを使ってエンコードされた発信元の名前
  • ID -> A 32-bit big endian unsigned integerを用いて表現。15bitsが重要で残りは0になる
  • Serial -> A 32-bit big endian unsigned integerを用いて表現。13bitsが重要で残りは0になる
  • Creation -> A 32-bit endian unsigned integerを用いて表現。同じNodeから送信される識別子は同じ作成を持つ必要がある。この値を用いてcrashしたNodeと新しいNodeを区別することが可能。値0はデバッグに用いるため、使用しないで下さい

だそうです。何となく意味は分かるが、より深い理解をするためにはErlangの深い理解が必要なようだ。ここから先はまた深い知識が必要になりそうなのでこの記事では一旦、ここまでとしておこう。現状の自分ではここでギブアップだ。来年、同じものを見た時に少しでも理解できるようになってはいたい

まとめ

今回の調査で分かった事はElixirPIDErlangで定義されている最大32bitのbinaryであり、この定義されたbinaryを使って電話線やプロトコルのようにやり取りをしているんだろうなーっていうこと

_人人人人人人人人人人人人人_
> PID完全に理解した <
 ̄YYYYYYYYYYYYYYYYYYYY ̄

参考文献

ほとんど毎日パソコン触って目の疲れがやばいのでブルーライトカットメガネを買って1週間使った感想

健康診断で目の疲れが発覚

人生で初めての屈辱を味わった。自分の取り柄はどうでも良い事を覚える事と、視力が良い事(下がらないこと)だった。しかしながら、先週、会社の健康診断があり視力を測定したところ、左目が1.5 -> 1.0に。右目が1.2 -> 0.6になってしまっていた
めちゃくちゃショックや...しかしながら日常生活を送る上では視力が下がった弊害を受けていた自覚が全くない..なぜだろうか...

思い当たる点としては平日は毎日朝10時から19時まで業務でパソコン。家帰ってから21時から23時~24時までパソコン。土日は朝10時から24時ぐらいまでパソコン触ってたりで、思ってみればパソコン触ってない日がないなぁと...

こんな生活してれば、そりゃ目も疲れるわなぁと思いつつ、この件がショックでこんなツイートをしたところ、ブルーライトカットメガネがいいというリプライをもらった

また、ベテランのエンジニアの方からも「ブルーライトカットメガネいいっすよ」とアドバイスをもらった。うーん、これはせっかくの機会だし買ってみるかと思い立ったのが丁度、先週の出来事である

地元のJINSに向かう

人生で初めてのメガネ購入。教えて頂いたJINSというお店に向かうことに...
しかしながら、到着して気づいたが地元にあったのはJINSではなくてZoffだった。「アホすぎるやろ笑」と思いつつ、お店を覗いてみることに。人生初のメガネ物色に汗が止まらなかった

3分ほど、物色を続けているとついに目的のブルーライトカットメガネを発見。価格は5000円(税込で5500円)で思っていたよりも安かった。メガネ拭き兼、持ち運び用の巾着袋も付いているので結果的に5500円で事足りた。バリエーションはフレームの色の違いとレンズの大きさの違いで用意されていて、店舗に常時置いてある種類は9種類だったと記憶している
どうやら、2, 3日営業日を待てば、好きなフレームにレンズを用意してもらえるらしいので、拘りがある方はそちらを利用すれば良いかと思う。私はメガネの似合う似合わないとか全然、気にしてなかったので即日購入可能な既製品を購入した

こちらが購入したブルーライトカットメガネ。箱から出す前に写真撮っとけばよかった...
f:id:takamizawa46:20191201123705j:plain:h550

1週間、使ってみた感想

職場に急にインテリジェンスな男が現れたと初日からざわつく。鏡を覗いてみると「悪くないじゃん」と思う一方で、フレームがもう少し太いものを買えば良かったかなぁとも思う。まぁ、見た目のことは今回は重視しないないのでヨシ!

目の疲労の感じ方の変化について

まず肝心な目の疲労についてだが、確かにブルーライトカットメガネを使うことで目の疲れ方のペースが落ちた気はする。具体的な測定値があるわけではないので、「そんな気がする」というプラシーボ効果的な面もあるので信用には足らないかもしれないが、個人的にはしっくりきている。本来の目標が達成されたのでかなり満足だ
肌が弱いが、耳のところやフレームが接触する部分がかゆくなることもなかった。しかし、一方で以下のような問題もある

ずっとかけていると世界がおかしくなる(RPG風味

レンズを通して見える世界の色が、若干黄色味がかって見えるため、メガネを外した際に目がぼやけるような感覚に襲われる。度の入ったメガネを使用したことがないので「メガネってそういうものだよ」と言われてしまえばそれまでだが、1日中、ブルーライトカットメガネをかけ続けるのはオススメしない。こんな感覚に初日に襲われてから1時間に1回はブルーライトカットメガネを5分ほど外すようにしている。こうすることで、かなりマシになった

総評

5500円で良い買い物をした。滅多に物を買うことはないが、「目の疲れを削減する」という目標は達成することが出来た。しかしながら、ずっと掛け続けていると上記のような問題もあるので、上手く使っていきたいと思う。エンジニアの方はじめ多くの方は1日中パソコンを触ることは当たり前になっているので、自身の目を大切にしたいという方はぜひ一度、買ってみて効果を体感して頂ければと思う

【第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代のためのプログラマー勉強方法を聞いてきた。