Elixir GUIDES を普通に読む5

Elixir GUIDES を読んだメモ

elixir-lang.jp

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) が実行される

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
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 プログラムを構築する手段も提供する

e-words.jp

  • 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 使って書こうと思ったけどわからなかった