『Programming Elixir: Functional, Concurrent, Pragmatic, Fun』を読んでいます。前回の続きでI-6章からI-13章までです。
環境は OSX, Elixir 1.0.2です。
本投稿における間違いは私の理解不足に依るものなので, 本書の内容の正確性とは一切関係ありません。また, 英語が苦手なので誤訳もあるかと思います。
6. Modules and Named Functions
Timesモジュールに, 引数の値を2倍して返す double関数を持たせる。
defmodule Times do
def double(n) do
n * 2
end
end
モジュールのコンパイル方法は2種類紹介されている。まずは, iex にファイルを指定して loadするやり方。
$ iex times.exs
iex(1)> Times.double(2)
4
REPLの中では c でimportができる。
doubleに文字列を渡すと例外 ArithmeticErrorが挙がる。
$ iex
iex(1)> c "times.exs"
[Times]
iex(2)> Times.double(3)
6
iex(3)> Times.double("cat")
** (ArithmeticError) bad argument in arithmetic expression
times.exs:3: Times.double/1
続いて, do記法について。
do:の syntactic sugar (糖衣構文)としてdo…endブロックがある。下記のように do: で1行で書く場合や, 複数行でもdo:ブロックを()で囲う時は endはなくても良い。
def double(n), do: n * 2
do…endブロックを使った場合。
defmodule Factorial do
def of(0), do: 1
def of(n), do: n * of(n-1)
end
デフォルトパラメータは param \\ value の構文で書く。
例として, 引数2つの足す funcを持つ Exampleモジュールを定義する。
defmodule Example do
def func(a \\ 10, b \\ 20) do
a + b
end
end
$ iex param_example.exs
iex(1)> Example.func(1)
21
iex(2)> Example.func(1,2)
3
Exampleモジュールを拡張して, デフォルトパラメータや型によって関数の中身を変えてみる。
defmodule Example do
def func(a, b \\ 20)
def func(a, b) when is_list(a) do
"You said #{b} with a list"
end
def func(a, b) do
"You passed in #{a} and #{b}"
end
end
渡す型をIntegerとStringと変えて振る舞いが変化している。
iex(3)> IO.puts Example.func(5)
You passed in 5 and 20
:ok
iex(4)> IO.puts Example.func(5, "cat")
You passed in 5 and cat
:ok
iex(5)> IO.puts Example.func([5], "cat")
You said cat with a list
:ok
iex(6)> IO.puts Example.func([5])
You said 20 with a list
:ok
続いて, pipe演算子について。本文中の pipe演算子の説明を引用する。
“Programming is transforming data, and the |> operator makes that transformation explicit.”
|> は明示的に変換を行う。構文的には, val |> f(a, b) と書き f(val, a, b)と同じである。
例えば, result = buz(bar(foo(DB.find_uer)))と () がネストしていくような書き方は読み辛くなってしまう。 |> は読みやすい。
result = DB.find_uer
|> foo
|> bar
|> buz
Enum.map と組み合わせた例。
iex(2)> (1..10) |> Enum.map(&(&1*&1))
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
次は, モジュールの名前空間について。
defmodule Outer do
defmodule Inner do
def func1 do
end
end
def func2 do
Inner.func1
end
end
Outer.func2 や Outer.Inner.func1 のようにネストされたモジュールにdot演算子でアクセスできる。
また, ネストされたモジュールを直接定義することもできる。下記では, Foo.Bar.buzでアクセスできる
defmodule Foo.Bar do
def buz do
end
end
モジュールを動かすための Directiveとして alias, import, requireの3つある。aliasで モジュールを使う例。
iex(4)> alias Foo.Bar, as: Example
iex(5)> Example.buz
7. Lists and Recursion
再帰構造の例として, 独自のListモジュールの関数を書いてみる。
[head | tail]を使って, Listの最初の要素を計算して, 残ったtailを再帰的に自身に渡していく。
defmodule RecursiveList do
def len([]), do: 0
def len([_head | tail]), do: 1 + len(tail)
def sum([], total), do: total
def sum([ head | tail ], total), do: sum(tail, head+total)
def square([]), do: []
def square([ head | tail ]), do: [ head*head | square(tail) ]
def map([], _func), do: []
def map([ head | tail ], func), do: [ func.(head) | map(tail, func) ]
def reduce([], value, _), do: value
def reduce([head | tail], value, func), do: reduce(tail, func.(head, value), func)
end
関数型言語なので, この辺がすっきり書けるのは良い感じ。
iex(1)> RecursiveList.len(["dog", "cat", "bird"])
3
iex(2)> RecursiveList.sum([1,2,3], 0)
6
iex(3)> RecursiveList.square([1,2,3,4,5])
[1, 4, 9, 16, 25]
iex(4)> RecursiveList.map([1,2,3,4,5], fn (n) -> n > 3 end)
[false, false, false, true, true]
iex(5)> RecursiveList.reduce([1,2,3,4,5], 1, &(&1*&2))
120
もう少し複雑なListのパターン。Listの先頭の2つの要素を入れ替える例。
# https://media.pragprog.com/titles/elixir/code/lists/swap.exs
defmodule Swapper do
def swap([]), do: []
def swap([ a, b | tail ]), do: [ b, a | swap(tail) ]
def swap([_]), do: raise "Can't swap a list with an odd number of elements"
end
Listの要素数が奇数の場合, swapできずに例外を挙げるようになっている事を確認してみる。
iex(1)> c "swapper.exs"
[Swapper]
iex(3)> Swapper.swap([1,2,3,4,5])
** (RuntimeError) Can't swap a list with an odd number of elements
swapper.exs:4: Swapper.swap/1
swapper.exs:3: Swapper.swap/1
iex(3)> Swapper.swap([1,2,3,4])
[2, 1, 4, 3]
8. Dictionaries: Maps, HashDicts, Keywords, Sets, and Structs
Dictionaryから hashDicts, Mapへの型変換。
iex(1)> dict = [name: "fisproject", likes: "Programming", where: "tokyo", likes: "cat"]
[name: "fisproject", likes: "Programming", where: "tokyo", likes: "cat"]
iex(2)> hash = Enum.into dict, HashDict.new
#HashDict<[name: "fisproject", where: "tokyo", likes: "cat"]>
iex(3)> map = Enum.into dict, Map.new
%{likes: "cat", name: "fisproject", where: "tokyo"}
dictの要素へのアクセスはget, get_valuesなどがある。get_valuesは複数の値が取れる。
iex(4)> dict[:likes]
"Programming"
iex(5)> Dict.get(dict, :likes)
"Programming"
iex(6)> Keyword.get_values(dict, :likes)
["Programming", "cat"]
mapとパターンマッチを使った例として BMIを計算する関数を書いた。
本文中では確か, ホテルのベッドサイズを身長から求める関数だったと思う。
people = [
%{name: "bob", height: 1.75, weight: 90},
%{name: "jack", height: 1.7, weight: 66},
%{name: "james", height: 1.8, weight: 50}
]
defmodule BMI do
def calc(%{name: name, height: height, weight: weight})
when 25 < (weight / (height * height)) do
IO.puts "#{name}'s BMI is high"
end
def calc(%{name: name, height: height, weight: weight})
when 18.5 > (weight / (height * height)) do
IO.puts "#{name}'s BMI is low"
end
def calc(%{name: name}) do
IO.puts "#{name}'s BMI is normal"
end
end
呼び出す方も, pipe演算子とEnum.eachですっきり書ける。
people |> Enum.each(&BMI.calc/1)
bob's BMI is high
jack's BMI is normal
james's BMI is low
Mapに限らず Elixirの全ての値は不変であるので Mapの更新も new mapを作っている。
iex(10)> map = %{a: 1, b: 2, c: 3}
%{a: 1, b: 2, c: 3}
iex(11)> map1 = %{ map | b: "two", c: "three"}
%{a: 1, b: "two", c: "three"}
モジュールの中で Structureを定義するには, defstructを使う。
Dictionary Structures はネストすることができ, company.prefecture.city のようにdot記法でアクセスできる。
続いて, Elixirの Kernelによるマクロや関数について。
ネストされた構造体に対して値を取得する get_in, 値を置く put_inの使い方。
nested = %{
today: %{
name: %{
first: "tarou",
last: "tanaka"
},
age: 21
},
yesterday: %{
name: %{
first: "toru",
last: "satou"
},
age: 19
}
}
IO.inspect get_in(nested, [:today])
# %{age: 21, name: %{first: "tarou", last: "tanaka"}}
IO.inspect get_in(nested, [:today, :name])
# %{first: "tarou", last: "tanaka"}
IO.inspect get_in(nested, [:today, :name, :first])
# "tarou"
9. An Aside—What Are Types?
Elixirのプリミティブな型とモジュールに関する補足という内容。
プリミティブなlist型は, […]リテラルでlistを作ったり, | 演算子はlistを分解して再度作る機能がある。また別のレイヤーとして, Listモジュールにはlistsを操作する関数が用意されている。
10. Processing Collections—Enum and Stream
EnumモジュールとStreamモジュールについて。
まずはEnumモジュール。CollectionのListへの変換, 定番系のconcat, map, filter, sort関数がある。
iex(1)> list = Enum.to_list 1..5
[1, 2, 3, 4, 5]
iex(2)> Enum.concat([1,2,3], [4,5,6])
[1, 2, 3, 4, 5, 6]
iex(3)> Enum.map(list, &(&1*&1))
[1, 4, 9, 16, 25]
iex(4)> Enum.map(list, &String.duplicate("*", &1))
["*", "**", "***", "****", "*****"]
iex(5)> Enum.filter(list, &(&1 > 3))
[4, 5]
iex(6)> Enum.sort ["a", "b", "d", "e", "c"]
["a", "b", "c", "d", "e"]
古そうで新鮮に感じたのは, ポジションを指定して要素を取り出す at関数。
iex(6)> Enum.at(10..20, 3)
13
max関数は文字列に対しては, Zに最も近い文字を返す。minの場合だと逆になりAを返す。
iex(7)> Enum.max ["a", "b", "d", "e", "c"]
"e"
iex(8)> Enum.min ["a", "b", "d", "e", "c"]
"a"
take, split, join, zip。
iex(9)> Enum.take(list, 2)
[1, 2]
iex(10)> Enum.split(list, 3)
{[1, 2, 3], [4, 5]}
iex(11)> Enum.join list
"12345"
iex(12)> Enum.zip(list, [:a, :b, :c])
[{1, :a}, {2, :b}, {3, :c}]
続いて, Elixirの特徴のひとつであるStream機能はStreamモジュールで提供される。本文中のStreamの説明を引用,
“A Stream Is a Composable Enumerator”
そのまま訳すと, Streamは合成可能なEnumerator (列挙子)である, となる。
iex(1)> stream = Stream.map [1,3,5,7], &(&1+1)
#Stream<[enum: [1, 3, 5, 7], funs: [#Function<45.29647706/1 in Stream.map/2>]]>
iex(2)> Enum.to_list stream
[2, 4, 6, 8]
iex(3)> odds = Stream.filter [1,2,3,4,5], fn x -> rem(x,2) == 1 end
#Stream<[enum: [1, 2, 3, 4, 5],
funs: [#Function<39.29647706/1 in Stream.filter/2>]]>
iex(4)> Enum.to_list odds
[1, 3, 5]
Streamで扱えるのは Listだけではない。IO.streamは IO Deviceも streamに変換できる。
遅延評価なので, 1..10_000_100 も必要なだけ計算される。
試しに Streamを Enumに変えると非常に大きい listをメモリ上に実際に作ってしまう。
iex(1)> Stream.map(1..10_000_100, &(&1+1)) |> Enum.take(10)
[2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
Stream.iterateは無限の streamを生成する。
iex(2)> Stream.iterate(0, &(&1+1)) |> Enum.take(50)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41,
42, 43, 44, 45, 46, 47, 48, 49]
iex(3)> Stream.iterate([], &[&1]) |> Enum.take(5)
[[], [[]], [[[]]], [[[[]]]], [[[[[]]]]]]
他にも Stream.cycle, Stream.unfold, Stream.resourceなどの紹介もある。
続いて, Comprehensions (内包表記)について。基本構文は下記。
result = for generator or filter… [, into: value ], do: expression
値の全てのコンビネーションを抜き出して評価し, 新しい Collectionを生成する。2行目の例は25回繰り返される。
iex(1)> for x <- [1,2,3,4,5], do: x * x
[1, 4, 9, 16, 25]
iex(2)> for x <- [1,2,3,4,5], y <- [1,2,3,4,5], x >= y, rem(x*y, 10) == 0, do: {x,y}
[{5, 2}, {5, 4}]
11. Strings and Binaries
stringは single-quoted formと double-quoted formの2種類があり内部表現は異なるが, 共通点は多い。
single-quoted formはキャラクタlist, double-quoted formは stringsとなる。single-quoted formはキャラクタのlistなので, Listの関数が使える。
例えば, to_tupleで個々のキャラクタの文字表現から 10進数表現が得られる。
iex(1)> single = 'elixir'
'elixir'
iex(2)> is_list single
true
iex(3)> length single
6
iex(4)> List.to_tuple single
{101, 108, 105, 120, 105, 114}
binaryリテラルは <<…>> と書く。
用途としては様々だけど, マルチメディアやネットワークプログラミングのパケットを扱う時などには bit単位でデータを処理する必要があったりする。n-bitで表現したい時, size::(n) と書ける。
iex(5)> bit = << 1::size(2), 1::size(3)>>
<<9::size(5)>>
iex(6)> byte_size bit
1
iex(7)> bit_size bit
5
Elixirに限らないかもしれないが, Elixirにはいくつかの構文に代替の構文がある。
例えば, 正規表現は ~r{…}を使ってかける。Elixirでは ~スタイルのリテラルは sigils (a symbol with magical powers)と呼ばれる。
~wは, whitespaceで区切ったエスケープされている単語listの代替構文。
iex(3)> ~w[the cat sat]
["the", "cat", "sat"]
iex(4)> ~w"""
...(4)> the
...(4)> cat
...(4)> sat
...(4)> """
["the", "cat", "sat"]
iex(5)> ~w[the c#{'a'}t sat]a
[:the, :cat, :sat]
本章では, 他にもStringモジュールやビッグ/リトルエンディアンの話題も紹介されている。
12. Control Flow
制御フローについて, if, unlessの使い方。
iex(1)> if 1 == 2, do: "true", else: "false"
"false"
iex(2)> unless 1 == 2, do: "true", else: "false"
"true"
FizzBuzz問題を Elixirでエレガントに書いてみる例。defp は private function。
defmodule FizzBuzz do
def upto(n) when n > 0 do
1..n |> Enum.map(&fizzbuzz/1)
end
defp fizzbuzz(n) when rem(n, 3) == 0 and rem(n, 5) == 0, do: "FizzBuzz"
defp fizzbuzz(n) when rem(n, 3) == 0, do: "Fizz"
defp fizzbuzz(n) when rem(n, 5) == 0, do: "Buzz"
defp fizzbuzz(n), do: n
end
実行してみる。
iex(1)> FizzBuzz.upto(15)
[1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz"]
ガード条件 (Guard Clause)の例。
defmodule Users do
def live(user) do
case user do
%{state: some_state} = person ->
IO.puts "#{person.name} lives in #{some_state}"
_ ->
IO.puts "No matches"
end
end
end
_ (underscore) はそれ以外の全てにパターンマッチする。
iex(2)> Users.live(%{ name: "Dave", state: "TX", likes: "programming" })
Dave lives in TX
:ok
iex(3)> Users.live(%{ name: "Dave", likes: "programming" })
No matches
:ok
raise関数で例外エラーを挙げることができる。メッセージは必須で, オプションで例外の型を指定する。
iex(1)> raise "Oops"
** (RuntimeError) Oops
iex(1)> raise RuntimeError, message: "Oops!"
** (RuntimeError) Oops!
13. Organizing a Project
今回は文法を覚えることが目標なので, Elixir Projectのデザインと周辺ツールを紹介する本章は短い紹介に留めておく。Mixは build tool, ExUnitはUnit Testing Framework。
ExDocはRubyのRocのようなDocumentation toolで mix.exs に名前や, GitHubリポジトリ, 依存ライブラリなどを書いておくと mix deps.get で依存ライブラリを取得したり, mix docs でドキュメントを生成したりできる。
おわりに
13章は結構ページ数も割いているので, ガッツリElixirで何かつくりたい方は読んでおくと良いかもしれません。
改めてBlogの内容を見返してみると, 断片的なメモを繋ぎ合わせた適当な感じになってしまってたので, 気になった方は是非本書を手に取って読んでみて下さい。
[1] Programming Elixir
[2] Elixir ご紹介