Ruby初心者が失敗しがち/間違えがちなこと5選

「楽しく書ける」「英文を書くようにコーディングできる」と評判のRuby。

少ない手間でさまざまな機能を実装できるフレームワーク「Rails」と共に、初心者からベテランまで幅広い層に好まれるプログラミング言語です。

今回はRuby初心者がつまづきそうな箇所を、5つピックアップしました。

1. 範囲演算子の範囲を読み違えがち

Rubyには、範囲オブジェクト「Range」をつくるための、範囲演算子があります。

範囲演算子には「..」「…」のふたつ(ドットの数が違います)があります。このうち「..」は「範囲の終端を含む演算子」であり、「…」は「範囲の終端を含まない演算子」です。

# 「..」は範囲の終端を含む
(0..3).each do |i|
  puts(i)
end  # 0, 1, 2, 3が順に出力

# 「...」は範囲の終端を含まない
(0...3).each do |i|
  puts(i)
end  # 0, 1, 2が順に出力

このように範囲演算子は「..」と「…」とで、終端を含むかどうかが分かれます。

「..」と「…」は見た目が酷似しているので、うっかり読み違えないようにしましょう。

2. 引数にProcをそのまま渡しがち

関数が第一級オブジェクトではないRubyでは、「オブジェクトとして扱える手続きのまとまり」としてProcオブジェクトが用意されています。

Procオブジェクトは、eachメソッドなどのブロック引数の代わりに使用できます。

しかし通常のオブジェクトのように、括弧内にオブジェクトを渡すだけではエラーが出てしまいます。

# 引数の文字列の長さを取得するProc(プロシージャオブジェクト)の作成
procedure = -> str {str.length}

# 普通のオブジェクトと同じ書き方だとエラーになってしまう
p ["foo", "wm", "engineer"].each(procedure)  # エラー:wrong number of arguments (given 1, expected 0) (ArgumentError)

このように、eachに通常の括弧でProcオブジェクトを渡すとエラーが出てしまいます。

エラーには「eachは引数が0個のはずなのに、1個の引数が与えられている」と書かれています。eachが通常受け取る「ブロック引数」は、「通常の括弧の引数」とは別物としてカウントされているのです。

ブロック引数の代わりにProcオブジェクトを渡すには、以下のように書きます。

# 引数の文字列の長さを取得するProc(プロシージャオブジェクト)の作成
procedure = -> str {str.length}

# Procをブロック引数として渡すために、&演算子を使う
p ["foo", "wm", "engineer"].each(&procedure)  # 出力:["foo", "wm", "engineer"]

このように&演算子をつけることで、Procをブロック引数として渡せるのです。

なお&演算子には、to_procメソッドを自動的に補完する性質があります。この性質を利用すると、上記コードを以下のように簡略化できます。

# ▼ こんな書き方もできる(&演算子はto_procを自動的に補完するため) p ["foo", "wm", "engineer"].each(&:length) # 出力:["foo", "wm", "engineer"]

上記コードにおける (&:length) は、実際には (& :length.to_proc) を意味します。「シンボル.to_proc」でそのシンボルに対応したメソッドを、Procオブジェクトに変換しているのです。

3. Array#mapのブロック引数で、途中結果を返却するためにreturnを書きがち

Array#mapやArray#selectなど、ブロック引数を受けとるタイプのメソッドを使用する際、「ブロックの途中で得られた値」を「そのブロックで得られた最終結果」として返却したい場合があります。

そんなときにRubyでは、「return」ではなく「next」を使わなければなりません。JavaScriptなどの「関数が第一級オブジェクトである言語」に慣れ親しんでいた人には、分かりづらく感じるかもしれませんね。

ブロック引数の途中で「return」を書くと、ブロック引数内の手続きではなく、ブロック引数を受けとるメソッド側の処理が終了してしまいます。

def count_up_odd_index(arr)
  arr.map.with_index do |num, i|
    if (i % 2 == 0) then
      return num
    else
      return num + 1
    end
  end
end
p count_up_odd_index([1, 2, 3, 4])  # 出力:1

ブロック引数を配列要素の数だけ繰り返し実行しようと思っても、returnに到達した最初のループの時点で、関数そのものが終了してしまうのです。

一方「return」ではなく「next」を使うと、ブロック引数の途中で得られた値を、そのループでの結果として取得できます。

def count_up_odd_index(arr)
  arr.map.with_index do |num, i|
    if (i % 2 == 0) then
      next num
    else
      next num + 1
    end
  end
end
p count_up_odd_index([1, 2, 3, 4])  # 出力:[1, 3, 3, 5]

4. 正規表現のマッチ結果を思うように取得できない

Rubyには正規表現パターンと文字列のマッチングを行う演算子「=~」があります。

正規表現とのマッチングを行う場合、多くのプログラミング言語では「正規表現パターン内の括弧で囲われたグループを、マッチング結果の配列などから取得する」ことができます。

しかし「これと同じことをRubyでもできるはずだ!」と思って次のようなコードを書いても、グループを取り出せません。

