for Startups Tech blog

このブログのデザインを刷新しました。(2023/12/26)

意外と知らないRubyの基礎知識(と少し応用)

こんにちは。フォースタートアップス株式会社、エンジニアの石田です。

前回の記事では「主にバックエンドのエンジニアをしています。」と自己紹介しましたが、今はバックエンドとフロントエンド両方を担当しています。

特に直近数ヶ月はフロントエンド(React/Next + TypeScript)ばかりやっています。ですが、私が一番好きな言語はRubyですので、今回も前回と同じくRubyの話をしたいと思います。

今回の内容はRubyの基礎知識+豆知識的な内容になります。後半になるにつれて認知度が低く、マニアック度が高くなる気がします。お付き合いください。

ゲッター/セッターメソッド

Rubyで頻繁に使うattr_reader, attr_writer, attr_accessor の正体について説明します。

class Hoge
  attr_reader :foo # ゲッターを定義
  attr_writer :bar # セッターを定義
  attr_accessor :baz # ゲッター/セッターを定義

  def initialize(foo, bar, baz)
    @foo, @bar, @baz = foo, bar, baz
  end
end

これはよく見るRubyのコードですね。ゲッター・セッターを定義しています。Rubyにおいてこの「ゲッター・セッター」とは何でしょうか?実はこれはただのメソッドです。

上のコードは下のコードと同じ意味です(注意: 文字通り「意味的に」同じという意味です。Rubyインタプリタがどのようにこれを実行するかは別です。後述)。

class Hoge
  def initialize(foo, bar, baz)
    @foo, @bar, @baz = foo, bar, baz
  end

  # ゲッターを定義
  def foo
    @foo
  end

  # セッターを定義
  def bar=(val)
    @bar = val
  end

  # ゲッター/セッターを定義
  def baz
    @baz
  end
  def baz=(val)
    @baz = val
  end
end

hoge = Hoge.new(1, 2, 3)
hoge.bar=(100) # 普通のメソッド実行
hoge.bar= 100  # カッコが省略できる
hoge.bar = 100 # 特別ルールでメソッド名の「=」の前にスペースが入ってもOK

Rubyインスタンス変数は全て必ずprivateスコープです。外部からアクセスするためにはpublicスコープのメソッドをつかう必要があります。

attr_〇〇 を使うとこれらのメソッドが定義されます。特に注目なのがセッターメソッドです。実際にはメソッドの実行なので、bar=という名前のメソッドを実行しているだけですが、hoge.bar = 100 というふうにあたかもhoge.barに値を代入しているように書くことができます。この辺がRubyマジックですね。

注意

attr_〇〇を使うのとゲッター/セッターメソッドを自力で書くのは意味的には同じですが、実行速度は違います。(少なくともCRubyでは)attr_〇〇はCレベルの専用処理が動くのでattr_〇〇を使ったほうが実行速度は速くなります。特に理由がなければattr_〇〇の方を使いましょう。

privateメソッドは「外部からアクセスできないメソッド」という意味ではない(?)

以下のコードがあったとします。これを実行するとNoMethodErrorが発生します。privateなので当たり前ですね。

class Hoge
private

  def say_foo
    puts 'foo'
  end
end

hoge = Hoge.new
hoge.say_foo # private method `say_foo' called for #<Hoge:0x00007f86bb887f40> (NoMethodError)

では以下のコードの場合はどうなるでしょうか?

class Hoge
  def public_say_foo
    self.say_foo
  end

private

  def say_foo
    puts 'foo'
  end
end

hoge = Hoge.new
hoge.public_say_foo # どうなるでしょう?

この実行結果は実はRubyのバージョンによって変わります

# Ruby 2.7より前
hoge.public_say_foo # private method `say_foo' called for #<Hoge:0x00007f86bb887f40> (NoMethodError)

# Ruby 2.7以降
hoge.public_say_foo #=> foo

なぜRuby 2.7より前のバージョンではNoMethodErrorが発生したのでしょうか?それは以下のような理屈です。

  • もともとprivateメソッドは「レシーバを明示的に指定して実行できないメソッド」という意味だった
  • レシーバのselfは省略できる
  • privateメソッドはレシーバを省略しなければならない = レシーバは必ずself

上記コードはレシーバselfを明示的に指定しているので、Ruby 2.7より前ではNoMethodErrorが発生しました。

Ruby 2.7ではこれにルールが追加されました。

なのでRuby 2.7以降ではエラーにならなかったんですね。 レシーバselfを明示的に指定しない場合はバージョンに関わらずエラーにはなりません。

