04 関数型言語 Elixir の基礎(コレクション,制御フロー)を学ぶ
コレクション
コレクションを扱うモジュール
- Enum モジュール(Elixir で最も使用頻度が高い)
- Map モジュール
リスト
- 異なる型の値を格納できる(Linked)リスト
[1, 2, "elixir", "python", :a, true, 3.14]
マップ
- キーと値からなるデータ構造
%{"name" => "osamu", "age" => 44, "height" => 170}
%{name: "osamu", age: 44, height: 170}
Enum モジュール
- リストやマップなどのコレクションを操作する関数群です。
-
Enum モジュールの代表的な関数
-
Enum.map/2
-
Enum.filter/2
-
Enum.reduce/3
-
Enum.at/3
-
Enum.map/2 (コレクションの要素を変換)
Enum.map([1, 2, 3], fn x -> x * 2 end)
Enum.map(1..3, fn x -> x * 3 end)
Enum.map(%{a: 1, b: 2, c: 3}, fn {_k, v} -> v end)
Enum.filter/2 (コレクションをフィルタリング)
-
各要素を第 2 引数の関数に適用して、実行結果が Truthy(
nil
,false
ではない)となる要素だけをフィ ルタリングする。 - 0 個〜元のコレクションの長さと同じリストが得られる。
Enum.filter([1, 2, 3], fn x -> rem(x, 2) == 0 end)
Enum.filter(1..3, fn x -> x > 0 end)
Enum.filter(%{a: 1, b: 2}, fn {_k, v} -> v > 1 end)
Enum.reduce/3 (畳み込み)
- 第 2 引数は初期値。第 3 引数は要素とアキュムレータ(それまでの計算結果)を引数とする関数。
-
第 3 引数の関数の戻り値の型が、
Enum.reduce/3
の結果となる。
Enum.reduce([1, 2, 3], 0, fn x, acc -> x + acc end)
Enum.reduce(%{a: 2, b: 3, c: 4}, 0, fn {_key, val}, acc -> acc + val end)
f = fn v, acc -> Map.update(acc, v, 1, fn cnt -> cnt + 1 end) end
Enum.reduce([:cat, :dog, :dog, :dog, :cat, :bird], %{}, f)
Enum.at/3(インデックスによる値の取り出し)
- インデックスは 0 はじまり。-1 は末尾。
- 第 1 引数の要素が 1 つ返ります。
-
存在しない場合は
nil
または 指定したデフォルト値。
Enum.at([2, 4, 6], 0)
Enum.at([2, 4, 6], -1)
Enum.at([2, 4, 6], 4)
Enum.at([2, 4, 6], 4, 0)
Map モジュール
- マップを操作する関数群です。
-
Map モジュールの代表的な関数
- Map.get/3
- Map.update/4
- Map.merge/3
- Map.keys/1
- Map.values/1
- Map.take/2
Map.get/3(マップから値を取り出す)
- 第 2 引数には、キーを指定する
- 第 2 引数に指定したキーに対応する値か
-
キーが存在しない場合には
nil
もしくは第 3 引数に指定したデフォルト値が返る
Map.get(%{}, :a)
Map.get(%{a: 1}, :a)
Map.get(%{a: 1}, :b)
Map.get(%{a: 1}, :b, 3)
Map.update/4 (マップの更新)
- 第 2 引数にはキー、第 3 引数には値、第 4 引数には値を調停する関数を指定する。
- 第 1 引数に指定したマップが更新されたマップ(内部では新しく作られています)が返ります。
Map.update(%{a: 1}, :a, 13, fn existing_value -> existing_value * 2 end)
Map.update(%{a: 1}, :b, 11, fn existing_value -> existing_value * 2 end)
Map.update(%{dog: 1}, :cat, 1, fn existing_value -> existing_value + 1 end)
Map.update(%{dog: 1}, :dog, 1, fn existing_value -> existing_value + 1 end)
Map.merge/3 (マップのマージ)
- 第 1 引数と第 2 引数に指定されたマップをマージしたマップが得られます。
- 後勝ちです。第 3 引数の関数で値の調停をできます。
Map.merge(%{a: 1, b: 2}, %{a: 3, d: 4})
第 1 引数と第 2 引数ともにマップを指定する。後勝ち。
Map.merge(%{a: 1, b: 2}, %{a: 3, d: 4}, fn _k, v1, v2 -> v1 + v2 end)
第 3 引数には値を調停する関数を指定する。
Map.keys/1(マップからキーの取り出し)
- キーのリストが得られます。
Map.keys(%{a: 1, b: 2})
Map.values/1(マップから値の取り出し)
- 値のリストが得られます。
Map.values(%{a: 1, b: 2})
Map.take/2 (マップから指定されたキーのみのマップを作る )
- マップから指定されたキーのみのマップを得られます。
Map.take(%{a: 1, b: 2, c: 3}, [:a, :c, :e])
Map.take(%{user: "osamu", url: "http://example.com", age: 44}, [:user, :age])
制御フロー
-
if 条件式 do 〜 else end
があります。 -
else if
はありません。 -
else
の中にif
を書くことはできます。 -
Elixirの場合、
nil
とfalse
が偽でそれ以外(0
など)は真です。 -
if
文ではなくif
式。if
式は値を返します。
FizzBuzz
3で割り切れる数字の場合はFizz、5で割り切れる場合はBuzz、3でも5でも割り切れる 場合にはFizzBuzzとし、それ以外の場合は数字をそのまま。
ElixirらしくないFizzBuzz
ElixirらしいFizzBuzz
defmodule FizzBuzz do
def fizz_buzz(n) do
do_fizz_buzz(rem(n, 3), rem(n, 5), n)
end
defp do_fizz_buzz(0, 0, _n), do: "FizzBuzz"
defp do_fizz_buzz(0, _, _n), do: "Fizz"
defp do_fizz_buzz(_, 0, _n), do: "Buzz"
defp do_fizz_buzz(_, _, n), do: n
def say(n) do
1..n |> Enum.map(fn i -> fizz_buzz(i) end) |> Enum.join(", ")
end
end
FizzBuzz.say(15)
解説
-
defp
はプライベートメソッドの定義。FizzBuzz
モジュール内でのみ利用できる。 -
defp do_fizz_buzz(◯, △, ◎), do: xxx
は、do〜end
を省略した書き方です -
変数の
_
は未使用であることを明示しています -
fizz_buzz
関数では、do_fizz_buzz
関数を呼び出しています。do_fizz_buzz
関数には引数で与えられた値を3で割った余りと5で割った余り、値そのものを渡しています -
do_fizz_buzz
は4つ定義されており、最初にパターンマッチした関数が実行されます -
if 条件 do 〜 else 〜 end
だけですむ場合はif
を使うこともありますが条件が複雑になるときは関数のパターンマッチでif
を代用するのがElixirらしい書き方です
Elixir における順次、分岐、繰り返し
どんなプログラミング言語も基本は「順次」、「分岐」、「繰り返し」です。Elixirではどのように書くのかを整理しました。
-
順次
- プログラムは上から下に実行される
-
パイプ演算子(
|>
)を用いて書くとElixirらしくなる
-
分岐
-
if
などで分岐することも可能 - 複雑な分岐は関数の引数でパターンマッチする書き方がElixirらしくなる
-
-
繰り返し
- データ(コレクション)ありき
- Enumモジュールの関数を利用する