loading...
gumi TECH Blog

Elixir入門 17: 内包表記

gumitech profile image gumi TECH Updated on ・3 min read

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

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

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

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

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

ジェネレータ

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

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

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

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"]

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

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]

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

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

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

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]
]

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

フィルタ

ジェネレータから取り出す値は、フィルタで絞り込めます。フィルタとして定めるのは関数です。戻り値が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]

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

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

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

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

内包表記を用いると、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
$ elixir example.exs
6
7

図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]
]

:intoオプション

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

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

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

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

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

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

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  #<- 出力

Elixir入門もくじ

番外

Posted on by:

gumitech profile

gumi TECH

@gumitech

gumi TECH は、株式会社gumiのエンジニアによる技術記事公開やDrinkupイベントなどの技術者交流を行うアカウントです。 gumi TECH Blog: http://dev.to/gumi / gumi TECH Drinkup: http://gumitech.connpass.com

gumi TECH Blog

株式会社gumiのエンジニアによる技術記事を公開しています。

Discussion

pic
Editor guide