LoginSignup
6
4

More than 5 years have passed since last update.

すごいE本をElixirでやる(7章)

Posted at

より.


Erlang には関数型と並行性という 2 つのパラダイムがある

関数型の側面については巻頭から説明してきました。参照透過性、再帰、高階関数などです。
Erlangを有名にしたものの 1 つは、並行性の側面です。アクター、何千もの並行プロセス、監視ツリーなどがあります。

なるほど.関数型のパラダイムについては Ruby や Haskell で目にした覚えがある.
並行性のパラダイムについてはほぼ初めてな感じがするのは,今までそれを掲げた言語/フレームワークを触ってこなかったせいだろう.

7.1 エラー大集合

コンパイル時エラー

module.beam: Module name 'madule' does not match file name 'module'
    -module属性内に書いたモジュール名がファイル名と一致していません。

Elixir ではモジュール名とファイル名が一致していなくてもよいので上のコンパイル時エラーは出ないだろう.

# 関数を公開していない、あるいはその関数が使われている場所が間違った関数名やアリティになっています。
defmodule X do
  defp foo, do: "foo"
end
#> qr_1202Hxm.exs:2: warning: function foo/0 is unused
# 関数が存在していません。
defmodule X do
  def bar, do: baz
end
#> ** (CompileError) orgmode_elixir_src.exs:4: function baz/0 undefined
#>     (stdlib) lists.erl:1336: :lists.foreach/2
#>     (stdlib) erl_eval.erl:657: :erl_eval.do_apply/6

# シンタックスエラー
# エラーメッセージで「2 行目で始まった `"` の終わりが見つからない」と書いてある
defmodule X do
  def bar, do: "bar
end
#> ** (TokenMissingError) orgmode_elixir_src.exs:3: missing terminator: " (for string starting at line 2)
#>     (elixir) lib/code.ex:307: Code.require_file/2

Erlang でのシンタックスエラーのメッセージより少し親切に思えるが,
全ての場合でこういった形でエラー表記してくれるかはわからない.
( 対応する end をつけ忘れた場合はしてくれた )

# 型付けに失敗する数式を見つけてくれます。
defmodule X do
  def bar, do: :bar + 5
end
#> qr_1202iNu.exs:2: warning: this expression will fail with ArithmeticError
# 使っていない変数を宣言しています。
defmodule X do
  def bar do
    baz = "a"
    nil
  end
end
#> qr_120271P.exs:3: warning: variable baz is unused
# この警告は、何か必要ないことをしていたり間違えている場合、それを知らせます。
# ./module.erl:5: Warning: a term is constructed, but never used
defmodule X do
  def bar do
    fn -> nil end
    {:a, :b}
    nil
  end
end

Elixir での出し方がわからなかった.上の式では出ない.もし Elixir でこれを出せたら教えてほしい.

# あるアリティを持つ関数の宣言と、別のアリティを持つ関数の宣言をまぜこぜにはできません。
# ある関数を別の関数のヘッドの節の間で定義したときも起きます。
# ./module.erl:5: head mismatch
defmodule X do
  def bar, do: "bar"
  def baz, do: "baz"
  def bar(name), do: "bar " <> name
end

これも Elixir では出ないんじゃないかなあ.上の式では出なかった.
Erlang だと複数の関数を一繋ぎに宣言できるけど,Elixir では一つずつしか宣言できないので,「宣言のグループ」のような概念がたぶんない.
もし Elixir で出せた人がいたら教えてほしい.

# catch-all 節のあとに節があります。
defmodule X do
  def bar(0), do: "bar0"
  def bar(_), do: "bar"
  def bar(1), do: "bar1"
end
#> qr_1202jVp.exs:5: warning: this clause cannot match because a previous clause at line 4 always matches
# case ... of ブランチの中で宣言されている変数を、その外側で使っている場合に起きます。
# ./module.erl:9: variable 'A' unsafe in 'case' (line 5)
defmodule X do
  def bar(arg) do
    cond do
      {:a, x} = arg -> x
      true -> "OTHER"
    end
    IO.inspect x
  end
