Edited at

[ruby]`"".split(",").length` は 0 で、[golang]`len(strings.Split("", ","))` は 1 の話


TL;DR

Rubyにて、

[1] pry(main)> ''.split(',')

=> []
[2] pry(main)> ''.split(',').length
=> 0

👆これと…

Golangにて、

gore> :import strings

gore> strings.Split("", ",")
[]string{
"",
}
gore> len(strings.Split("", ","))
1

👆に驚かなかった方は以下読まなくて大丈夫です!


発端

Goのコードレビューしている時に以下のようなコードを見つけました。

arr := strings.Split(target, ",")

if len(arr) == 1 {
return nil, error.New("error")
}

return arr[len(arr)-1], nill

👆のコードだと target="" の場合 len(arr)=0 となるので invalid slice index -1 (index must be non-negative) で落ちるんじゃないかな、と思ったわけです。

goreで確認すると…

gore> :import strings

gore> strings.Split("", ",")
[]string{
"",
}
gore> len(strings.Split("", ","))
1

あれ?サイズが1の配列が戻ってきた…とびっくりしたのを発端として、少し調べてみました。

✋この時点ではRubyに侵された私の脳では、空文字(empty string)をsplitしたら 0宇宙の法則常識じゃないの?って思ってました。また、splitがこんなにもめんどいことになっているとは露知らず深みにハマったのでした…


2019/05/16 補足)

@ikngtty 様から ([]string)[] じゃなくて []string{""} が正しいと御指摘いただき修正致しました:raising_hand_tone1:

この違いは実はgoreのバージョンが異なるためで、記事では 0.4.1 での結果ですが、例えば 0.2.6 では以下のように ([]string)[] で表示されてしまいます。

gore version 0.2.6  :help for help

gore> :import strings
gore> strings.Split("", ",")
([]string)[]
gore> len(strings.Split("", ","))
(int)1

このような場合は fmt.Printf("%#v") を使うと幸せになります。@ikngtty 様ありがとうございました:relaxed:

💭ただ、goreの古いバージョンだと出力がバグるみたいなのでできる限り最新のバージョンで試すことをオススメします:point_up:

target := strings.Split("", ",")

zero := []string{}
emptyOne := []string{""}
fmt.Printf("%v %v %v\r\n", target, zero, emptyOne)
// [] [] []
fmt.Printf("%#v %#v %#v\r\n", target, zero, emptyOne)
// []string{""} []string{} []string{""}


他の言語を調べてみた

とりあえず手っ取り早く他の言語を確認してみました。


Python

Python 3.6.5 (default, May  5 2018, 03:05:30)

[GCC 6.3.0 20170516] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> "".split(",")
['']
>>> len("".split(","))
1

あれ、1だね…


Elixir

💭公式のdokcerイメージ結構重い…

Interactive Elixir (1.6.5) - press Ctrl+C to exit (type h() ENTER for help)

iex(1)> String.split("", "-")
[""]
iex(2)> length(String.split("", "-"))
1

これも1


javascript

chromeのdeveloper toolで確認

> "".split("-")

< [""]
> "".split("-").length
< 1

これも1


ruby

[1] pry(main)> RUBY_VERSION

=> "2.4.2"
[2] pry(main)> ''.split(',')
=> []
[3] pry(main)> ''.split(',').length
=> 0

rubyは0


Perl

docker run perl perl -e '@a=split(/,/, ""); $a=@a; print "count=",$a,"\n"'

count=0

👆でいいのかな?


とにかく0のようです…

結果、空文字(empty string)を各言語のsplit関数に渡したときの配列サイズは 1 が優勢で、rubyとperlは 0 ということがわかりました。


なぜそうなっているのか?

わからなかったのでググってみたのですが、結構あるあるネタみたいでした。

Splitting Stringsに転載されているpythonの作者、Guido van Rossumさんのコメントがわかりやすいです。


“In Python the generalization is that since:

"xx".split(",") is ["xx"], and
"x".split(",") is ["x"], it naturally follows that
"".split(",") is [""].”

-- Guido van Rossum on the Python Mailing list (formatting added)


意訳すると…


“Pythonでの常識だと以下の通り:

"xx".split(",") is ["xx"], で
"x".split(",") is ["x"], ときたら普通は
"".split(",") is [""]. でしょ。”

✋はい、私もそう思います。splitできなかった場合は入力値をそのまま配列に入れる仕様は素直に受け入れられました…ってことで、自分の常識がおかしいことに気付いたのでした😇

ちなみにRubyについては、AWKの仕様(を元にしたPerlの仕様)から、 項目 として考えられているんじゃないかと説明されています。


"x,x".split(",") has 2 fields and ["x", "x"] has length 2

"x".split(",") has 1 field and ["x"] has length 1
"".split(",") has 0 fields and [] has length 0

下記みたいにワンライナーで書いたときに count=0 って出るのを正とした仕様となっているみたいです。

$ ls

——
$ ruby -e 'puts "count=#{`ls`.split("\n").length} "'
count=0
——
$ python -c 'import subprocess;print "count=%s" % len(subprocess.check_output("ls").split("\n"))'
count=1
——


ついでに…

いろいろパターン試してみたのですが、下記の動作もまた分かりづらい。。。


Perl

$ docker run perl perl -e '@a=split(/,/, ","); $a=@a; print "count=",$a,"\n"'