class Hoge
  def public_say_foo
    say_foo
  end

private

  def say_foo
    puts 'foo'
  end
end

hoge = Hoge.new
hoge.public_say_foo # どのバージョンでも foo が返る

子クラスからのprivateの実行

さて、次は以下のコードです。

class SuperHoge
private

  def say_foo
    puts 'foo'
  end
end

class Hoge < SuperHoge
  def public_say_foo
    say_foo
  end
end

hoge = Hoge.new
hoge.public_say_foo # どうなるでしょう?

これは"foo"が返ります。

hoge.public_say_foo #=> foo

先述の通りprivateメソッドはレシーバを省略すれば実行できます。そのクラスに対象メソッドが定義されていなかった場合、Rubyは自動的に先祖クラスを順番に探しに行きます。この場合SuperHogeクラスにsay_fooが存在しているので無事実行できました。

「あれ?」と思った方へ

ここまで読んで、一部の方(とくにJavaオブジェクト指向を学んだ方)はこう思うかもしれません。「子孫クラスで先祖クラスのprivateメソッドを実行できるならprotectedスコープって何?」と。これについては次の章で話しますが、RubyprotectedJavaprotectedは全く別の概念です。

とりあえず今は「子孫クラスで先祖クラスのprivateメソッドを実行できる」とおぼえて、例えばテンプレートメソッドパターン(リンクは弊社ブログ)protectedキーワードが使われているのを見つけたら、そっとprivateに置き換えてあげてください

protectedメソッドは「自身と子孫のクラスからしかアクセスできないメソッド」という意味ではない

先述の通り、RubyprotectedJavaprotectedは全く別の概念です。 Rubyのprotectedメソッドは「自身と子孫のクラスからしかアクセスできないメソッド」という意味ではありません。

Ruryのprotected は公式ドキュメントでは以下のように説明されています。

protected に設定されたメソッドは、そのメソッドを持つオブジェクトが selfであるコンテキスト(メソッド定義式やinstance_eval)でのみ呼び出せます。

https://docs.ruby-lang.org/ja/latest/doc/spec=2fdef.html#limit

これだけ読んでもよく意味がわかりませんね。簡単な例を用意しました。

protectedを使ったサンプルコード

サンプルの要件は以下です。

  1. FullNameという値オブジェクトクラスが存在している
  2. FullNameクラスのインスタンスは2つの値を持っている
    • first_name
    • family_name
  3. FullNameクラスのインスタンスの比較は「持っている値がすべて同じ場合」はtrueにしたい
  4. FullNameクラスが持っている値は外部から隠蔽したい(この要件が存在する理由は不明ですが、とにかくそういう要件があると思ってください)。

この要件を満たすFullNameクラスを作っていきます。

まず単純に要件1と要件2を満たすクラスを作ってみます。

class FullName
  def initialize(first_name, family_name)
    @first_name = first_name
    @family_name = family_name
  end
end

foo = FullName.new('foo', 'bar')
bar = FullName.new('foo', 'bar')

puts foo == bar #=> false <- これがtrueになるようにしたい

このままでは要件3を満たしません。要件3を満たすために==メソッドをoverrideしましょう。

class FullName
  def initialize(first_name, family_name)
    @first_name = first_name
    @family_name = family_name
  end

  def ==(other)
    self.first_name  == other.first_name && \
    self.family_name == other.family_name
  end

  def first_name
    @first_name
  end

  def family_name
    @family_name
  end
end

foo = FullName.new('foo', 'bar')
bar = FullName.new('foo', 'bar')

p foo == bar #=> true <- 要件3を満たす

foo.first_name #=> "foo" <- 要件4違反

これで要件3を満たすようになりました。しかし要件4に違反しています。どうすればいいでしょうか?#first_name, #family_name をprivateにすることはできません。other.first_name, other.family_nameNoMethodErrorになってしまうからです。

ここでprotectedの出番です。

class FullName
  def initialize(first_name, family_name)
    @first_name = first_name
    @family_name = family_name
  end

  def ==(other)
    self.first_name  == other.first_name && \
    self.family_name == other.family_name
  end

protected

  def first_name
    @first_name
  end

  def family_name
    @family_name
  end
end

foo = FullName.new('foo', 'bar')
bar = FullName.new('foo', 'bar')

p foo == bar #=> true <- 要件3を満たす

foo.first_name #=> NoMethodError <- 要件4を満たす

これですべての要件を満たすFullNameクラスができました!

