Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

04 関数型言語 Elixir の基礎(コレクション,制御フロー)を学ぶ

notebooks/04-enum.livemd

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の場合、nilfalseが偽でそれ以外(0など)は真です。
  • if文ではなくif式。if式は値を返します。

image

FizzBuzz

3で割り切れる数字の場合はFizz、5で割り切れる場合はBuzz、3でも5でも割り切れる 場合にはFizzBuzzとし、それ以外の場合は数字をそのまま。

ElixirらしくないFizzBuzz

image

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モジュールの関数を利用する