# 正規表現パターン内のグループ部分が取得できるかと思いきや、、
p (/\/\/(.+)\.(.+?)\// =~ "http://foo.com/")[1]  # 出力:0
# ▲ グループ部分が抜き出せていたら"foo"が取り出せたはず

上記コードを実行すると「0」が出力されます。

実はRubyの「=~」演算子が返却するのは、配列ではなく「正規表現がヒットした位置」なのです。(Rubyでは数値を誤って配列のように要素番号を指定すると、0が得られます)

p /\/\/(.+)\.(.+?)\// =~ "http://foo.com/"  # 出力:5  ◁ =~は正規表現がヒットした位置を返却する

上記のコードで「=~」演算子でマッチングを行った結果、正規表現パターンが一致した最初の文字の要素番号である「5」が返却されました。

では、グループ化した正規表現パターンに一致する文字列は、どうしたら取得できるのでしょうか?

方法は何通りかあります。多くのプログラミング言語と同じ感覚でマッチングを行うのであれば、String#matchメソッドを利用するのが良いでしょう。

一方で、Ruby独自の「=~」演算子を利用したまま、グループ一致を取得することもできます。

以下コードのように、「=~」演算子を実行した後に、いくつかの組み込み変数に格納された値を確認してみましょう。

p /\/\/(.+)\.(.+?)\// =~ "http://foo.com/"  # 出力:5  ◁ =~は正規表現がヒットした位置を返却する

# ▼ マッチした文字列は組み込み変数に入っている
p $&  # 出力: "//foo.com/"
p $1  # 出力: "foo"
p $2  # 出力: "com"

なんと組み込み変数に、マッチング結果が格納されています。

「$&」にはマッチングした全体の文字列、「$1」にはマッチングしたひとつ目のグループ、「$2」にはマッチングしたふたつ目のグループ、「$3」には……というように、マッチングの結果が格納されるのです。

これを知らないと、突然登場した組み込み変数の意味を理解できずに苦労します。「=~」演算子で正規表現マッチングをするときには気をつけましょう。

5. privateなクラスメソッドを定義しようとしてpublicにしがち

Rubyでprivateなインスタンスメソッドを実装する場合、次のように書きます。

# インスタンスメソッドならこう書けるけど、
class Foo
  def do_public_work
    p "done public work"
  end
  def do_all
    do_public_work
    do_private_work
  end
  private
  def do_private_work
    p "done private work"
  end
end

Foo.new.do_public_work  # publicなので実行できる
Foo.new.do_private_work  # エラー:private method `do_private_work' called for # (NoMethodError)
Foo.new.do_all  # do_allの中からprivateメソッドを呼び出せている

インスタンスメソッドでは、「private」を書いた後に定義したメソッドが、privateメソッドになります。

しかしクラスメソッドで同じようにprivateメソッドを作ろうとすると、どうなるでしょうか?

# クラスメソッドだとこう書いてもprivateは無視される
class Hoo
  def self.do_public_work
    p "done public work"
  end
  def self.do_all
    do_public_work
    do_private_work
  end
  private
  def self.do_private_work
    p "done private work"
  end
end

Hoo.do_private_work  # 出力: "done private work"
# ▲ privateと書いたはずなのに呼べてしまう?!

このように「def self.メソッド名」形式でクラスメソッドを定義するときには、privateが無視されてしまいます。

「def self.メソッド名」形式でprivateなクラスメソッドを定義する場合には、以下のように書きましょう。

# privateなクラスメソッドを「private_class_method」で指定する
class Hoo
  def self.do_public_work
    p "done public work"
  end
  def self.do_all
    do_public_work
    do_private_work
  end
  def self.do_private_work
    p "done private work"
  end
  private_class_method :do_private_work
end
Hoo.do_private_work  # エラー: private method `do_private_work' called for Hoo:Class (NoMethodError)
▲ privateなクラスメソッドとして正しく定義できた!

このように「private_class_method」でメソッドのシンボル名を指定することで、privateなクラスメソッドを定義できます。

なお「def self.メソッド名」形式ではなく、「class << self」形式(特異クラス)でクラスメソッドを定義するときには、インスタンスメソッドの定義と同じ記法でprivateを設定できます。

まとめ

Rubyはいわゆる「罠」と呼ばれるような、「一般的に想定される挙動を裏切るような仕様」は比較的少ない言語です。

しかしProcオブジェクトの扱いや、ブロック引数などをはじめとするRuby独自の言語仕様は、プログラミング初心者だけではなく他言語経験のあるエンジニアでも混乱する場合があります。

どの言語でも言えることですが、公式リファレンスなどで正しい言語仕様を知ることが、その言語を正しく使いこなすコツです。なにか壁にぶつかったときは、公式リファレンスを丁寧に読み返して、言語仕様を正しく理解するよう心がけましょう。

Ruby公式リファレンス

執筆:sig_Left
編集:内田一良(じきるう)
監修:石倉彰悟

SHARE

  • 広告主募集
  • ライター・編集者募集
  • WorkshipSPACE
エンジニア副業案件
Workship