end
#> ** (CompileError) orgmode_elixir_src.exs:8: function x/0 undefined
#>     (stdlib) lists.erl:1336: :lists.foreach/2
#>     (stdlib) erl_eval.erl:657: :erl_eval.do_apply/6

これも再現できなかった.cond の中で宣言した変数は,cond の外からは見えないようになっている.
Erlang と Elixir で変数のスコープが異なるのかもしれない.
もし Elixir で出せた人がいたら教えてほしい.

たいていの場合、いちばん難しいのは、多くのエラーの根本原因となっているエラーがどれかを見つけるところです。
コンパイル時エラーは表示された順に解決していくことをおすすめします。

はい.

ランタイムエラー

# 関数内のすべてのガード節で失敗する、あるいはすべてのパターンマッチで失敗することです。
defmodule X do
  def foo(1), do: "foo1"
  def foo(2), do: "foo2"
end

X.foo(3)
#> ** (FunctionClauseError) no function clause matching in X.foo/1
#>     orgmode_elixir_src.exs:2: X.foo(3)
#>     (elixir) lib/code.ex:307: Code.require_file/2
# 特定の条件を書くのを忘れたか、間違った種類のデータを送ったか、catch-all 節が必要かのどれかです。
defmodule X do
  def foo(arg) do
    case arg do
      1 -> "foo1"
      2 -> "foo2"
    end
  end
end

X.foo(3)
#> ** (CaseClauseError) no case clause matching: 3
#>     orgmode_elixir_src.exs:4: X.foo/1
#>     (elixir) lib/code.ex:307: Code.require_file/2
# true と評価される節が見つからないときに、このエラーが起きます
defmodule X do
  def foo do
    cond do
      2 > 4 -> :ok
      0 > 1 -> :ok
    end
  end
end

X.foo
#> orgmode_elixir_src.exs:4: warning: this check/guard will always yield the same result
#> orgmode_elixir_src.exs:5: warning: this check/guard will always yield the same result
#> ** (CondClauseError) no cond clause evaluated to a true value
#>     X.foo/0
#>     (elixir) lib/code.ex:307: Code.require_file/2

上の式は Erlang の if と似た Elixir の cond で試した.
true と評価される節が見つからないときは no cond clause evaluated to a true value というエラーになるようだ.

# 間違ったマッチに関するエラーはパターンマッチが失敗したときに起きます。
[a, b] = {4, 5}
#> orgmode_elixir_src.exs:1: warning: variable a is unused
#> orgmode_elixir_src.exs:1: warning: variable b is unused
#> orgmode_elixir_src.exs:1: warning: no clause will ever match
#> ** (MatchError) no match of right hand side value: {4, 5}
#>     orgmode_elixir_src.exs:1: (file)
#>     (elixir) lib/code.ex:307: Code.require_file/2
# 関数を間違った引数で呼び出しているという点で、関数節のエラーに似ています。
elem({1,2}, 0) # => 1
elem([1,2], 0)
#> orgmode_elixir_src.exs:2: warning: the result of the expression is ignored (suppress the warning by assigning the expression to the _ variable)
#> orgmode_elixir_src.exs:3: warning: this expression will fail with ArgumentError
#> ** (ArgumentError) argument error
#>     orgmode_elixir_src.exs:3: (file)
#>     (elixir) lib/code.ex:307: Code.require_file/2

Erlang だと bad argument であるものは,Elixir だと argument error のようだ.

# 存在しない関数を呼び出したときに起きます。
foo(123)
#> ** (CompileError) orgmode_elixir_src.exs:1: undefined function foo/1
#>     (elixir) lib/code.ex:307: Code.require_file/2
# 存在しない算術演算をしようとしたときに起きます。
5 + :llama
#> orgmode_elixir_src.exs:1: warning: this expression will fail with ArithmeticError
#> ** (ArithmeticError) bad argument in arithmetic expression
#>     orgmode_elixir_src.exs:1: (file)
#>     (elixir) lib/code.ex:307: Code.require_file/2
# 値が関数でない変数を関数として使ってしまうことです。
defmodule X do
  def foo(f), do: f.()
end

