読む順番、何か間違えているような気もしますが…
Programming Elixirを読みました。
以下、メモです。
読んでて、まだ知らなかったとか、なるほどなーと感じたところだけ…
nestしたstructにアクセスする便利な機能
iex(1)> defmodule Customer do
...(1)> defstruct name: "", company: ""
...(1)> end
iex(2)> defmodule Bug do
...(2)> defstruct owner: %{}, details: "", s: 1
...(2)> end
iex(3)> report = %Bug{owner: %Customer{}, details: "broke"}
%Bug{details: "broke", owner: %Customer{company: "", name: ""}, s: 1}
iex(4)> report.owner.name
""
# 以下のようにアクセスする
iex(5)> %Bug{ report | owner: %Customer{ report.owner | name: "neko" }}
%Bug{details: "broke", owner: %Customer{company: "", name: "neko"}, s: 1}
# put_inを使うと、より簡単にアクセス可能
iex(6)> put_in report.owner.name, "neko"
%Bug{details: "broke", owner: %Customer{company: "", name: "neko"}, s: 1}
ちなみに、update_in を使うと、
update_in report.owner.name, &("inu" <> "neko")
のように関数を与えることもできる。
さらに、他にもmacroなど用意されてたり、更新する値を [:a, :b, :c] のような記述でkeyを指定できたりと、思いの外奥が深い…
Stream関連
IO.streamの中身(Elixir 1.0.5時点)
@spec stream(device, :line | pos_integer) :: Enumerable.t
def stream(device, line_or_codepoints) do
IO.Stream.__build__(map_dev(device), false, line_or_codepoints)
end
# Map the Elixir names for standard io and error to Erlang names
defp map_dev(:stdio), do: :standard_io
defp map_dev(:stderr), do: :standard_error
defp map_dev(other) when is_atom(other) or is_pid(other) or is_tuple(other), do: other
__build__ が何かなーと思うと…
def __build__(device, raw, line_or_bytes) do
%IO.Stream{device: device, raw: raw, line_or_bytes: line_or_bytes}
end
なるほどー。何か大層なことしていると思ったのですが、構造体に突っ込んでいるだけなのですね。
StreamはLazy Enumerablesなので、その処理対象が必要になった時に値を処理したい、という時に有効なのですね。
Comprehensions
以下のようにElixirでは記述することができるのですが、この for 、いわゆるループのforだと認識していたのですが、 ふと考えてみると ~ for you とかの、~に対してという感じのforとして使っているのですね。元々そうなのかな。。。そう見えると、この記述、なるほどなーという感じ。
iex> for x <- 1..5, y <- 2..6, rem(x, y) == 0, do: x * y [4, 9, 8, 16, 25]
Char lists
[a | b] はリストの結合。なので、以下は 'cat' というchar_listに対して、 'dog' という別のリストを結合する、という処理になるので、以下のような形になるのかな。
iex> [ 'cat' | 'dog' ] ['cat', 100, 111, 103]
例えば、以下のような感じ。ここで a のリストがそのまま 'cat' に該当するという。
iex> a = [1,2] [1, 2] iex> b = [3,4,5] [3, 4, 5] iex> [a | b] [[1, 2], 3, 4, 5]
escript
以下のように escript を指定することで、mix実行時にコマンドを実行することができる。
defmodule MyApp.Mixfile do
def project do
[app: :myapp,
version: "0.0.1",
escript: escript]
end
def escript do
[main_module: MyApp.CLI]
end
end
Cuoncurrent programingの話
processの生成とその経過時間
プロセスに処理を渡すspawnは、 Kernel、Nodeとあって、それぞれがspawn、spawn_link、spawn_monitorなど、種類とその引数のバリエーションがあってパッとこれだ!と思い浮かばない…
http://elixir-lang.org/docs/stable/elixir/Kernel.html#spawn/3
:timer.tc- 引数に与えた処理の、かかった時間を計測する
- http://www.erlang.org/doc/man/timer.html#tc-1
例えば、以下の例はn回プロセスを生成する。その時間を計測する、というもの。
defmodule Chain do
def counter(next_pid) do
receive do
n ->
send next_pid, n + 1
end
end
def create_processes(n) do
last = Enum.reduce 1..n, self,
fn (_, send_to) ->
IO.inspect send_to
spawn Chain, :counter, [send_to]
end
send last, 0
receive do
final_answer when is_integer(final_answer) ->
"result is #{inspect final_answer}"
end
end
def run(n) do
IO.puts inspect :timer.tc(Chain, :create_processes, [n])
end
end
名前付け
Process.register/2は、Process.alive?/1でtrueとなる、生存しているプロセスに対してPIDと紐づく名前をつけることができるProcess.alive?/1が大事で、そこが無いと
番外: Kernel.selfにより得られるPIDの違い
iex > current = self
iex > child = spawn(fn -> send current, {Kernel.self, 1 + 2} end)
#PID<0.87.0>
iex > flush
{#PID<0.87.0>, 3}
:ok
iex > child = spawn(fn -> send Kernel.self, {Kernel.self, 1 + 2} end)
#PID<0.87.0>
iex > flush
:ok
これ、少し考えれば面白い。
spawn は、与えた関数を処理する別processを立ち上げ、処理させる。
そのため、その関数内で実行される Kernel.self は立ち上げられた別process内で実行され、そのprocessに関する値を返す。なので、以下の Kernel.self の実行でも異なる結果が表示される。
iex > child = spawn(fn -> Kernel.self end) #PID<0.74.0> iex > Kernel.self #PID<0.59.0>
macros
Never use a macro when you can use a function.
この言葉から始まるのが印象的。
defmodule My do
defmacro macro(param) do
IO.inspect param
end
end
defmodule Test do
require My
My.macro :atom #=> :atom
My.macro 1 #=> 1
My.macro 1.0 #=> 1.0
My.macro [1, 2, 3] #=> [1, 2, 3]
My.macro "binaries" #=> "binaries"
My.macro { 1, 2 } #=> {1, 2}
My.macro do: 1 #=> [do: 1]
My.macro do #=> [do: 1]
1
end
My.macro { 1, 2, 3, 4, 5 } #=> {:{}, [line: 20], [1, 2, 3, 4, 5]}
My.macro do: ( a = 1; a + a ) #=>
# [do: {:__block__, [],
# [{:=, [line: 22], [{:a, [line: 22], nil}, 1]},
# {:+, [line: 22], [{:a, [line: 22], nil}, {:a, [line: 22], nil}]}]}]
My.macro do #=> [do: {:+, [line: 24], [1, 2]}, else: {:+, [line: 26], [3, 4]}]
1 + 2
else
3 + 4
end
end
quote and unquote
値をinjectするには2通りの方法がある。1つはunquote。
defmodule My do
defmacro macro(code) do
quote do
IO.inspect code
end
end
end
の世界では、 quote で囲まれた範囲はregular codeとしてパースされる。そのため、codeはリテラルとして処理されることになり、CompileErrorとなる。
そのため、regular codeではないことを明示するために、 unquote を使う。
defmodule My do
defmacro macro(code) do
quote do
IO.inspect unquote(code)
end
end
end
これにより、
defmodule Test do require My My.macro(IO.puts "hello") end
のように使った時、”hello” が出力されるようになる。
これは、Elixirが与えられた値をパースしているときに、 unquote にぶつかったらパースを一旦止め、その unquote されている値のパラメータを生成されたコードにそのまま渡す。その後、またパースを再開する。という挙動をするためです。
if と unless をmacroで書くと以下。unlessは、ifに対して反対の判定結果を与えることで実現が可能。
defmodule My do
defmacro if(condition, clauses) do
do_clause = Keyword.get(clauses, :do, nil)
else_clause = Keyword.get(clauses, :else, nil)
quote do
case unquote condition do
val when val in [false, nil] -> unquote else_clause
_otherwise -> unquote do_clause
end
end
end
defmacro unless(condition, clauses) do
quote do
if(!unquote(condition), unquote(clauses))
end
end
end
quoteとbind_quote
通常、 unquote を使って変数の代入などするのですが、 :bind_quote オプションを付与することで、そのquote内では束縛された変数はすべてunquoteなものとして(明示していないけれども)扱うことが可能になる。
:bind_quoted can be used in many cases and is seen as good practice, not only because it helps us from running into common mistakes but also because it allows us to leverage other tools exposed by macros, such as unquote fragments discussed in some sections below.
defmodule My do
defmacro mydef(name) do
quote bind_quoted: [name: name] do
def unquote(name)(), do: unquote(name)
end
end
defmacro squared(x) do
quote bind_quoted: [x: x] do
x * x
end
end
end
# invoke without bind_quote
# ** (CompileError) eg.exs:11: invalid syntax in def xA()
# (elixir) src/elixir_def.erl:44: :elixir_def.store_definition/6
# (elixir) lib/enum.ex:537: Enum."-each/2-lists^foreach/1-0-"/2
# (elixir) lib/enum.ex:537: Enum.each/2
# eg.exs:9: (file)
# invoke without bind_quote
# ** (CompileError) eg.exs:28: function x/0 undefined
# (stdlib) lists.erl:1337: :lists.foreach/2
# eg.exs:23: (file)
# (elixir) lib/code.ex:307: Code.require_file/2
なるほど。少し頭にすんなり入りませんでしたが、理解できた気がします。
Invoke quoted codes
iex(1)> frag = quote do: IO.puts "hello"
{{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [], ["hello"]}
iex(2)> Code.eval_quoted frag
hello
{:ok, []}
iex(3)>
iex(7)> frag = Code.string_to_quoted("defmodule A do def b(c) do c+1 end end")
{:ok,
{:defmodule, [line: 1],
[{:__aliases__, [counter: 0, line: 1], [:A]},
[do: {:def, [line: 1],
[{:b, [line: 1], [{:c, [line: 1], nil}]},
[do: {:+, [line: 1], [{:c, [line: 1], nil}, 1]}]]}]]}}
iex(8)> Macro.to_string(frag)
"{:ok, defmodule(A) don def(b(c)) don c + 1n endnend}"
iex(9)> Code.eval_string("[a, a*b, c]", [a: 2, b: 3, c: 4])
{[2, 6, 4], [a: 2, b: 3, c: 4]}
importした以降だけのScopeなのは、まだわかりやすいかな。
defmodule Operators do
defmacro a + b do
quote do
to_string(unquote(a)) <> to_string(unquote(b))
end
end
end
defmodule Test do
IO.puts 123 + 456 #=> "579"
import Kernel, except: [+: 2]
import Operators
IO.puts 123 + 456 #=> "123456"
end
IO.puts 123 + 456 #=> "579"
Macroで定義されている操作には、以下があるとのこと。
iex(1)> require Macro iex(2)> Macro.binary_ops [:===, :!==, :==, :!=, :=, :&&, :||, :, :++, :--, :\\, :::, :, :=~, :, :->, :+, :-, :*, :/, :=, :|, :., :and, :or, :when, :in, :~>>, :<, :<~, :, :, :<<>>, :|||, :&&&, :^^^, :~~~] iex(3)> Macro.unary_ops [:!, :@, :^, :not, :+, :-, :~~~, :&]
- http://elixir-lang.org/docs/stable/elixir/Code.html
- http://elixir-lang.org/docs/stable/elixir/Macro.html
Elixirでは、モジュールの振る舞いを、macroの __using__ を使って簡単にinjectできる。
defmodule Tracer do
defmacro def(definition, do: content) do
quote do
Kernel.def(unquote(definition)) do
IO.puts "==> call: "
result = unquote(content)
IO.puts "<==== result: #{result}"
result
end
end
end
defmacro __using__(_ops) do
quote do
import Kernel, except: [def: 2]
import Tracer, only: [def: 2]
end
end
end
defmodule Test do
use Tracer
def neko(a), do: IO.puts a
end
Test.neko 5 #=>
# ==> call:
# 5
# <==== result: ok
このような感じで、対象となるモジュールを use なりで読み込んだら、その __using__ が読み込まれ、ここでは Test モジュールに対して Tracer の __using__ でinjectされた内容が常に反映されるようになる。
これにより、ボイラープレートやコードの重複を防ぐことができるようになる。
Protocols
Elixirはクラスが無いので、継承で使うような、親の持つ関数を子が共通して使う、ということができません。その問題に対してprotocolを使って解決しようとしています。Getting Startedにもありましたね。
protocolを使って、複数の defimpl を用意する
# Define defprotocol somewhere.
defprotocol Inspect do
@fallback_to_any true
def inspect(things, opts)
end
# Implement protocol for particuler PID
defimpl Inspect, for: PID do
def inspect(pid, _opts) do
"my new inspect for pid #PID " <> :erlang.iolist_to_binary(:erlang.pid_to_list(pid))
end
end
# Be able to set target for multiple types
defimpl Inspect, for: [List, Integer, Float] do
def inspect(item, _opts) do
"for any multi types #{item}"
end
end
defimpl Inspect, for: Atom do
def inspect(atom, _opts) do
"my new inspect for atom is " <> :erlang.iolist_to_binary(:erlang.atom_to_list(atom))
end
end
# my.exs:2: warning: redefining module Inspect
# my.exs:7: warning: redefining module Inspect.PID
# my.exs:13: warning: redefining module Inspect.Atom
#
# iex(1)> inspect self
# "my new inspect for pid #PID<0.70.0>"
# iex(2)> inspect :neko
# "my new inspect for atom is neko"
ここで、 for で指定可能なタイプは以下
- Any, Atom, BitString, Float, Function, Integer, List, PID, Port, Record, Reference, Tuple
protocolを使って構造体へのAccessを実装する
このprotocolを使って、構造体を持つモジュールへのアクセスを実装すると、親から継承した構造体を使うような感じでモジュールの構造体を利用することもできる。
defmodule Sample do
defstruct value: 0
# built-in protocol: http://elixir-lang.org/docs/stable/elixir/Access.html
# If implement access to %Sample{}, then the Sample module looks as a inherited struct like object oriented language
defimpl Access do
def get(container, key) do
# impliment something
end
def get_and_update(container, key, fun) do
# impliment something
end
end
# built-in protocol: http://elixir-lang.org/docs/stable/elixir/Enumerable.html
defimpl Enumerable do
def count(collection) do
# impliment something
end
def member?(collection, value) do
# impliment something
end
def reduce(collection, acc, fun) do
# impliment something
end
end
# Others...
# inspect, String.Chars and so on...
end
dialyzer
静的解析ツールとしての dialyzer
dialyzer –build_plt –apps erts kernel stdlib mnesia
try/catch/raise/rescueによるエラーハンドリング
Elixir/Erlangに対して、適切にエラーを処理したいときが多々有ります。そんなとき、以下のようにtry/catch/raise/rescueを使って処理を分けます。
defmodule Sample do
def neko(n) do
try do
raise_error(n)
rescue
# error処理
after
# 後処理
end
end
def inu(n) do
try do
# 何かの処理
catch
:exit, code -> # exitシグナルを得たときの処理
:throw, value -> # throwでなんらかの値を得たとき
wath, value -> # 上記以外
end
end
end
ひとまず、何かするにしても最低限必要な書物と知識は追いついたかな、という感じ。
1 Comment