Ruby のオススメの機能7選

  • 448
    いいね
  • 5
    コメント
この記事は最終更新日から1年以上が経過しています。

この記事は Ruby Advent Calendar 2015 の 8日目です。

比較的あまり知られていないと思うオススメの Ruby の機能を 7つ紹介します。

Enumerator.new

Enumerator.new は知ってはいても、あまり使わない人が多いように思います。
私は非常によく使います。
理由は

  • スコープを新たに導入したい
  • Producer-Consumer パターン。値の生成と消費でフェーズの違いを明確化したい。
  • そのメソッド内で1度使いたいだけなのに yield するメソッドを別に作るのはちょっと気が引ける。名前空間を汚したくない。
  • 全部、一度配列にしちゃうとメモリが気になる。メモリ消費を節約したい。
  • ネストを浅くしたい
  • Enumerable モジュールの機能が欲しい

といったところです。

たとえば下記のようなかんじで使います。

# Producer フェーズ
user_ids = Enumerator.new do |y|
  open("candidates.txt") do |f|
    f.each_line do |line|
      next if line.start_with?("#")
      user_id = line.chomp
      y << user_id
    end
  end
end

# Consumer フェーズ
user_ids.with_index 1 do |user_id, index|
  puts "#{index} #{user_id}"
end

上の例では Consumer フェーズが単純なので、直接書いてもいいのですが、複雑になってくるとネストを浅くできるメリットも活きてきて、Enumerator.new の良さが分かってきます。
y というのは慣習的な変数名で、yielder の頭文字です。Yielder オブジェクトだから y ですね。

Object#tap

Object#tap も多くの人が知っていますが、単にメソッドチェインの中でデバッグする目的で使う人が多いように思います。

私はいろんな状況で Object#tap をよく使います。
おもな理由は下記のとおりです。

  • 変数の初期化とその利用でスコープ、フェーズの分割を明示したい
  • 返り値を返すときに、最後にわざわざ hash とかオブジェクトを書くのに違和感を感じる。特に map などのブロックの場合とか。
  • 昔 ActiveSupport にあった Kernel#returning のように値を返すときにすっきり書きたい

たとえば下記のように使います。

alphabet_nums = {}.tap do |hash|
  ("a".."z").each.with_index 1 do |alphabet, index|
    hash[alphabet] = index
  end
end

こう書くと

alphabet_nums = {}
("a".."z").each.with_index 1 do |alphabet, index|
  alphabet_nums[alphabet] = index
end

と書く場合に比べて、tap に囲まれた場所が初期化フェーズであることがよりコード中に明確に表現されていてわかりやすいように感じます。

上記の alphabet_nums が構築後は変更がない場合など特に、こう書いた方が構築中のブロック内だけ変更があるということが強調されていて、分かりやすいように感じます。

Float::INFINITY

Ruby には無限大を表す Float::INFINITY という値があります。

Float::INFINITY を使うとコードをすっきり統一的に書けることがあります。

すべての数を扱うときとある範囲を扱うときの両方がありうる場合を例に説明します。

この場合よく下記のように書きますが、

# to が nil ならどんなに大きな値でもOK! to に値があればその値まで
if to.nil? || x < to
  do_something(x)
end

事前に to のデフォルト値を Float::INFINITY にしておけば、nil かどうかの判定は不要にできます。

to ||= Float::INFINITY # 事前に nil の場合は無限大を表す値を代入しておくことで
if x < to # スッキリ!!
  do_something(x)
end

x < to のような評価が一度だけであれば嬉しさはあまり伝わらないかもしれませんが、何度も出てくるような場合はスッキリした見た目で分かりやすくなります。

Enumerable#each_slice

Enumerable#each_slice は何個かずつブロックに渡して順に処理を行えるメソッドです。
このメソッドそのものはよく知られているかと思いますが、似た処理を繰り返すときにすっきり書く方法を実例とともに紹介します。

電気料金を比較し、最適な電力会社を選ぼう!エネチェンジ」のサイトの開発の中で Enumerable#each_slice を使った例が以下になります。

require 'selenium-webdriver'
require "webdriver-user-agent"

driver = Webdriver::UserAgent.driver browser: :firefox, agent: :iphone, orientation: :portrait

url = "http://localhost:3000/"
driver.navigate.to url

%w(
  order[family_name] 山田
  order[given_name] 太郎
  order[family_name_kana] ヤマダ
  order[given_name_kana] タロウ
  order[postcode_1] 100
  order[postcode_2] 0001
  order[phone_number_1] 012
  order[phone_number_2] 3456
  order[phone_number_3] 7890
).each_slice(2) do |name, value|
  fields = driver.find_elements :name, name
  field = fields.first
  field.send_keys value
end

Selenium を使った自動操作の例ですが name 属性でエレメントを見つけてそこに自動入力させています。

2要素の配列を要素に持つ配列(配列の配列)を作ってもいいのですが、each_slice を使うことで、配列のネストを1個減らすことができています。
また、%w() を使うことで "", を書く必要がなくなっているのでかなりすっきりした見た目になっています。

特にロジックに変化が少なくデータの改変が多いときはこのように %w() で一気にデータ部を記述して、Enumerable#each_slice で何個かずつ分割する方が分かりやすいように思います。

Array#shelljoin

マイナーですが、Ruby標準ライブラリには shellwords というライブラリが含まれています。

shellwordsrequire すると sh 等でエスケープが必要な文字は自動的にエスケープしながら文字列を連結してくれる Array#shelljoin メソッドが追加されます。

require 'shellwords'
["vlc", "Ruby on Rails.mp4"].shelljoin #=> "vlc Ruby\\ on\\ Rails.mp4"

外部コマンドを実行するときは自分でエスケープするよりも Array#shelljoin を使う方が確実で見通しも良くなります。

SecureRandom.hex

セッションキーなどに用いるランダムな文字列を生成するには securerandomrequire すると使えるようになる SecureRandom.hex を使うと便利です。

require 'securerandom'
SecureRandom.hex # "396bc8f76cffce4d3bfea3d07f58b09b"

ランダムなメールアドレスの生成、一時ファイルの命名など、案外使います。

Flip Flop 演算子

ほとんどだれも知らないと思われますが Flip Flop 演算子という記法があります。

条件文を .. で結んで、最初の条件が満たされてから次の条件が満たされるまで true を返します。

この日本語ではわからないと思いますので、次の例を見てください。

(1..5).each do |x|
  puts x if (x == 2) .. (x == 4)
end

この結果は次のようになります。

2
3
4

この例では嬉しさは伝わりにくいと思いますので、もう一例紹介します。Markdown 形式で書かれているファイルの中で、Ruby のコードスニペットのところだけを処理するコードは下記のように書けます。

ARGF.each_line do |line|
  if (line.start_with?("```rb"))..(line.chomp == "```")
    do_something(line)
  end
end