書籍「プログラミングElixir」の中に書かれていた練習問題を考えてみました。
問
問:なぜiexは'cat'
を文字列として表示しているのに、'dog'
を文字コードで表示しているのだろう?
iex> ['cat' | 'dog']
['cat', 100, 111, 103]
回答
内部的には、['cat' | 'dog']
は、「[[99, 97, 116], 100, 111, 103]
」という「数値のリスト」と「数値」が混在したリストとして扱われています。
そして、「数値のリスト」と「数値」が混合したリストを扱った際に、iexが「数値のリスト」を「文字リテラル」として解釈するためです。
これらの動きには、以下の3つが関連しています。
- 単一引用符(シングルクォート)で囲んだ場合、「文字のリスト」になるから。
- ヘッドとテイルでは、ヘッドは値、テイルはリストとして扱われるから。
- iexでは、ヒューリスティックで動作しているから。値として「数字のリスト」があるとき、そのリスト内の全ての値が「印字(表示)可能文字の文字コードの値と同じ」だと「文字リテラル」として表示する動きとなっている。
もう少しわかりやすくイメージできるのが以下のパターン。
iex> ['cat'++[0]|'dog']
[[99, 97, 116, 0], 100, 111, 103]
ヘッドの値は、「++[0]」により「文字のリスト」に「印字できない文字コード」が追加されたことにより、「文字のリスト(文字リテラル)」ではなく「数値のリスト」となりました。
一方、以下のようにヘッド「h」とテイル「t」に割り当てたのち、hとtの値をそれぞれ確認してみると、以下のようになりました。
iex> [h|t] = ['cat'++[0]|[100,111,103]]
[[99, 97, 116, 0], 100, 111, 103]
iex> h
[99, 97, 116, 0]
iex> t
'dog'
テイル「t」の値は、[100,111,103]
の数値リストに設定されています。
ただし、数値のリスト内の値が全て「印字(表示)可能文字の文字コードの値と同じ」であるため、'dog'
として表示されています。
関連事項の内容を、もう少し深掘りしてみます。
1. 単一引用符(シングルクォート)で囲んだ場合、「文字のリスト」になるから。
Elixirでは、文字リテラルは「二重引用符(ダブルクォート)」と「単一引用符(シングルクォート)」で囲んだ値として扱っています。
しかし、二重引用符で囲んだ場合と単一引用符で囲んだ場合とで、Elixir内部での扱いに違いがあります。
その違いは、簡単にいうと以下となります。
- 二重引用符(ダブルクォート)で囲んだ場合は「文字列」1として扱われる
- 単一引用符(シングルクォート)で囲んだ場合は「文字のリスト」として扱われる
それぞれを「ガード節」で利用する型判定の「is_bitstring」(文字列判定)と「is_list」(リスト判定)を利用して確認してみます。
iex> s1 = "hello world"
"hello world"
iex> is_bitstring(s1)
true
iex> is_list(s1)
false
iex> s2 = 'hello world'
'hello world'
iex> is_bitstring(s2)
false
iex> is_list(s2)
true
このように、二重引用符で囲んだ場合は「文字列であるがリストではない」と、単一引用符で囲んだ場合は「文字列ではないが文字2のリストではある」と、わかります。
なお、単一引用符で囲んだ場合は「文字のリスト」の扱いですので、Enumを利用して数値計算などができます。
iex> s2
'hello world'
iex> s2 ++ [0]
[104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 0]
iex> sp2 = s2 |> Enum.map(fn x -> x + 1 end)
'ifmmp!xpsme'
iex> sp2 ++ [0]
[105, 102, 109, 109, 112, 33, 120, 112, 115, 109, 101, 0]
2. ヘッドとテイルでは、ヘッドは値、テイルはリストとして扱われるから。
Elixirのリストは、空でない場合には、リストの先頭要素である「ヘッド」と、先頭要素を除いたリストを構成する要素である「テイル」に分かれます。
具体的なイメージは以下。
リストが、先頭要素の「1」の値と、先頭要素を除いたリスト「2, 3, 4, 5, 6」にわかれています。
iex> [h|t] = [1,2,3,4,5,6]
[1, 2, 3, 4, 5, 6]
iex> h
1
iex> t
[2, 3, 4, 5, 6]
リストの構成要素はどの型とでも組み合わせられます。
そして、色々な型が要素になったリストも、ヘッドとテイルで分けることができます。
iex> [h|t] = [[1,2,3],"4,5,6",7,8,9,true]
[[1, 2, 3], "4,5,6", 7, 8, 9, true]
iex> h
[1, 2, 3]
iex> t
["4,5,6", 7, 8, 9, true]
iex> [h2|t2] = t
["4,5,6", 7, 8, 9, true]
iex> h2
"4,5,6"
iex> t2
[7, 8, 9, true]
3. iexでは、ヒューリスティックで動作しているから。
Wikipediaによると、ヒューリスティックとは『必ず正しい答えを導けるわけではないが、ある程度のレベルで正解に近い解を得ることができる方法』とあります。
そして、冒頭でも触れましたが、iexは「値として数字のリストがあるとき、そのリスト内の全ての値が「印字(表示)可能文字の文字コードの値と同じ」だと「文字リテラル」として表示する動き」をとります。
これは、以下のように確認できます。
iex> [1,2,3,4,5]
[1, 2, 3, 4, 5]
iex> [100,101,102]
'def'
iex> [1,2,3,[100,101,102],4,5]
[1, 2, 3, 'def', 4, 5]
では、「印字(表示)可能文字の文字コードの値」の範囲はどこまでかとなると、「32」から「126」までとなっていました。
ml = 32..126 |> Enum.map(fn x -> x end)
' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'
例えば、全体に+1すると、「印字(表示)可能文字の文字コードの値」以外の値がリストに入るので、文字リテラルではなく、数値のリストとして表示されます。
ml |> Enum.map(fn c -> c + 1 end)
[33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52,
53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72,
73, 74, 75, 76, 77, 78, 79, 80, 81, 82, ...]
もう少しわかりやすい例を。
101から126までの数値のリストに対して、「-1」と「+1」を実施した例です。
「印字(表示)可能文字の文字コードの値」の126を超えた値がリストに入ると、文字リテラルは数値のリストとして表示されるようになります。
iex> ml |> Enum.filter(fn c -> c > 100 end)
'efghijklmnopqrstuvwxyz{|}~'
iex> ml |> Enum.filter(fn c -> c > 100 end) |> Enum.map(fn c -> c - 1 end)
'defghijklmnopqrstuvwxyz{|}'
iex> ml |> Enum.filter(fn c -> c > 100 end) |> Enum.map(fn c -> c + 1 end)
[102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117,
118, 119, 120, 121, 122, 123, 124, 125, 126, 127]
まとめ
書籍「プログラミングElixir」の「第7章 リストと再帰」「第11章 文字列とバイナリ」を読み返しながら整理してみました。
単一引用符で囲んだ際に出来上がった「文字のリスト」は「数値3のリスト」でもあります。
iex側でどちらかの区別がつかないために、印字可能な文字を示す数値であれば、文字のリスト、すなわち文字リテラルとして判断して表示しています。
ヘッドとテイルの組み合わせ([head|tail]
)を設定した際に表示されるのは、全体のリストです。
その中で、ヘッドがリストの先頭要素でありながら値として「文字のリスト」でもある場合には、文字リテラルとして表示されます。同様に、テイルを構成しているリストの中に「文字のリスト」があれば、やはりその要素の値は文字リテラルとして表示されます。
他のプログラミング言語とは異なり、Elixirでは単一引用符と二重引用符の違いを意識して扱う必要があります。