問題は「そもそもこんな要件が実際に存在するのか(とくに要件4)?」ということです。想像の通りprotectedを使わなければいけない場面は相当少ないと思われます。私は実務でも私的なプロジェクトでもprotectedを使ったことが一度もありません。

Rubyの親であるMatz氏もprotectedを実装したことを後悔している様子です

注意

このFullNameは値オブジェクトのクラスとしては完全ではありません。HashのキーとしてFullNameインスタンスが使えないからです。Hashのキーの同一性は==メソッドではなくeql?メソッドを使って比較します。詳しくは述べませんが、Hashのキーとしても使える値オブジェクトを実装したい場合、上記のコードは参考にしないでください

Bool/Boleanクラスは存在しない

どの言語でもありそうなBool/Bolean型・Bool/BoleanクラスというものはRubyには存在しません。確かめてみましょう。

p true.class  #=> TrueClass
p false.class #=> FalseClass

TrueClassFalseClassクラスは全く別のクラスです。共通の親クラスも(すべてのクラスの共通の先祖クラスであるObject/BaseObjectクラスを除いて)ありません。

ちなみに、TrueClass/FalseClass はシングルトンパターンで実装されているのでインスタンスtrue/falseただひとつです。

Procとlambdaの違い

RubyにはProclambdaというかなり似ているけどちょっと違う2つの実装があります。

下のコードを見てみましょう。

proc = Proc.new { |x| x + 1 }
lamda = lambda { |x| x + 1 }

p proc.call(1) #=> 2
p lamda.call(1) #=> 2

p proc.class #=> Proc
p lamda.class #=> Proc

はい、全く同じに見えますね。しかし、細かく見ると違いがあります。

ちなみにProc.newlambdaにはそれぞれエイリアス(のようなもの)があります。

  • Proc.new -> proc
  • lambda -> -> (Ruby1.9より)

書き換えると以下になります。

proc = proc { |x| x + 1 }
lamda = -> (x) { x + 1 }

Procとlambdaはどう違うか

returnの挙動が違う

def say_foo1
  proc = Proc.new { return 10 }
  proc.call
  puts 'foo1'
end

def say_foo2
  lambda = lambda { return 10 }
  lambda.call
  puts 'foo2'
end

puts say_foo1
puts '-----'
puts say_foo2

このコードを実行すると以下の出力になります。

10
-----
foo2
nil

どういうことかというと、Procの場合はreturnした時点で#say_foo1から抜けてしまうんですね。それに対してlambdaはreturnしてもブロックから抜けるだけです(このブロックというものの概念の説明も難しいのですが、余計ややこしくなるので今回は説明しません)。

引数の扱いが違う

以下のコードを見てください。

proc = Proc.new { |a, b| [a, b] }

p proc.call(1) #=> [1, nil]
p proc.call(1, 2, 3) #=> [1, 2]

# 足りないところをnilで埋められる
# 余ったところは切り捨てられる

lambda = lambda { |a, b| [a, b] }

p lambda.call(1) # ArgumentError
p lambda.call(1, 2, 3) # ArgumentError

上記のようにProcとlambdaでは引数の扱いが違います。

Procとlambdaの識別方法

Procとlambdaは#lambda?をつかって識別できます。

prop = Proc.new { |x| x + 1 }
lamda = lambda { |x| x + 1 }

p prop.lambda? #=> false
p lamda.lambda? #=> true

(Procとlambdaはすくなくともruby 1.8のころからあったのに)#lambda? が実装されたのはruby 2.1です。それまではProcなのかlambdaなのか区別するのは困難でした。

Procとlambdaの違いについてまとめ

正直、Procとlambdaの違いは「Rubyの闇」「Rubyの技術的負債」のように感じます。

とりあえずProcは使わずにlambdaのみを使うようにすればトラブルは少なそうです。

ちなみにブロックというものはProcともlambdaとも挙動が違うのですが、それについて話すと長くなりすぎますし、ますます混乱するので割愛します。1つだけ言うとブロックはProcやlambdaと違い、なにかのインスタンスではないので変数に入れたり、引数で渡すことはできないものです。

なぜdef self.hogeと書くとクラスメソッドになるのか

Rubyではクラスメソッドを定義するときに以下のように書くのが普通です。

class Foo
  def self.what_is_your_name?
    puts "my name is Foo"
  end
end

Foo.what_is_your_name? #=> my name is Foo

でもなんでdef self.what_is_your_name?と書くとwhat_is_your_name?はクラスメソッドになるのでしょうか?それを理解するためには段階を踏む必要があります。

