Ruby

空の配列に対するRubyのメソッドの挙動(数学寄り)

以下の結果は何なのかという話。空の配列でバグったら格好悪いので復習する。

quiz.rb
p [].any?
p [].all?

p [].combination(0).to_a
p [].combination(1).to_a

p [].cycle.size

p [[], [], []].transpose.transpose

any? と all?

any? は「要素にひとつ以上真なものがあるか」なので、空の配列に対して false を返すのは納得しやすい(真逆である none? も簡単)。それに対して all? は「要素が全て真なものか」なので、空の配列に対してだと何も無いのに全てとは何なのか混乱する。

そこで以下のように考える。簡単のため ary はbooleanの配列とする。

ary.all? の実装
# イメージとしては
init && ary[0] && ary[1] && ... && ary[-1]

# より形式的には
ary.inject(init) { |res,elem| res && elem }

このとき ary.size > 0 に対して妥当な結果になる init を決められれば、それは空の配列に対する戻り値としても妥当である。 false だとダメだが true ならうまくいく。

あるいは、ド・モルガンの法則を使って any? に書き換えても理解できる。一般に ary.all? == !ary.any?(&:!) なので、空の配列に対してだと all?any? と逆の値を返す。

以上より、空の配列に対して all? は true を返す


大抵の場合はこの性質で真偽判定が単純になるはずだが、 all? は与えられた評価が何であっても空の配列だと true を返してしまうことには注意。成り立つわけがない評価ほど見逃しやすい。

vacuous_truth.rb
puts "文字数が'負'な英単語のみをカンマ区切りで入力してください"
ary = gets.chomp.split(',', -1)
p ary

if ary.all? { |str| str.length < 0 }
    puts "成功"
else
    puts "失敗"
end

順列・組み合わせ

配列の要素に対して指定した個数での選び方を列挙するメソッド。並び順を区別するか、同じものを選んでもいい(重複を許す)か、によって 2×2 = 4種類ある。

  • どの場合においても、「0個のものの中から0個を選ぶ方法」は1通りだけある。その結果、「0個を選んだことを表す空の配列」を1つ持つリスト [[]] が作られる1
  • それに対して、「0個のものの中から1個以上を選ぶ方法」は存在しない。結果として、選び方を1つも持たないリスト [] が作られる1
# [[]] を返す
ary = []; r = 0
ary.permutation(r).to_a
ary.combination(r).to_a
ary.repeated_permutation(r).to_a
ary.repeated_combination(r).to_a

# [] を返す
ary = []; r = 1
ary.permutation(r).to_a
ary.combination(r).to_a
ary.repeated_permutation(r).to_a
ary.repeated_combination(r).to_a

cycle

each を無限に繰り返すようなメソッド。(引数で回数指定もできる)

ary = [1, 2, 3]
ary.cycle.size     #=> Infinity
ary.cycle.take(10) #=> [1, 2, 3, 1, 2, 3, 1, 2, 3, 1]

では0個を無限回繰り返すとどうなるのだろう?と思ったが、これは0になった。取り出せるものが無いのだから当然か。

[].cycle.size #=> 0
[].cycle.to_a #=> []

無限が有限になるぶんには問題は起きそうにない。空の配列だとループが実行されないのは each でも何でも同じなので cycle 特有の問題ではない。

transpose

2次元配列を行列とみなして、行と列を入れ替える(転置する)メソッド。

空の配列なら0行であり、0行0列は転置しても同じく空の配列になる。

[].transpose #=> []

…と思ったら罠があった。本当は0行なら何列なのかわからないので、上の動作はごく稀に都合が悪い2。わかりやすいところでは、「転置を2回すれば元に戻る」が成り立たない事例がある。

ary = [[], [], []]             # 3行0列
ary.transpose           #=> [] # 0行3列?
ary.transpose.transpose #=> [] # != ary

なお Matrix クラスなら行列の大きさを保持するので空行列でもうまく扱える。空の配列の転置のためだけに使うくらいなら場合分けなどするが。

require 'matrix'

m = Matrix.build(3, 0) {}  #=> Matrix.empty(3, 0)
m.to_a                     #=> [[], [], []]
m.transpose.to_a           #=> []
m.transpose.transpose.to_a #=> [[], [], []]

その他

self時々nilを返す破壊的メソッド」を空の配列に作用させると nil が返る。配列に対する変更が無いことを意味する。

[].keep_if {} #=> []  # 常にself
[].select! {} #=> nil # 普通はselfだが、変更が無いとnil

何か1個を返すつもりのメソッドだとだいたい nil が返る。個人的には minmax で対処を忘れてしまいやすい。

[].sample #=> nil
[].minmax #=> [nil, nil]

all? の例でも使ったが、 inject で空の配列を扱うなら初期値を指定しておく必要がある(それを決めるのが難しい…)。例えば「与えられた全ての数の積」を計算するなら、0個のときは1を返すのが望ましい。

[].inject(:*)    #=> nil
[].inject(1, :*) #=> 1

  1. 正確には、ブロックを与えれば順次実行され、与えなければ Enumerator が返される。 

  2. 実際にこれのため0の場合だけアルゴリズムを適用できないことがあった。