🌹

リストとタプル -- YouTube チャンネル daimon.ex 第3回解説

2024/04/08に公開

動画

https://www.youtube.com/watch?v=e_e2wDB5Yhg

リストとタプル

Elixir にはリストタプルというよく似た概念があります。どちらも複数の要素をまとめて扱うための概念です。集合の一種です。要素の型はばらばらでも構いません。

動画では、3, "Hello", true という3要素を持つリストとタプルを例として出しました。

  • [3, "Hello", true]
  • {3, "Hello", true}

各要素をコンマで区切って角括弧([ ])で囲むとリストになり、各要素をコンマで区切って波括弧({ })で囲むとタプルになります。

リストとタプルの違いとしては、次の2点が重要です。

  • 任意の位置の要素を取得するとき、リストは遅く、タプルは速い。
  • 先頭に要素を追加するとき、リストは速く、タプルは遅い。

関数 File.read/1

動画中で紹介した関数 File.read/1 は第1引数にファイルのパス、戻り値として2要素からなるタプルを返します。

  • 成功したら {:ok, binary} を返します。ただし、binary は読み込まれたファイルの中身です。
  • 失敗したら {:error, reason} を返します。ただし、reason はエラーの原因を示すアトムです。例えば、ファイルが存在しなければ :enoent、読み込み権限がなければ :eacces、等。

このように、関数からの戻り値の中に処理の成否の情報が含まれる時、しばしばタプルが使われます。これが、タプルの第1の用途です。

Fizz Buzz

動画の中のライブコーディングで、次のような短いプログラム(fizz_buzz.exs)を作成しました。

for n <- [1, 2, 3, 4, 5, 6] do
  cond do
    rem(n, 15) == 0 -> IO.puts("Fizz Buzz")
    rem(n, 5) == 0 -> IO.puts("Buzz")
    rem(n, 3) == 0 -> IO.puts("Fizz")
    true -> IO.puts(n)
  end
end

これは「Fizz Buzz」という言葉遊びに由来するプログラムです。1 から順に数字を出力してくのですが、例外があります。3 で割り切れたら、Fizz、5 で割り切れたら Buzz、3 で
も 5 でも割り切れたら Fizz Buzz と表示します。

ターミナル上でこれを実行すると、次のような結果が出力されます。

1
2
Fizz
4
Buzz
Fizz

for マクロ

forマクロ は、JavaScriptの配列が持つ forEach メソッドのように、リストから要素を1つずつ取り出すために利用できます。

次のスクリプトをターミナル上で実行すると、4行に渡って 2 4 6 8 と出力されます

for n <- [1, 2, 3, 4] do
  IO.puts(n * 2)
end

for マクロはリストを別のリストに変換するためにも使えます。

list1 = [1, 2, 3, 4]

list2 =
  for n <- list1 do
    n * 2 
  end

IO.inspect(list2)

これをターミナル上で実行すると、[2, 4, 6, 8] と出力されます。

for <- の右側にはリストだけでなく、エニュメラブル(リスト、レンジ、マップ、ビットストリングなどの総称)を置くことができます。レンジについては後述します。

注意すべきは、タプルがエニュメラブルに属さないということです。そのため、上記の例で [1, 2, 3, 4]{1, 2, 3, 4} と書き換えると次のようなエラーが発生します。

** (Protocol.UndefinedError) protocol Enumerable not implemented for {1, 2, 3, 4} of type Tuple
    (elixir 1.16.2) lib/enum.ex:1: Enumerable.impl_for!/1
    (elixir 1.16.2) lib/enum.ex:166: Enumerable.reduce/3
    (elixir 1.16.2) lib/enum.ex:4396: Enum.map/2

エラーメッセージを日本語訳すると「タプル型の {1, 2, 3, 4} にはプロトコル Enumerable が実装されていない」となります。プロトコルは非常に面白い概念ですが、通常のアプリケーション開発をするだけなら理解する必要はないでしょう。エニュメラブルが存在すべき場所に、エニュメラブルでない値が置かれると上記のようなエラーメッセージが出るとだけ覚えておきましょう。

cond マクロ

Elixir には条件分岐を行うための構文(マクロ)が3つ用意されています。

  • if マクロ
  • cond マクロ
  • case マクロ

さきほどの fizz_buzz.exs では2番目の condマクロ が使われています。

次の例をご覧ください。

n = Enum.random([1, 2, 3, 4, 5])

cond do
  n == 1 -> IO.puts("One")
  n == 2 -> IO.puts("Two")
  true -> IO.puts("Many")
end

関数 Enum.random/1 はリストなどのエニュメラブルを引数に取り、その要素を1つランダムに取り出して返します。したがって変数 n には、1 から 5 までの整数のいずれからセットされます。

cond doend で囲まれた範囲には、-> 記号で結ばれた2つの式の組が複数個並びます。これらの式の組について上から順に -> の左辺を評価し、その値が false でも nil でもなければ、-> の右辺を評価します。

もし変数 n1 がセットされたのなら式 IO.puts("One") が評価され、ターミナルに One と出力されます。

cond マクロ内の最後の式の組では -> の左辺に true と書くのが一般的です。それまでのすべての式の組について左辺の条件が成立しなかったとき、ここにある -> の右辺が評価されることになります。

cond マクロ全体の戻り値は、左辺の条件が成立した式の組の右辺の値となります。そこで、上記の例は次のように書き換えることができます。

n = Enum.random([1, 2, 3, 4, 5])

word =
  cond do
    n == 1 -> "One"
    n == 2 -> "Two"
    true -> "Many"
  end
  
IO.puts(word)

レンジ

Elixirで 1..16 のように書くと、1 から 16 までの整数列を表すレンジという値になります。16..1 のように降順の整数列を表すレンジを作ることもできます。レンジはエニュメラブルに属しますので、for マクロや関数 Enum.random/1 で使用できます。

1..16//2 のように書くと、1, 3, 5, 7, 9, 11, 13, 15 という整数列を表すレンジができます。

Rubyプログラマへの注意

  • Rubyでは "a".."z" のように文字列のレンジを作ることができますが、Elixirではできません。
  • Rubyでは 1...5 のように終端を含まないレンジを作ることができますが、Elixirではできません。

Discussion