Elixir

Elixir の String.split がモヤモヤします

[ruby]"".split(",").length は 0 で、[golang]len(strings.Split("", ",")) は 1 の話 で、Elixirが「もうよくわからない」と書かれていたので、もうちょい調べてみました。

Elixr は、1.6.5 を使ってます。

文字列のsplit

Interactive Elixir (1.6.5) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> String.split("a", ",")
["a"]
iex(2)> String.split("a", " ")
["a"]
iex(3)> String.split("a", "") 
["", "a", ""]
iex(4)> String.split("a")
["a"]
iex(5)>

String.split("a", "") が謎の動きですね。

iex(5)> String.split("", ",")
[""]
iex(6)> String.split("", " ")
[""]
iex(7)> String.split("", "")
["", ""]
iex(8)> String.split("")
[]
iex(9)>

String.split("", "")String.split("") が、謎の動きですね。

公式ドキュメントを見る

公式ドキュメントを見ると、String.split/1String.split/3 があります。String.split の引数が1つの場合と3つの場合に別れるようです。引数が2つの場合は、第3引数にデフォルト値の[]が指定されたものとして扱われます。

String.split/1

Divides a string into substrings at each Unicode whitespace occurrence with leading and trailing

とあるので、

String.split(x, " ") == String.split(x)

となって欲しいのですが、 実際には

iex(21)> String.split("a", " ") == String.split("a")
true
iex(22)> String.split("", " ") == String.split("")
false

xが空文字のときは、成立しません。

うん? `Unicode whitespace ??? もしやと思って試してみると

iex(23)> String.split("a b c") # a と b の間は全角スペースでbとcの間は半角スペース
["a", "b", "c"]

おおっ。思った以上にこれは複雑な感じですね。

String.split/3

Divides a string into substrings based on a pattern.

とあり、更に

Empty strings are only removed from the result if the :trim option is set to true.

なんてことが書かれてます。

Exampleのところを見ると空文字で分割する例がありました。

Splitting on empty string returns graphemes:

と書かれてます。実際に手元で試してみます。

iex(26)> String.split("a", "")
["", "a", ""]
iex(27)> String.split("a", "", trim: true)
["a"]
iex(28)> String.graphemes("a")
["a"]
iex(29)> String.split("", "")
["", ""]
iex(30)> String.split("", "", trim: true)
[]
iex(31)> String.graphemes("")
[]

第2引数を指定するときは trim: true をつければ良いということ?

iex(32)> String.split("a", ",", trim: true)
["a"]
iex(33)> String.split("a", " ", trim: true)
["a"]
iex(34)> String.split("a", "", trim: true)
["a"]
iex(35)> String.split("a")
["a"]

おっ。全部 ["a"] になった。なんかスッキリ。

iex(43)> String.split("", ",", trim: true)
[]
iex(44)> String.split("", " ", trim: true)
[]
iex(45)> String.split("", "", trim: true)
[]
iex(46)> String.split("")
[]

おっ。全部 [] になった。なんかスッキリ...。
いやいや、trim: true ってつけないといけないって時点でモヤモヤする。第3引数を省略したときのデフォルトが trim: true ってなってた方が良かったんじゃないかと思います。
 

String.split のソースを見る

ちょっとだけソースを覗いてみたところ、String.split/3 の第2引数が "" で、第3引数を省略した場合、意図的に前後に "" をつけているようです。

先頭に "" をつけているところは、以下の split です。

string.ex
  def split(string, "", options) when is_binary(string) do
    parts = Keyword.get(options, :parts, :infinity)
    index = parts_to_index(parts)
    trim = Keyword.get(options, :trim, false) # 省略時が true だったら...

    if trim == false and index != 1 do
      ["" | split_empty(string, trim, index - 1)] # ここで意図的に "" を先頭に追加している
    else
      split_empty(string, trim, index)
    end
  end 

一番最後に "" をつけているところは、 split_empty です。

string.ex
  defp split_empty("", true, 1), do: []
  defp split_empty(string, _, 1), do: [string]

  defp split_empty(string, trim, count) do
    case next_grapheme(string) do
      {h, t} -> [h | split_empty(t, trim, count - 1)]
      nil -> split_empty("", trim, 1)
    end
  end

next_grapheme で取り出す文字がなくなると

split_empty("", trim, 1)

が実行されて、trim がデフォルトでは、 false なので、

defp split_empty(string, _, 1), do: [string]

が実行されて、 [""] が最後にくっつくようになっています。

String.split("", "")

の場合は、最初に "" がついて [""] となって、取り出す文字が最初からないので、最後に "" が追加されて、 ["", ""] になります。

うーん。でも、なんで、こんな実装(仕様)になっているんでしょうか?

ちなみに

String.split("", " ")

String.split("", ",")

の場合は、

string.ex
  def split(string, pattern, []) when is_tuple(pattern) or is_binary(string) do
    :binary.split(string, pattern, [:global])
  end

が実行されるので、 Erlang の世界に飛び込むことになり、ここで力尽きました。

String.split("")

の場合は、

string.ex
  defdelegate split(binary), to: String.Break

で、 String.Break.split/1 が呼ばれるみたいですが、ここで力尽きました。

まとめ

  • Elixr の String.split は奥が深くて謎だらけ
  • String.split を使うときには、 trim: true をつけておいた方が結果がスッキリするような気がしないでもない。
  • 結局なんか、モヤモヤして謎は深まるばかり。