nekoTheShadow’s diary

IT業界の片隅でひっそり生きるシステムエンジニアです(´・ω・`)

Rubyにおける多重ループの回避方法と自己流の美学を語る。

 最近はプログラミングコンテスト的なものを利用してプログラミングを勉強しているのですが、その際多重ループを書くことが頻繁にもとめられます。

(1..3).each do |i|
    (1..3).each do |j|
        p [i, j]
    end
end

 上の例などは典型的ですが、個人的な美学からするとやや不満。上記のような多重ループを書く場合、それだけでは完結せず、その外側にもループを書くことが大半です。したがってネストがどんどん深くなって、非常に読みづらくなります。また個人的な美学として「ネストは浅いに越したことはない」と考えているので、何とかネスト深くせずに多重ループを行う方法を考えたいと思います。

 まず思いつくのはArray#repeated_permutation。つまり重複組み合わせですね。

(1..3).to_a.repeated_permutation(2) do |i, j|
    p [i, j]
end

(1..3).to_a.repeated_permutation(3) do |i, j, k|
    p [i, j, k]
end

 これはわかりやすいものの「ijがまったく別の範囲にある」というような状況には対応できません。そこでこの悩みを解消してくれるArray#productを使ってみましょう。

ary1 = (1..5).to_a
ary2 = (2..5).to_a
ary3 = (3..5).to_a

ary1.product(ary2) do |i, j|
    p [i, j]
end

ary1.product(ary2, ary3) do |i, j, k|
    p [i, j, k]
end

 こちらのよいところは引数が可変であるところ。何重のループでも簡単に書けてしまいます。ただし(個人的な感性の問題かもしれませんが)Array#repeated_permutationに比べるとやや可読性が落ち、かつコード量も増えるような気がするので、そのあたりは適当な使い分けが必要になりそうです。

 さてここまでふたつのメソッドを利用した「多重ループの回避法」を紹介したわけですが……どうしても多重ループを書かねばならないという場合は当然存在します。例えば次のようなケース。

(1..5).each do |i|
    next if i % 3 == 0

    (1..5).each do |j|
        break if j > 3
        p [i, j]
    end
end

 つまり脱出や継続の条件がループごとに違う場合は多重ループを書くほかありません――というより、上記に紹介したように1重ループ(?)にすることも可能といえば可能ですが、そうすると多重ループに比べると計算量が明らかに増大するということが多いため、コンマ1秒を競うプログラミングコンテストでは役に立たないこともしばしば。そういうわけで、あきらめて多重ループをしこしこ作るほかありません。

 ただし他の人のコードを見ていると、どうしてもRubyで多重ループを書くほかない場合は次のようにして可読性を確保しているケースがまま見受けられます。

(1..5).each do |i|
    next if i % 3 == 0

    (1..5).each{|j|
        break if j > 3
        p [i, j]
    }
end

 ブロックの表現として{}do-endのふたつがあることを利用した方法ですが……正直にいって個人的にはあまり使いたくない。やはり美学の問題ですが。わたしの場合、ブロックが1行におさまる場合は{}、2行以上の場合はdo-endというように使い分けており、{}の中に何行も書かれていると非常に違和感があります。まあ好き好きですが。