Step1. インスタンス固有のメソッドを定義できる

クラスメソッドからは一旦離れて、以下のコードを見てください。

foo = "foo"

def foo.add_ban
  self + "!!!"
end

p foo.add_ban #=> "foo!!!"

bar = "bar"
p bar.add_ban # NoMethodError

実はRubyではインスタンスに固有のメソッドを定義することができます。これを一般的に「fooインスタンスの特異クラスのインスタンスメソッド」と呼びます。単に「fooの特異クラスメソッド」や「fooの特異メソッド」と呼ばれることもあります。(公式の呼び方はなさそう?)

Rubyの「.」には少なくとも3つの意味があります。

  1. メソッド実行: 「.」の左がレシーバインスタンスで右がメソッド名
  2. 特異メソッドの定義: 「.」の左がインスタンスで右がメソッド名
  3. 小数点の「.

上記のdef foo.add_banは2の意味ですね。1の意味では無いので注意です。

Step2. 全てのクラスはインスタンスである

Rubyでは「全てのクラスはインスタンス」です。言っている意味がわからないかもしれませんが、とりあえず以下のコードを見てください。

class Hoge
end

hoge = Hoge.new
p hoge.class #=> Hoge

p Hoge.class   #=> Class
p String.class #=> Class

hogeのクラスはHogeです。これは当たり前ですね。実はHogeのクラスはClassという名前のクラスです(言い換えるとHogeClassクラスのインスタンス)。Hogeに限らすStringなど、全てのクラスはClassクラスのインスタンスです。

なのでHogeクラスは以下のように定義することもできます。

Hoge = Class.new # こうやってクラス定義できる

hoge = Hoge.new
p hoge.class  #=> Hoge

これは余談になりますが、Rubyでクラス名の頭が大文字なのはクラスが定数だからです。Rubyでは変数の頭の文字によってそれがどんな変数なのかを識別します。

  • 先頭が$ : グローバル変数
  • 先頭が@ : インスタンス変数
  • 先頭が@@ : クラス変数(こいつのことは忘れてください)
  • 先頭が大文字(全部大文字である必要はない): 定数
  • 先頭が小文字: ローカル変数

Step3. クラスの特異メソッドを定義できる

「クラスはインスタンス」ということは、Step1のように「クラスに特異メソッドが定義できる」ということです。これがクラスメソッドの正体です。

class Foo
  p self #=> Foo

  def self.what_is_your_name? # `def Foo.what_is_your_name?` と同じ意味
    puts "my name is #{self}"
  end
end

Foo.what_is_your_name? #=> "my name is Foo"

つまり、Rubyには「クラスメソッド」というものは実は存在せず、全てはインスタンスメソッドです。「クラスに所属するインスタンスメソッド」か「特異クラスに所属するインスタンスメソッド」かの違いしかありません。

変数名やメソッド名はASCII文字じゃなくてもいいんだよ

注意: 下記はソースコードファイルをUTF-8で保存して動作確認しています。それ以外の文字コードを使った場合は動作確認していません。

変数名やメソッド名に使える文字にはルールがありますが、「ASCIIコード文字に限る」のようなルールは存在しません。日本語だろうがUnicode記号だろうが使うことができます。

日本語 = "Japanese"

p 日本語 #=> "Japanese"

Q これなんの役にたつの?

A 特になんの役にもたちません。ただしユビキタス言語を定義するときにどうしても英語にできないものがあったときはつかえるかも?(例は思いつきませんが。)

「なんの役にもたちません」と言いましたが、やろうと思えば以下のような事もできます。

def √(x)
  Math.sqrt(x)
end

p √ 2 #=> 1.4142135623730951

def i(num)
  Complex(0, num)
end

e = Math::E
π = Math::PI

# オイラーの公式
p e ** (i π) + 1 == 0 #=> true

世の中にはこういうコードが「美しい」と思う人が一定数いるようで、こういうのがたくさん定義されたgem(例:unicode_math)が存在します。

おわりに

「基礎知識」と言いながらRubyの深淵を撫でるような内容になってしまいました。ボリュームも多いですね。しかし、これでも省略した内容が複数あります。

  • ブロックとはなにか
  • def には戻り値がある
  • なぜprivateと書くとその下のメソッドがprivateになるのか
  • 評価戦略

これらはまた別に機会があれば話せたらよいな、と思います。

お付き合いいただきありがとうございました。

参考文献

メタプログラミングRuby