DEV Community

gumi TECH for gumi TECH Blog

Posted on • Updated on

Elixir入門 17: 内包表記

本稿はElixir公式サイトの許諾を得て「Comprehensions」の解説にもとづき、加筆補正を加えて、Elixirで使える内包表記の構文についてご説明します。

Elixirでは列挙可能なデータをループして取り出し、フィルタリングして、他のリストに値をマッピングするといったことがよくあります。内包表記はそうした処理の糖衣構文です。for/1を用いてつぎの3つの要素で組み立てます。

  • ジェネレータ
  • フィルタ
  • コレクタブル

たとえば、つぎのコードは整数のリストの値を2乗してマッピングします。

iex> for n <- [1, 2, 3, 4, 5], do: n * n
[1, 4, 9, 16, 25]
Enter fullscreen mode Exit fullscreen mode

ジェネレータ

もととなるデータ構造から値を取り出す式がジェネレータです。内包表記で<-の右辺からつくられた値が、順に左辺に渡されます。列挙型のデータであれば、ジェネレータ式の右辺に置けます。

iex> for n <- 1..5, do: n * n
[1, 4, 9, 16, 25]
Enter fullscreen mode Exit fullscreen mode

キーワードリストやマップ、バイナリなどもジェネレータで扱えます。

iex> for {_key, val} <- [one: 1, two: 2, three: 3], do: val
[1, 2, 3]
iex> for {key, val} <- %{one: 1, two: 2, three: 3}, do: {key, val}
[one: 1, three: 3, two: 2]
iex> for <<char <- "hello">>, do: <<char>>
["h", "e", "l", "l", "o"]
Enter fullscreen mode Exit fullscreen mode

ジェネレータ式の左辺にはパターンマッチングが使えます。パターンに一致しない値は無視されます。たとえば、キーワードリストのキーをパターンに用いたのがつぎの例です。

iex> numbers = [identity: 1, prime: 2, prime: 3, normal: 4, prime: 5]
[identity: 1, prime: 2, prime: 3, normal: 4, prime: 5]
iex> for {:prime, n} <- numbers, do: n * n
[4, 9, 25]
Enter fullscreen mode Exit fullscreen mode

ジェネレータは複数使えます。

iex> for i <- [:a, :b, :c], j <- [1, 2], do:  {i, j}
[a: 1, a: 2, b: 1, b: 2, c: 1, c: 2]
Enter fullscreen mode Exit fullscreen mode

複数のジェネレータは、入れ子のループと捉えられます。

iex> for i <- [1, 2, 3], j <- 1..i, do: [i: i, j: j]
[
  [i: 1, j: 1],
  [i: 2, j: 1],
  [i: 2, j: 2],
  [i: 3, j: 1],
  [i: 3, j: 2],
  [i: 3, j: 3]
]
Enter fullscreen mode Exit fullscreen mode

なお、内包表記の中における変数の代入は、外には影響を与えません。

フィルタ

ジェネレータから取り出す値は、フィルタで絞り込めます。フィルタとして定めるのは関数です。戻り値がfalseでもnilでもない値だけが使われます。

iex> multiple_of_3? = fn(n) -> rem(n, 3) == 0 end
#Function<6.99386804/1 in :erl_eval.expr/5>
iex> for n <- 0..10, multiple_of_3?.(n), do: n * n
[0, 9, 36, 81]
Enter fullscreen mode Exit fullscreen mode

フィルタは内包表記におけるガードと捉えればよいでしょう。組み込みの関数もフィルタとして用いることができます。

iex> import Integer
Integer
iex> for n <- 0..10, is_even(n), do: n
[0, 2, 4, 6, 8, 10]
Enter fullscreen mode Exit fullscreen mode

ジェネレータと同じく、フィルタも複数与えられます。

iex> for n <- 0..100,
...>   is_odd(n),
...>   rem(n, 7) == 0,
...> do: n
[7, 21, 35, 49, 63, 77, 91]
Enter fullscreen mode Exit fullscreen mode

内包表記を用いると、EnumStreamモジュールの関数を使う処理がずっと簡潔に書けます。さらに、ジェネレータやフィルタをいくつも加えることができるのです。つぎのコードはディレクトリのリストからファイルのパスを探し、標準ファイルであることを確かめたうえで、それぞれのサイズをリストで出力します(図001)。

# example.exs
dirs = ['home/mickey', 'home/minnie']
for dir  <- dirs,
    file <- File.ls!(dir),
    path = Path.join(dir, file),
    File.regular?(path) do
  IO.puts(File.stat!(path).size)
end
Enter fullscreen mode Exit fullscreen mode
$ elixir example.exs
6
7
Enter fullscreen mode Exit fullscreen mode

図001■ディレクトリ内のファイル

elixir_17_001.png

ビットストリングジェネレータ

ビットストリングもジェネレータで扱えます。ビットストリングのストリームを解析するのに便利です。たとえば、ピクセルのRGBカラー成分値をストリームで受け取って、ピクセルごとにRGB成分値のタプルにまとめることもできます。

iex> pixels = <<213, 45, 132, 64, 76, 32, 76, 0, 0, 234, 32, 15>>
<<213, 45, 132, 64, 76, 32, 76, 0, 0, 234, 32, 15>>
iex> for <<r::8, g::8, b::8 <- pixels>>, do: [r: r, g: g, b: b]
[
  [r: 213, g: 45, b: 132],
  [r: 64, g: 76, b: 32],
  [r: 76, g: 0, b: 0],
  [r: 234, g: 32, b: 15]
]
Enter fullscreen mode Exit fullscreen mode

:intoオプション

内包表記はデフォルトではリストを返します。けれども、結果はリスト以外のデータ構造に納めることもできるのです。その場合には:intoオプションでそのデータ構造を与えます。つぎのコードは、文字リストから文字列を得る例です。

iex> for c <- [104, 101, 108, 108, 111], into: "", do: <<c>>
"hello"
Enter fullscreen mode Exit fullscreen mode

つぎの例は、文字列からスペースを除いて、文字列にして返します。

iex> for <<char <- " to be, to be, ten made to be ">>, char != ?\s, into: "", do: <<char>>
"tobe,tobe,tenmadetobe"
Enter fullscreen mode Exit fullscreen mode

:intoオプションには、Collectableプロトコルを実装したデータ構造が与えられます。よく使われるのは、マップのキーはそのままにして値を変える場合です。

iex> for {key, val} <- %{a: 1, b: 2, c: 3}, into: %{}, do: {key, val * val}
%{a: 1, b: 4, c: 9}
Enter fullscreen mode Exit fullscreen mode

IO.stream/2は、入力をIO.Streamにして返します。そして、IO.Streamの実装するプロトコルは、EnumerableCollectableです。つぎのコードは、キーボードから入力した英字をString.upcase/2で大文字にしてシェルに出力します。なお、入力待ちの状態から抜けるには、IExを終了させてください。

iex> stream = IO.stream(:stdio, :line)
%IO.Stream{device: :standard_io, line_or_bytes: :line, raw: false}
iex> for line <- stream, into: stream do
...>   String.upcase(line) <> "\n"
...> end
elixir  #<- 入力
ELIXIR  #<- 出力
Enter fullscreen mode Exit fullscreen mode

Elixir入門もくじ

番外

Discussion (0)