X.foo(fn -> :done end) # => :done
X.foo(1)
#> ** (BadFunctionError) expected a function, got: 1
#>     orgmode_elixir_src.exs:2: X.foo/1
#>     (elixir) lib/code.ex:307: Code.require_file/2
# 高階関数を使っていて、必要な数の引数よりも多いあるいは少ない引数を渡したときに起きます。
f = fn(_) -> :ok end
f.(:a, :b)
#> ** (BadArityError) #Function<0.131248289 in file:orgmode_elixir_src.exs> with arity 1 called with 2 arguments (:a, :b)
#>     orgmode_elixir_src.exs:3: (file)
#>     (elixir) lib/code.ex:307: Code.require_file/2

7.2 例外を上げる

Erlangには3種類の例外があります。エラー(error)、終了(exit)、スロー(throw)です。すべて異なった用途があります。

ふむふむ.

Elixir での例外処理機構については公式サイトの GETTING STARTED にある try, catch and rescue にまとまっている.

エラー例外

エラーは、いま起きたことを制御するコードを呼び出せないときに、関数にその実行を止めさせるための手段です。

エラーは制御できない動作の実行を止めるためのもの.なるほど.

defmodule X do
  def doit do
    raise(ArithmeticError)
  end

  def doit2 do
    raise("custom error")
  end
end

X.doit
#> ** (ArithmeticError) bad argument in arithmetic expression
#>     orgmode_elixir_src.exs:3: X.doit/0
#>     (elixir) lib/code.ex:307: Code.require_file/2

X.doit2
#> ** (RuntimeError) custom error
#>     orgmode_elixir_src.exs:7: X.doit2/0
#>     (elixir) lib/code.ex:307: Code.require_file/2

Elixir でエラーを発生させるには Kernel.raise/1Kernel.raise/2 を使う.

終了例外

内部終了はexit/1 関数を呼び出すことで発生し、いま走っているプロセスの動作を止めます。

外部終了はexit/2 関数を呼び出すことで発生し、Erlang の並行な面で起こるマルチプロセスと関係があります。

ほうほう.内部終了はエラーと似ていて,歴史的には両方とも同じもの.

非常に似たような使い方ができますが、本当の違いはその意図にあります。
単なるエラーを手にしたのか、それともいまのプロセスを殺すに足る状況なのか。

どこかで扱われるかもしれないことを期待しているならエラー,
もうプロセス殺すしかない慈悲はないなら内部終了.
なるほど.

Elixir で exit するには Kernel.exit/1 を使う.

exit("もうだめだ!")
#> ** (exit) "もうだめだ!"
#>     orgmode_elixir_src.exs:1: (file)
#>     (elixir) lib/code.ex:307: Code.require_file/2

例外を投げる

意図は、終了やエラーのように「プロセスをクラッシュしろ!」と伝えることではなく、フローの制御です。

なるほど,フローの制御のために大域脱出をする場合に使う.

Elixir では Kernel.throw/1 を使う.

throw("スロー")
#> ** (throw) "スロー"
#>     orgmode_elixir_src.exs:1: (file)
#>     (elixir) lib/code.ex:307: Code.require_file/2

7.3 例外を処理する

Elixir での例外は Kernel.SpecialForms.try/1 で取り扱える.

try のドキュメントに例があるのでそのまま貼る.

try do
  do_something_that_may_fail(some_arg)
rescue
  ArgumentError ->
    IO.puts "Invalid argument given"
catch
  value ->
    IO.puts "caught #{value}"
else
  value ->
    IO.puts "Success! The result was #{value}"
after
  IO.puts "This is printed regardless if it failed or succeed"
end
  • rescue 節は exception を扱う
  • catch 節は throw された値を扱う.終了 exit もこちらで扱う
  • else 節は try の結果をパターンマッチングさせることができる
  • after 節は必ず実行され,もっぱら try で開いたリソースの後始末に用いられる
defmodule Exceptions do
  def throws(f) do
    try do
      f.()
    catch
      thrown -> {:throw, :caught, thrown}
    end
  end
end

