kokura.ex / fukuoka.ex のim_miolabと申します。
さて、Elixirで整数の数値リストを取り扱っていると、謎の文字 が返ってくることがあります。
iex()> list_int = [80, 90, 110]
'PZn'
この現象にからめて、以下つらつらと書いてみます。
先に結論とまとめ
-
整数リスト内の全要素が ASCIIコードのエスケープシーケンスや印字可能文字の範囲内 にあると、文字リスト(charlist)として出力される。
-
Elixirでは、「文字列」と「文字リスト」は区別される。
文字の囲いかた | e.g. | 特徴 | データ型 | |
---|---|---|---|---|
文字列 | ダブルクオート("") | "hoge" | バイナリ | BitString |
文字リスト | シングルクオート('') | 'hoge' | 整数のリスト | List |
- 関数によって、
・文字列を引数に期待するもの
・List(文字リスト)を引数に期待するもの
とがあるので、使い分けが必要。
ざっくり検証
それでは、具体的にみていきます。
実行環境
- Elixir ver 1.9.4
- Windows 10 Pro
- Bash
リスト(List)
Elixirのリスト(List) は、[ 要素, 要素, ... ]
のように書きます。
(異なる型の要素も同居できます)
以下、iex
で整数のリストを書いてみました。
iex()> list_a = [80, 1, 1100]
[80, 1, 1100]
いちおう、型も見てみます。
# 「list_a」の型を確認
iex()> i list_a
Term
[80, 1, 1100]
Data type
List
.
.
.
# 「list_a の最初の要素」の型を確認
iex()> i list_a |> Enum.at(0)
Term
80
Data type
Integer
.
.
.
リストの中身は、ちゃんとInteger(整数)型になっています。
整数リストの全要素が、ASCIIコードの印字可能範囲にあるケース
冒頭でふれた現象についてです。
iex()> list_int = [80, 90, 110]
'PZn'
「先に結論とまとめ」でも書いたとおり、リスト内の要素がASCIIコード10進数の印字可能範囲に全ヒットしたため、
80 → P
90 → Z
110 → n
となって、'PZn'
として出力されています。
型を見てみると、以下のとおりです。
# 「list_int」の型を確認
iex()> i list_int
Term
'PZn'
Data type
List
.
.
.
# 「list_int の最初の要素」の型を確認
iex()> i list_int |> Enum.at(0)
Term
80
Data type
Integer
.
.
.
狐につままれたような感がしなくもないですが、中の要素はIntegerだということです。
念のため、もう少し確認してみます。
iex()> list_int
'PZn'
iex()> list_int |> Enum.sum
280
リスト内の要素は文字でなく、数値ですね。
(∵ 80 + 90 + 110 = 280)
iex以外でもやってみる
iex
以外でも一応確認してみます。
スクリプト実行
おためしで、簡単なコードを書いてみました。
defmodule Charlist do
def print_list do
list_int = [80, 90, 110]
list_int |> IO.puts
Enum.sum(list_int) |> IO.puts
end
end
Charlist.print_list
こちらをスクリプト実行してみます。
$ elixir charlist.exs
PZn
280
iexと同様、ASCIIコード文字列でいったん出力されています。
リストで中身はIntegerなので、Enum.sum
はきちんと機能しています。
コンパイル実行
コンパイル実行だとどうなるのでしょうか。
さっきと同じソースコードを、こんどはex
ファイルとして用意します。
defmodule Charlist do
def print_list do
list_int = [80, 90, 110]
list_int |> IO.puts
Enum.sum(list_int) |> IO.puts
end
end
Charlist.print_list
elixirc
でコンパイル実行してみます。
$ elixirc charlist.ex
PZn
280
こちらも同様の出力結果となっています。
つまり、この現象はiex
特有のものではない、ということです。
コードポイントを指定して文字のリストを返す
逆に、これを利用して、ASCIIコード10進数の整数リストを「文字のリスト」として返すことが可能です。
iex()> list_im = [105, 109]
'im'
'im'
の出力結果が得られました!
(ASCIIコード10進数で、105がi
、109がm
に該当)
文字列と文字リスト
「文字のリスト」について、ここで一度整理します。
まず、文字リスト(charlist)と文字列は別のものです。
たとえば、以下の2つは似ていますが異なるものです。
- "im"
- 'im'
iex()> "im" == 'im'
false
ダブルクオート「""
」で囲まれた文字は 文字列、
シングルクオート「''
」で囲まれた文字は 文字リスト になります。
文字列 は、UTF-8でエンコードされたバイナリです。
(いわゆる他の言語でいう文字列に近いのはこっち)
文字リスト は、文字のリストとは言うものの、じつは整数のリストです。
さきほどのlist_im = [105, 109]
(→結果は'im'
)を見てみると。。。
iex()> i list_im
Term
'im'
Data type
List
Description
This is a list of integers that is printed as a sequence of characters
delimited by single quotes because all the integers in it represent printable
ASCII characters. Conventionally, a list of Unicode code points is known as a
charlist and a list of ASCII characters is a subset of it.
Raw representation
[105, 109]
.
.
.
Description
に、書かれていますね。
This is a list of integers ... であると。
(いっしょにASCIIコードの印字範囲うんぬんの話も説明されてる)
文字リストは整数のリストであるので、以下のようなことができます。
iex()> 'im' |> Enum.sum
214
# 105 + 109 = 214
反対に、文字列を引数として使う関数では、文字リストは使えません(リストなので)。
# これはOK
iex()> "im" |> String.reverse
"mi"
# これはNG
iex()> 'im' |> String.reverse
** (FunctionClauseError) no function clause matching in String.Unicode.next_grapheme_size/1
.
.
.
つまり、
- 文字列を引数として期待する関数
- リスト(文字リスト)を引数として期待する関数
の使い分けが必要、ということになります。
文字リストが準備されてる目的は?(結論にかえて)
結局、「何が目的で[80, 90, 110]
が'PZn'
として出力されるのか?」
ってことになるんですが、
一部のErlangモジュールに文字情報を渡すとき、バイナリの文字列でなくて文字リスト(整数のリスト)として持たせておく必要があるので、その名残(?)が由来なのかなあと推測しています。
文字リストがサポートされているのは一部のErlangモジュールがそれを必要としているからです。
(出典:Elixir School)
これ以上のはっきりした由来はよく分かりませんでしたmm
なにかわかったら追記します!
p.s.
@お詳しい方 ぜひそっと教えてくださいませ。
参考文献
- 『プログラミングElixir』
- Elixir公式 (JP)「バイナリ、文字列、文字リスト」
- Elixir School 「文字列」
- Wikipedia (ASCII)