Elixir GUIDES を普通に読む5
Elixir GUIDES を読んだメモ
9. 再帰
繰り返し
defmodule Recursion do def print_multiple_times(msg, n) when n <= 1 do IO.puts msg end def print_multiple_times(msg, n) do IO.puts msg print_multiple_times(msg, n - 1) end end Recursion.print_multiple_times("Hello!", 3)
- ガードを用いて終了条件を表している
reduce
- 数値のリストを合計する
defmodule Math do def sum_list([head | tail], accumulator) do sum_list(tail, head + accumulator) end def sum_list([], accumulator) do accumulator end end IO.puts Math.sum_list([1, 2, 3], 0) #=> 6
- リストを取得して1つの値に減らす計算を reduceアルゴリズムという
- 関数型プログラミングの中心
- リストが
[ head | tail ]
である場合はsum_list(tail, head + accumulator)
Math.sum_list([1, 2, 3], 0)
のときhead: [ 1 ], tail: [ 2, 3 ], accumulator: 0
sum_list(tail: [2, 3], head: 1 + accumulator: 0 )
となる
sum_list([2, 3], 1)
のときhead: [ 2 ], tail: [ 3 ], accumulator: 1
sum_list([3], 3)
のときhead: [ 3 ], tail: [ ], accumulator: 3
- このとき sum_list([ ], 6) となり、
def sum_list([], accumulator)
が実行される
- このとき sum_list([ ], 6) となり、
10. Enumerables と Streams
Enumerables
- 列挙型は Enum モジュールで提供される
- リストとマップは列挙型である
Eager vs Lazy
- リストとマップは列挙型である
- Enum モジュールの関数はすべてEagerである(先行評価)
- 列挙可能なものを期待する
パイプ演算子
- 列挙可能なものを期待する
|>
これは左辺から出力を受け取り右辺の関数呼び出しの最初の引数として渡す
iex(1)> odd? = &(rem(&1, 2) != 0) #Function<7.126501267/1 in :erl_eval.expr/5> iex(2)> Enum.sum(Enum.filter(Enum.map(1..100_000, &(&1 * 3)), odd?)) 7500000000 iex(3)> total_sum = 1..100_000 |> Enum.map(&(&1 * 3)) |> Enum.filter(odd?) |> Enum.sum 7500000000
Stream
- 遅延評価を行うモジュール
iex(4)> 1..100_000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?) |> Enum.sum 7500000000
- Enum では
iex(9)> 1..100_000 |> Enum.map(&(&1 * 3)) |> Enum.filter(odd?) [3, 9, 15, 21, 27, 33, 39, 45, 51, 57, 63, 69, 75, 81, 87, 93, 99, 105, 111, 117, 123, 129, 135, 141, 147, 153, 159, 165, 171, 177, 183, 189, 195, 201, 207, 213, 219, 225, 231, 237, 243, 249, 255, 261, 267, 273, 279, 285, 291, 297, ...]
- これは評価される
- Stream では
iex(10)> 1..100_000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?) #Stream<[ enum: 1..100000, funs: [#Function<49.119101820/1 in Stream.map/2>, #Function<41.119101820/1 in Stream.filter/2>] ]>
これは
Enum.sum
などで呼び出されるまで評価されないStream.cycle/1
などはストリームを作成する関数。これは引数で与えられたEnum
の繰り返しを作る
iex(11)> stream = Stream.cycle([1, 2, 3]) #Function<65.119101820/2 in Stream.unfold/2> iex(12)> Enum.take(stream, 10) [1, 2, 3, 1, 2, 3, 1, 2, 3, 1]
- ファイルをStreamで読み出すこともできる
iex> stream = File.stream!("path/to/file") #Function<18.16982430/2 in Stream.resource/3> iex> Enum.take(stream, 10)
- これはファイルの最初の10行を読み出す
- 大きいファイルを処理する場合全て読みださなくても良い
11. プロセス
- Elixir ではすべてのコードはプロセス内で実行される
- プロセスは互いに分離され、並行して実行し、メッセージの受け渡しを介して通信を行う
- プロセスは分散された fault-tolerant プログラムを構築する手段も提供する
- Elixir のプロセスは軽量であり、数万、数十万のプロセスが同時に実行されることも珍しくない
spawn
- 新しいプロセスを生成する
iex(13)> spawn fn -> 1 + 2 end #PID<0.123.0>
spawn/1
は PID を返す(プロセスのID)- 引数で与えられる関数を実行した後に終了する
self/0
で現在のPIDを取得できる
iex(14)> self #PID<0.104.0>
send と receive
send/2
はプロセスへメッセージを送信するreceive/1
は受信を行う
iex(15)> send self(), {:hello, "world"} {:hello, "world"} iex(16)> receive do ...(16)> {:hello, msg} -> msg ...(16)> {:world, msg} -> "won't match" ...(16)> end warning: variable "msg" is unused (if the variable is not meant to be used, prefix it with an underscore) iex:18 "world"
- メッセージがプロセスに送信されると、メッセージはプロセスのメールボックに保存される
receive/1
は指定されたいずれかのパターンに一致するメッセージをメールボックスから探し出す- パターンに一致するメッセージがない場合、一致するメッセージが到着するまで待機する
タイムアウトの指定も可能
flush/0
はメールボックス内のメッセージを出力する
iex(1)> send self(), :hello :hello iex(2)> flush() :hello :ok
Links
- Elixir でプロセスを生成する場合、それらをリンクプロセスとして生成する
- 次の例は
spawn/1
で失敗して開始したときの動作
iex(3)> spawn fn -> raise "oops" end #PID<0.108.0> iex(4)> 17:25:08.495 [error] Process #PID<0.108.0> raised an exception ** (RuntimeError) oops (stdlib) erl_eval.erl:678: :erl_eval.do_apply/6
- エラーをログに出力しただけだが、プロセスはまだ生きている
spawn_link/1
を用いると
iex(1)> self() #PID<0.104.0> iex(2)> spawn_link fn -> raise "oops" end ** (EXIT from #PID<0.104.0>) shell process exited with reason: an exception was raised: ** (RuntimeError) oops (stdlib) erl_eval.erl:678: :erl_eval.do_apply/6 Interactive Elixir (1.9.4) - press Ctrl+C to exit (type h() ENTER for help) iex(1)> 17:41:49.347 [error] Process #PID<0.107.0> raised an exception ** (RuntimeError) oops (stdlib) erl_eval.erl:678: :erl_eval.do_apply/6
- 子プロセスが死んだため親プロセスがexitを受けシェルを終了した
- その後、iex は新しいシェルセッションを開始している
Process.link/1
を用いることで手動でリンクを行うこともできるElixir プロセスは分離されており、プロセスの障害がクラッシュしたり、別のプロセスを破壊したりしない
- リンクを用いるとプロセスが関係を確率でき、プロセスの死を確認して新しいプロセスを開始したりすることができる
タスク
- タスクはspawn関数の上に構築される
iex(1)> self() #PID<0.104.0> iex(2)> Task.start fn -> raise "oops" end {:ok, #PID<0.107.0>} iex(3)> 23:48:28.952 [error] Task #PID<0.107.0> started from #PID<0.104.0> terminating ** (RuntimeError) oops (stdlib) erl_eval.erl:678: :erl_eval.do_apply/6 (elixir) lib/task/supervised.ex:90: Task.Supervised.invoke_mfa/2 (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3 Function: #Function<21.126501267/0 in :erl_eval.expr/5> Args: [] self() #PID<0.104.0>
- なんか出力おかしいけどエラー出力前に入力に戻っちゃっただけ
Task は別プロセスで動かしているのでTaskが落ちてもシェルセッションは生きている
spawn/1
,spawn_link/1
の代わりにTask.start/1
,Task.start_link/1
を使用する
State
defmodule KV do def start_link do Task.start_link(fn -> loop(%{}) end) end defp loop(map) do receive do {:get, key, caller} -> send caller, Map.get(map, key) loop(map) {:put, key, value} -> loop(Map.put(map, key, value)) end end end
- メッセージを受信し続けるプロセス
iex(5)> {:ok, pid} = KV.start_link {:ok, #PID<0.128.0>} iex(6)> send pid, {:get, :hello, self()} {:get, :hello, #PID<0.104.0>} iex(7)> flush() nil :ok
start_link
で空mapのloopが回る- get しても空マップなのでnilが変える
iex(11)> send pid, {:put, :hello, :world} {:put, :hello, :world} iex(12)> send pid, {:get, :hello, self()} {:get, :hello, #PID<0.104.0>} iex(13)> flush() :world :ok
- put で map に値を入れる
- get でメッセージ送信
flush() でメールボックスに入っているメッセージを出す
State は Agent で抽象化できる?
- よくわからなかった
- defmodule KV を Agent 使って書こうと思ったけどわからなかった