Exceptions.throws(fn -> throw(:thrown) end) # => {:throw, :caught, :thrown}
Exceptions.throws(fn -> raise("pang") end)
#> ** (RuntimeError) pang
#>     orgmode_elixir_src.exs:12: anonymous fn/0 in :elixir_compiler_0.__FILE__/1
#>     orgmode_elixir_src.exs:4: Exceptions.throws/1
#>     (elixir) lib/code.ex:307: Code.require_file/2
Exceptions.throws(fn -> exit("goodbye") end)
#> ** (exit) "goodbye"
#>     orgmode_elixir_src.exs:17: anonymous fn/0 in :elixir_compiler_0.__FILE__/1
#>     orgmode_elixir_src.exs:4: Exceptions.throws/1
#>     (elixir) lib/code.ex:307: Code.require_file/2

catch 節で明示的に指定しなければ exit は無視して throw だけを受けとるようだ.
また,例外を発生させたもの( raise させたもの ) も受けとっていない.

defmodule Exceptions do
  def errors(f) do
    try do
      f.()
    rescue
      error -> {:error, :caught, error}
    end
  end
end

# Exceptions.errors(fn -> throw(:thrown) end)
#> ** (throw) :thrown
#>     orgmode_elixir_src.exs:11: anonymous fn/0 in :elixir_compiler_0.__FILE__/1
#>     orgmode_elixir_src.exs:4: Exceptions.errors/1
#>     (elixir) lib/code.ex:307: Code.require_file/2
Exceptions.errors(fn -> raise("pang") end) # => {:error, :caught, %RuntimeError{message: "pang"}}
Exceptions.errors(fn -> exit("goodbye") end)
#> ** (exit) "goodbye"
#>     orgmode_elixir_src.exs:17: anonymous fn/0 in :elixir_compiler_0.__FILE__/1
#>     orgmode_elixir_src.exs:4: Exceptions.errors/1
#>     (elixir) lib/code.ex:307: Code.require_file/2

前述の通り Elixir で error を受けとるには rescue 節を利用する.
rescue 節では throwexit を受けとらない.

defmodule Exceptions do
  def exits(f) do
    try do
      f.()
    catch
      :exit, exit -> {:exit, :caught, exit}
    end
  end
end

#Exceptions.exits(fn -> throw(:thrown) end)
#> ** (throw) :thrown
#>     orgmode_elixir_src.exs:11: anonymous fn/0 in :elixir_compiler_0.__FILE__/1
#>     orgmode_elixir_src.exs:4: Exceptions.exits/1
#>     (elixir) lib/code.ex:307: Code.require_file/2
#Exceptions.exits(fn -> raise("pang") end)
#> ** (RuntimeError) pang
#>     orgmode_elixir_src.exs:16: anonymous fn/0 in :elixir_compiler_0.__FILE__/1
#>     orgmode_elixir_src.exs:4: Exceptions.exits/1
#>     (elixir) lib/code.ex:307: Code.require_file/2
Exceptions.exits(fn -> exit("goodbye") end) # => {:exit, :caught, "goodbye"}

exit を受けとるには catch 節で :exit を明示する.

実際は,例外 ( raise で発生させたもの ) も catch 節で :error と明示すれば受けとれる.

rescue 節の意味とは……となりそうだが,3つの例外のうち最もよく使うものだけ特別扱いして読みやすくしているのだろう.

Kernel.SpecialForms.try/1 のドキュメントにも

Besides relying on pattern matching, rescue clauses provides some conveniences around exceptions that allows one to rescue an exception by its name.

意訳すると「パターンマッチングに加えて, rescue 節では便利なように例外の名前で rescue できるようになっています」と書いてある.

defmodule CutArm do
  defexception [:message]
end

defmodule Exceptions do
  def sword(1), do: throw(:slice)
  def sword(2), do: raise(CutArm)
  def sword(3), do: exit(:cut_leg)
  def sword(4), do: throw(:punch)
  def sword(5), do: exit(:cross_bridge)

  def black_knight(attack) when is_function(attack, 0) do
    try do
      attack.()
    rescue
      CutArm -> "I've had worse."
    catch
      :throw, :slice   -> "It is but a scratch."
      :exit, :cut_leg  -> "Come on you pansy!"
      _, _ -> "Just a flesh wound"
    else
      _ -> "None shall pass."
    end
  end

  def talk, do: "blah blah"