count=0
——
$ docker run perl perl -e '@a=split(/,/, ",,"); $a=@a; print "count=",$a,"\n"'
count=0
——
$ docker run perl perl -e '@a=split(/,/, ",,a"); $a=@a; print "count=",$a,"\n"'
count=3


Ruby

[1] pry(main)> ','.split(',')

=> []
[2] pry(main)> ',,'.split(',')
=> []
[3] pry(main)> ',,a'.split(',')
=> ["", "", "a"]

💭このあたりも含めString#splitの動作はPerlに寄せたそうで、修正する予定もないみたいですね。


At least some of your points are rational. Those behaviors are inherited from Perl.

I don't think we can change the behavior. We are not going to break existing code for the sake of consistency.

Matz.

Feature #5120: String#split needs to be logical - Ruby trunk - Ruby Issue Tracking System


ちなみに、Rubyだと回避策はあって第二引数(limit)にマイナス値を設定すると GolangやPythonと同じになります。ただ、それでも空文字(empty string)の動作を同じにすることはできません。

[1] pry(main)> ','.split(',', -1)

=> ["", ""]
[2] pry(main)> ',,'.split(',', -1)
=> ["", "", ""]
[3] pry(main)> ',,a'.split(',', -1)
=> ["", "", "a"]
[4] pry(main)> ''.split(',', -1)
=> []


結論


  • Rubyも正しい、Golangも正しい。

  • とはいえ、言語によってsplitの仕様が異なる場合があるので注意が必要!


追記:2018/5/26 13:40

せっかくなので他にも試してみました。


Scala

まさかのハイブリッド!

"" はGolang/Python系と同じ結果(size=1)ですが、

Welcome to Scala version 2.10.6 (OpenJDK 64-Bit Server VM, Java 1.8.0_131).

Type in expressions to have them evaluated.
Type :help for more information.

scala> "" split ","
res0: Array[String] = Array("")

scala> ("" split ",").length
res1: Int = 1

",,a" のパターンはRuby/Perl系と同じく先頭を無視する動きです。

scala> ("," split ",").length

res2: Int = 0

scala> (",," split ",").length
res3: Int = 0

scala> (",,a" split ",").length
res4: Int = 3

scala> ",,a" split ","
res5: Array[String] = Array("", "", a)

"a,," もRuby/Perl系と同じく末尾が無視されました。

scala> "a,," split ","

res6: Array[String] = Array(a)

scala> ("a,," split ",").length
res7: Int = 1


Erlang

all を知らなかったので一瞬ビックリしましたが、Golang/Python系と同じでした。

Erlang/OTP 20 [erts-9.3.1] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]

Eshell V9.3.1 (abort with ^G)
1> string:split("", ",").
[[]]
2> length(string:split("", ",")).
1
3> string:split(",", ",").
[[],[]]
4> string:split(",,a", ",").
[[],",a"]
5> string:split(",,a", ",", all).
[[],[],"a"]
6> string:split("a,,", ",", all).
["a",[],[]]


PHP

本命PHP。おそらくやっかいになっているはず…と思ったらsplit自体が厄介になっていた…

警告

この関数は PHP 5.3.0 で 非推奨 となり、 PHP 7.0.0 で 削除 されました。

この関数の代替として、これらが使えます。
preg_split()
explode()
str_split()

http://php.net/manual/ja/function.split.php

うーん…今回は正規表現を使わないパターンで検証しているので explode で試してみます。

php > echo phpversion();

7.2.6
php > print_r(explode(",", ""));
Array
(
[0] =>
)
php > print_r(count(explode(",", "")));
1
php > print_r(explode(",", ","));
Array
(
[0] =>
[1] =>
)
php > print_r(explode(",", ",,a"));
Array
(
[0] =>
[1] =>
[2] => a
)
php > print_r(explode(",", "a,,"));
Array
(
[0] => a
[1] =>
[2] =>
)

予想外の結果に。 Golang/Python系 でした。


Dart

ついでに…と思ってDart試してみたらまた新たな発見が。

まずは基本の動作。これは Golang/Python系と同じです。

void main() {

print("".split(",")); =>[]
print("".split(",").length); =>1
print(",".split(",")); =>[, ]
print(",".split(",").length); =>2
print(",,a".split(",")); =>[, , a]
print("a,,".split(",")); =>[a, , ]
}

ただ、ドキュメントを読むと検索対象が空文字でかつ、splitする文字が空文字(empty string)の場合とがきちんと考慮されています。


If this string is empty, the result is an empty list if pattern matches the empty string, and it is [""] if the pattern doesn't match.

var string = '';

string.split(''); // []
string.split("a"); // ['']

https://api.dartlang.org/stable/1.24.3/dart-core/String/split.html


大抵の言語では、空文字でsplitすることは、単語ごとに分割されることを期待されているからの考慮のようです。

一方 Elixir は…

Interactive Elixir (1.6.5) - press Ctrl+C to exit (type h() ENTER for help)

iex(1)> String.split("","")
["", ""]

もうルールが良くわからない…

そして Python は...

>>> "xx".split("")

Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: empty separator

空文字でsplitできないんですね😨

>>> "xx".split()

['xx']
>>> import re
>>> re.split('', 'xx')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/local/lib/python3.6/re.py", line 212, in split
return _compile(pattern, flags).split(string, maxsplit)
ValueError: split() requires a non-empty pattern match.

ぐぬぬぬぬ…


追記を踏まえた最終的な結論…


  • 言語によってsplitの仕様は異なるので、自分の常識を疑ってください😂