- 2015/07/22/すごいE本をElixirでやる(14) - ヽ(´・肉・`)ノログ
- 2015/07/23/すごいE本をElixirでやる(15) - ヽ(´・肉・`)ノログ
- 2015/07/28/すごいE本をElixirでやる(16) - ヽ(´・肉・`)ノログ
より.
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/1
や Kernel.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
節では throw
や exit
を受けとらない.
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
を使わないほうが見通しがよくみえるんだけど
もうちょっと条件が加わると違うのかもしれない.
大域ジャンプをすると読みにくくなる.だからできるだけ避けたい.
という僕の知識と経験とは別の「大域ジャンプを上手に使って,速度と可読性を上げよう」ということが書いてあり,
速度については,処理する所が減るので異論ないものの,可読性について今はまだは少し戸惑っている.
そのうち「何でこんなこと書いたのかな」と思う日がくるだろうか.