Enumモジュールと|>演算子

  • このページの目標
    • Enumモジュールに触れ、頻出するデータ操作を知っておく
    • |>演算子を知り、よくあるデータ操作のコードを読み書きできるようになる
  • 所要時間: 15 分程度

資料: Enumerables and Streams - Elixir

  • 関数型言語では「データと、それを扱う関数」がプログラムのほぼ全てであると前のページで書きましたが、 そのために Elixir は標準ライブラリの中に基本的なデータ操作モジュールを備えています
  • Enumはその中でも代表的なもので、「数え上げられるデータ; Enumerable」を扱う関数を提供します
  • 以下で例に上げるEnum.map/2は非常に利用頻度の高い関数の 1 つです
iex> Enum.map([1, 2, 3], fn x -> x * 2 end)
[2, 4, 6]
iex> Enum.map(%{1 => 2, 3 => 4}, fn {k, v} -> k * v end)
[2, 12]
iex> Enum.map(1..3, fn x -> x * 2 end)
[2, 4, 6]
  • 1..3は range 構文というもので、整数の連番リストを簡単に定義できます

  • プログラミングを学んだことがあれば、言語にもよりますが、for文やwhile文で配列等に対するループ処理を書いたことがあると思います
  • Elixir にはまず、while文が文法・標準ライブラリにありません。forマクロはありますが、C 等のfor文とはだいぶ違います
  • Enumerable な値の要素に対する処理は、
    • 再帰関数として書くか、
    • Enumモジュールの関数を使うか、
    • forマクロを使うか、が主たる方法です
  • 再帰関数についてはRecursion - Elixirを参照
    • 少し高度な内容なので、後回しで構いません
  • forマクロは、いわゆる「内包表記; comprehension」と呼ばれるスタイルを実現するものです
    • Comprehensions - Elixirを参照
    • 結構便利なのですが、これもまたちょっと高度な内容なので後回しで OK

  • Enum.map/2は、対象とするデータと、対象とするデータの各要素に適用したい処理を関数として受け取ります
  • そして、受け取った関数を各要素に適用し、いわば変換したデータを返します
  • このような、「関数を受け取る関数」を高階関数と言ったりします
  • 「データと、それに対する処理」をより宣言的に書け、カウンタ変数を利用した記述と比べて off-by-one バグを生みにくい利点があります
  • こういったデータ処理を関数の組み合わせで表現するのは関数型言語において基本的なスタイルと言えます

  • Enum.reduce/3はもう一つの代表的な Enum 関数です。 ちょっと難しいですが見てみましょう
iex> Enum.reduce(1..3, 0, &+/2)
6
  • 結果としては 1 から 3 までの整数を足し上げているのですが、以下のことが言えます
    • 初期値(この場合0)を受け取っている
    • 前の要素に対する計算結果(この値を accumulator とよく呼ぶ)を次の要素の計算結果に利用して、最終的に 1 つの値を返している
    • 無名関数を書く方法だけでなく、関数や演算子を &name/n記法で指定しても良い
  • 少しわかりやすくすると、こうです
iex> Enum.reduce(1..3, 0, fn num, acc -> num + acc end)
6
  • このような処理を「畳み込み」と呼ぶこともあります。言語によってはfoldという名前で提供されています
  • 他にもEnumには便利な関数がたくさんあります。困ったらEnumを探しましょう

|>; The pipe operator

資料: The pipe operator

  • 「1 から 10 万までの整数をそれぞれ 3 倍したもののうち、奇数であるものの合計値」を求める処理が書きたいとします
  • Enumにはちょうどいい関数が揃っていますので、まず以下のように書けます
iex> odd? = &(rem(&1, 2) != 0)
#Function<6.80484245/1 in :erl_eval.expr/5>
iex> Enum.sum(Enum.filter(Enum.map(1..100_000, &(&1 * 3)), odd?))
7500000000
  • odd?には省略記法で定義した無名関数が束縛されています。奇数を判定する関数ですね
    • rem/2は標準ライブラリが提供する剰余を求める関数です
  • 100_000は単なる整数ですが、読みやすさのためこのように 3 桁ごとに_で区切る表記が推奨されています
  • Enum.map/2&(&1 * 3)を与えて「各要素を 3 倍」し、Enum.filter/2odd?を与えて奇数であるものだけを選び、Enum.sum/1で合計しています

  • しかし()が多くて読みにくい! そこで|>(パイプ演算子; pipe operator)を使うと以下のように書けます
iex> 1..100_000 |> Enum.map(&(&1 * 3)) |> Enum.filter(odd?) |> Enum.sum
7500000000
  • 元データから、処理の順番そのままに、平坦に読めるようになりました
  • |>は実体としてはマクロで、
    • 右辺の関数の第 1 引数を左辺に書き、
    • 右辺の関数は第 1 引数を省略して書くことで、通常の関数呼び出しと同じことを実現できるようにするものです
  • この演算子は別の関数型言語である F#(F Sharp)で登場し、データ処理の流れが読みやすく書けることから他言語にも波及しています

  • 最初になにかデータがあり、そこに連続的に処理を適用していく場合には、このスタイルが読みやすく、頻出ですので覚えておきましょう

results matching ""

    No results matching ""