Ruby
RubyDay 10

with_indexが便利だという話とstable_sort_by

More than 3 years have passed since last update.

Rubyはいろんなことがone lineで書けて便利ですよね。

いろんなことを1行に書けるようにするには

String, Array, Hash, Enumerable

あたりの、リスト系の構造になったものにどういうメソッドがあるかを覚えておけば、たいていのことは1行で書けます。

今回の話は、これに加えて、

Enumerator

のメソッドも知っておくともっと世界が広がるよという話です。


map_with_indexが欲しい!

例えば、あるユーザーのリストに対して、名前の前に通し番号をふった文字列を出力したい(あんまり無さそうな例でごめんなさい)という要求があったとしましょう。

それをただ出力すれば良いのであれば、each_with_indexを使って

users.each_with_index do |user, i|

puts "#{i+1}: #{user.name}"
end

とすれば実現できます。

ですが、この結果が配列として欲しいとなると、mapを使いたいところですが、map_with_indexなんて関数ないしなぁとなります。

これを解決する一つの方法としては、each_with_indexの結果をmapして

users.each_with_index.map { |user, i| "#{i+1}: #{user.name}" }

とやる方法があります。

でも、これ、カッコ悪いですよね?

で、調べてみると、ありました!Enumeratorの中にwith_indexというメソッドが!

これを使うと、

users.map.with_index { |user, i| "#{i+1}: #{user.name}" }

と書くことができ、とても読みやすくなります。

めでたしめでたし。

ちなみに、with_indexは何の数値からはじめるかを引数にとることができます。なので、

users.map.with_index(1) { |user, i| "#{i}: #{user.name}" }

として、1-originにすることでi+1と毎回しなくてよくなり、少し綺麗に書くことができます。


with_indexを使って安定ソートを1行で

with_indexにはかなり使える応用があって、それが安定ソート(Stable Sort)です。

何故かRubyはデフォルトで安定ソートが提供されてないんですよね。

例えばFacebookの投稿のようなモデルがあって、それの配列をLike数が多い順、Like数が同じ場合は作られた順でソートしようと思った場合、もしsort_byがstableであれば以下の様なコードでうまくいくはず。

posts.sort_by(&:created_at).sort_by(&:like_count)

ですが、多分ソートの内部実装がQuick Sortなため、値が等しい時の順番が崩れてしまいます。

posts.sort_by(&:created_at).sort_by.with_index{ |x,i| x.like_count }

と書いて、2回目のソートをstable sort風にすればone lineで書くことができるようになります。


普通にstable_sort_byを使えるようにする

上でもコマンド上でやるときはいいですが、通常は


enumerable.rb

module Enumerable

def stable_sort_by
self.sort_by.with_index{ |e, index| [yield(e), index] }
end
end

とやった上で、

posts.stable_sort_by(&:created_at).stable_sort_by(&:like_count)

のほうが綺麗ですね。