end

Exceptions.talk # => "blah blah"
Exceptions.black_knight(&Exceptions.talk/0) # => "None shall pass."
Exceptions.black_knight(fn -> Exceptions.sword(1) end) # => "It is but a scratch."
Exceptions.black_knight(fn -> Exceptions.sword(2) end) # => "I've had worse."
Exceptions.black_knight(fn -> Exceptions.sword(3) end) # => "Come on you pansy!"
Exceptions.black_knight(fn -> Exceptions.sword(4) end) # => "Just a flesh wound"
Exceptions.black_knight(fn -> Exceptions.sword(5) end) # => "Just a flesh wound"

Elixir では try と一緒に使わない catch はなさそう.整理して全部 try/1 にまとめちゃったんじゃないかな.

Erlang の erlang:get_stacktrace/0 は Elixir だと System.stacktrace/0 に相当するみたい.

7.4 二分木でtryをトライする

defmodule Tree do
  def empty, do: {:node, nil}

  def insert({:node, nil}, key, val) do
    {:node, {key, val, {:node, nil}, {:node, nil}}}
  end
  def insert({:node, {key, val, smaller, larger}}, new_key, new_val) when new_key < key do
    {:node, {key, val, insert(smaller, new_key, new_val), larger}}
  end
  def insert({:node, {key, val, smaller, larger}}, new_key, new_val) when new_key > key do
    {:node, {key, val, smaller, insert(larger, new_key, new_val)}}
  end
  def insert({:node, {key, _val, smaller, larger}}, new_key, new_val) when new_key === key do
    {:node, {key, new_val, smaller, larger}}
  end

  def lookup({:node, nil}, _key), do: :undefined
  def lookup({:node, {key, val, _smaller, _lager}}, key), do: {:ok, val}
  def lookup({:node, {node_key, _val, smaller, _lager}}, key) when key < node_key do
    lookup(smaller, key)
  end
  def lookup({:node, {node_key, _val, _smaller, lager}}, key) when key > node_key do
    lookup(lager, key)
  end

  # 木の中から値 val を探す
  def has_value({:node, nil}, _val), do: false
  def has_value({:node, {_key, val, _smaller, _larger}}, val), do: true
  def has_value({:node, {_key, _val, smaller, larger}}, val)  do
    has_value(smaller, val) || has_value(larger, val)
  end

  # throw - catch を用いた has_value の実装
  def has_value2(tree, val) do
    try do
      has_value_with_throw(tree, val)
    catch
      true -> true
    else
      _ -> false
    end
  end
  def has_value_with_throw({:node, nil}, _val), do: false
  def has_value_with_throw({:node, {_key, val, _smaller, _larger}}, val), do: throw(true)
  def has_value_with_throw({:node, {_key, _val, smaller, larger}}, val) do
    has_value_with_throw(smaller, val)
    has_value_with_throw(larger, val)
  end
end

t1 = Tree.insert(Tree.empty, "Jim Woodland", "jim.woodland@gmail.com")
t2 = Tree.insert(t1, "Mark Anderson", "i.am.a@hotmail.com")
addresses = t2
            |> Tree.insert("Wilson Longbrow", "longwil@gmail.com")
            |> Tree.insert("Kevin Robert", "myfairy@yahoo.com")
            |> Tree.insert("Anita Bath", "abath@someuni.edu")

Tree.has_value(addresses, "abath@someuni.edu")  # => true
Tree.has_value2(addresses, "abath@someuni.edu") # => true

Tree.has_value(addresses, "for@example.com")    # => false
Tree.has_value2(addresses, "for@example.com")   # => false

この例だと throw を使う意義があんまり感じられないな,
むしろ throw を使わないほうが見通しがよくみえるんだけど
もうちょっと条件が加わると違うのかもしれない.

大域ジャンプをすると読みにくくなる.だからできるだけ避けたい.
という僕の知識と経験とは別の「大域ジャンプを上手に使って,速度と可読性を上げよう」ということが書いてあり,
速度については,処理する所が減るので異論ないものの,可読性について今はまだは少し戸惑っている.

そのうち「何でこんなこと書いたのかな」と思う日がくるだろうか.

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4