ようへいの日々精進XP

よかろうもん

メタプログラミング Ruby 写経メモ - 2 章 月曜日:オブジェクトモデル -

この記事は

メタプログラミング Ruby 第 2 版を写経したり, 感想を書いたりしています.

www.oreilly.co.jp

そして, この記事は

既に 2018 年だけど...

qiita.com

初老丸 Advent Calendar 2017 23 日目の記事です.

オブジェクトモデル

  • このメソッドはどのクラスに所属するものなのか?
  • このモジュールをインクルードしたら何が起きるのか?

オープンクラス

Bookworm の to_alphanumeric メソッド

以下のメソッドは, Bookworm に実装されているテープラベルに書名を印字する為の機能. この機能は, 全ての句読点や特殊文字を削除して, アルファベットとスペースだけを残すというもの.

def to_alphanumeric(s)
  s.gsub(/[^\w\s]/, '')
end

このメソッドには, 以下のようなユニットテストがついている.

require 'test/unit'

class ToAlphanumericTest < Test::Unit::TestCase
  det test_strips_non_alphanumeric_characters
    assert_equal '3 the Magic Number', to_alphanumeric('#3, the *Magic, Number*?')
  end
end

ビルはつかさず, String クラスをオープンして, to_alphanumeric メソッドを追加する.

class String
  def to_alphanumeric
    s.gsub(/[^\w\s]/, '')
  end
end

ユニットテストString#to_alphanumeric を使うように変更.

require 'test/unit'

class ToAlphanumericTest < Test::Unit::TestCase
  def test_strips_non_alphanumeric_characters
    assert_equal '3 the Magic Number', '#3, the *Magic, Number*?'.to_alphanumeric
  end
end

クラス定義

  • Ruby では, クラスを定義するコードとその他のコードに違いは無い
  • クラス定義の中に, 好きなコードを配置出来る
3.times do
  class C
    puts "Hello."
  end
end

以下, 実行例.

[2] pry(main)> 3.times do
[2] pry(main)*   class C
[2] pry(main)*     puts "Hello"
[2] pry(main)*   end
[2] pry(main)* end
Hello
Hello
Hello
=> 3

同じ名前のクラスが 3 つ定義されている訳ではなくて, クラスを再オープンして定義している. 以下のコードで確認することが出来る.

class D
  def x; 'x'; end
end

class D
  def y; 'y'; end
end

obj = D.new
obj.x
obj.y

以下, 実行例.

[7] pry(main)> class D
[7] pry(main)*   def x; 'x'; end
[7] pry(main)* end
=> :x
[8] pry(main)> class D
[8] pry(main)*   def y; 'y'; end
[8] pry(main)* end
=> :y
[9] pry(main)> obj = D.new
=> #<D:0x007fd87330fef0>
[10] pry(main)> obj.x
=> "x"
[11] pry(main)> obj.y
=> "y"

class D が 2 回定義されているわけではなく, 最初に class Dx メソッドが定義されて, 2 回目の class D では既に存在している class D を再びオープンして y メソッドを定義している.

Rubyclass は, クラス宣言というよりもスコープ演算子のようなもの.

Monetize の例

Monetize という gem での実装例.

require 'monetize'

bargain_price = Monetize.from_numeric(99, "USD")
bargain_price.format #=> "$99.00"

以下のように, 数値を Money オブジェクトに変換するショートカットとして Numeric#to_money が使える.

require 'monetize'

standard_price = 100.to_money("USD")
standard_price.format #=> "$100.00"

これは, 以下のように標準クラスの Numeric クラスを再オープンしてメソッドを定義している.

class Numeric
  def to_money(currency = nil)
    Monetize.from_numeric(self, currency || Money.default_currency)
  end
end

オープンクラスの問題点

オープンクラスはカッコイイ部分とダークサイドも存在する...ということで, Array クラスに replace メソッドを追加した場面.

[12] pry(main)> [].methods.grep /^re/
=> [:reverse_each, :reverse, :reverse!, :reject, :reject!, :replace, :repeated_permutation, :repeated_combination, :reduce, :remove_instance_variable, :respond_to?]

上記の通り, うっかり replace メソッドを追加してしまうと, 既存のメソッドを上書いてしまう可能性がある.

オブジェクトモデルの内部

オブジェクトの中身

インスタンス変数

class MyClass
  def my_method
    @v = 1
  end
end

obj = MyClass.new
obj.class

以下, 実行例.

[1] pry(main)> class MyClass
[1] pry(main)*   def my_method
[1] pry(main)*     @v = 1
[1] pry(main)*   end
[1] pry(main)* end
=> :my_method
[2] pry(main)> obj = MyClass.new
=> #<MyClass:0x007fdb2b9ae490>
[3] pry(main)> obj.class
=> MyClass

Object#instance_variables を呼び出すことで, インスタンス変数を確認することが出来る.

[4] pry(main)> obj.instance_variables
=> []
[5] pry(main)> obj.my_method
=> 1
[6] pry(main)> obj.instance_variables
=> [:@v]

インスタンス変数は値が代入された時に初めて出現する為, 同じクラスのオブジェクトであってもインスタンス変数の数が異なることがある.

インスタンスメソッド

Object#methods を呼び出せば, メソッドの一覧を取得することが出来る.

[7] pry(main)> obj.methods.grep(/my/)
=> [:my_method]

殆どのオブジェクトは Object クラスから多くのメソッドを継承しているので, メソッド一覧はとても長いものとなるが, Array#grep を利用すれば意図したメソッドを取得することが出来る.

  • オブジェクトにはメソッドは無く, インスタンス変数とクラスへの参照があるだけ
  • 全てのオブジェクトはクラスのインスタンスだから
  • メソッドは何処にあるのか...

メソッドはオブジェクトではなく, クラスに存在する.

メソッドに関する重要な違いを認識する

まだまだ混乱している俺がメソッドに関する重要な違いを認識する場面...

  • 「MyClass は my_method を持っている」→ MyClass.mymethod のようなクラスメソッドのように呼び出されると思われ混乱する
  • my_method を MyClass のインスタンスメソッドと呼ぶべき

まとめると...

  • オブジェクトのインスタンス変数はオブジェクトに存在する
  • オブジェクトのメソッドはオブジェクトのクラスに存在する

クラスの真相

クラスはオブジェクト

  • クラスはオブジェクト
  • クラスにもクラスがある→ Class クラス
[8] pry(main)> "hello".class
=> String
[9] pry(main)> String.class
=> Class
[10] pry(main)> Class.instance_methods(false)
=> [:allocate, :new, :superclass]
  • new メソッドについては, オブジェクトを生成する時にいつも使っている
  • allocate メソッドは new メソッドを補助するもの
  • superclass メソッドは継承の概念と関連しており頻繁に利用する
[11] pry(main)> Array.superclass
=> Object
[12] pry(main)> Object.superclass
=> BasicObject
[13] pry(main)> BasicObject.superclass
=> nil

モジュール

Class クラスのスーパークラスを確認する場面.

[14] pry(main)> Class.superclass
=> Module
  • Class のスーパークラスは Module である
  • 全てのクラスはモジュールである
  • クラスはオブジェクトの生成やクラスを継承する為の 3 つのインスタンスメソッド(new, allocate, superclass)を追加したモジュール

定数

スコープ

  • 大文字で始まる参照は, クラス名やモジュール名を含めて, 全て定数
  • 定数と変数の重要な違いはスコープ

定数のパス

  • プラグラムにある全ての定数は, ファイルシステムのようにツリー上に配置されている
  • モジュール(及びクラス)がディレクトリ, 定数がファイル
# ファイルのようにパスを使って定数を参照することが出来る
# 定数のパスは, コロン 2 つで区切る
module M
  class C
    X = 'ある定数'
  end
  C::X #=> "ある定数"
end

M::C::X #=> "ある定数"

# ルートを示すコロン 2 つで書き始めれば, 外部の定数を絶対パスで指定可能
Y = 'ルートレベルの定数'
module M
  Y = 'M にある定数'
  Y #=> "M にある定数"
  ::Y #=> "ルートレベルの定数"
end
  • インスタンスメソッド Module#constants は現在のスコープにある全ての定数を返す
  • クラスメソッド Module.constants は, 現在のプラグラムのトップレベル定数を返す
  • パスが必要であれば Module.nesting を利用する
M.constants #=> [:C, :Y]
Module.constants.include? :Object #=> true
Module.constants.include? :Module #=> true

# Module.nesting
Module M
  class C
    module M2
      Module.nesting #=> [M::C::M2, M::C, M]
    end
  end
end

オブジェクトとクラス

オブジェクトとは何だろうか.

  • インスタンス変数の集まりにクラスへのリンクがついたもの
  • オブジェクトのメソッドは, オブジェクトではなくオブジェクトのクラスに住んでいて, クラスのインスタンスメソッドと呼ばれている

クラスとは何だろうか.

ネームスペース

  • Ruby のクラス名は慣習としてパスカルケース(最初の大文字の単語を連結したもの ThisTextIsPascalCased)を使う
  • クラスとモジュールの名前の衝突回避の為にネームスペースにラップする
module Bookworm
  class Text
...

クイズ:引かれていない線

サンプルコード

class MyClass; end
obj1 = MyClass.new
obj2 = MyClass.new

解答

  • Object のクラスは何?
# Object のクラスは Class
[4] pry(main)> Object.class
=> Class
# Module のスーパークラスは Object
[5] pry(main)> Module.superclass
=> Object
  • Class のクラスは何?
# Class のクラスは Class
[6] pry(main)> Class.class
=> Class
  • 以下のコードを実行した場合, obj3 はどこに入る?
# 全てのオブジェクトが自分だけのインスタンス変数の一覧を持っている
# 他のオブジェクトとは関係が無い, 同じクラスのオブジェクトであっても, お互いに無関係である
[4] pry(main)> obj3 = MyClass.new
=> #<MyClass:0x007f82c406ae98>
[5] pry(main)> obj3.instance_variable_set("@x", 10)
=> 10

メソッドを呼び出す時に何が起きているのか?

メソッド探索

レシーバと継承チェーン

Ruby はメソッドを呼び出すと以下の 2 つのことを行う.

  1. メソッドを探す(メソッド探索)
  2. メソッドを実行する(self が必要になる)

レシーバとは...

  • 呼び出すメソッドが属するオブジェクトのこと
  • mystring.reverse と書けば, my_string がレシーバである

継承チェーンとは...

メソッド探索を一言で纏めると...

Ruby がレシーバのクラスに入り, メソッドを見つけるまでの継承チェーンを上がること

class MyClass
  def my_method; 'my_method()'; end
end

class MySubClass < MyClass
end

obj = MySubClass.new
obj.my_method()
  • 右へ一歩, それから上へ(one step to the right, then up)
  • レシーバのクラスに向かって右へ一歩進み, メソッドが見つかるまで継承チェーンを上に進む
  • クラスの継承チェーンは ancestors メソッドで確認することが出来る
[5] pry(main)> MySubClass.ancestors
=> [MySubClass, MyClass, Object, PP::ObjectMixin, Kernel, BasicObject]

モジュールとメソッド探索

  • 継承チェーンにはモジュールも含まれる
  • モジュールをクラスに(あるいは別のモジュール)にインクルードすると Ruby はモジュールを継承チェーンに挿入する
[6] pry(main)> module M1
[6] pry(main)*   def my_method
[6] pry(main)*     'M1#my_method()'
[6] pry(main)*   end
[6] pry(main)* end
=> :my_method
[7] pry(main)> class C
[7] pry(main)*   include M1
[7] pry(main)* end
=> C
[8] pry(main)> class D < C; end
=> nil
[9] pry(main)> D.ancestors
=> [D, C, M1, Object, PP::ObjectMixin, Kernel, BasicObject]

prepend メソッド

  • include と同じように動作するが...
  • インクルードしたクラス(インクルーダー)の上ではなく, 下にモジュールが挿入される
[10] pry(main)> class C2
[10] pry(main)*   prepend M1
[10] pry(main)* end
=> C2
[11] pry(main)> class D2 < C2; end
=> nil
[12] pry(main)> D2.ancestors
=> [D2, M1, C2, Object, PP::ObjectMixin, Kernel, BasicObject]

多重インクルード

[1] pry(main)> module M1; end
=> nil
[2] pry(main)> module M2
[2] pry(main)*   include M1
[2] pry(main)* end
=> M2
[3] pry(main)> module M3
[3] pry(main)*   prepend M1
[3] pry(main)*   include M2
[3] pry(main)* end
=> M3
[4] pry(main)> M3.ancestors
=> [M1, M3, M2]
  • M3 が M1 をプリペンドして, それから M2 をインクルードしている
  • M2 も M1 をインクルードしているが, M1 は既に M3 の継承チェーンに含まれている為, この include には何の効果も無い
  • モジュールが既にチェーンに存在していたら, Ruby は 2 回目の挿入を無視する

Kernel

  • Ruby には print のように, どこからでも呼び出せるメソッドがある
  • print は Kernel モジュールの private インスタンスメソッドである
[5] pry(main)> Kernel.private_instance_methods.grep(/^pr/)
=> [:printf, :print, :proc]
  • Object クラスが Kernel モジュールをインクルードしているので, 全てのオブジェクトの継承チェーンに Kernel モジュールが挿入されている
  • Ruby コードは常にオブジェクトの内部で実行されるので, Kernel モジュールのメソッドはどこからでも呼び出せる

以下, Kernel メソッドを試す場面.

[6] pry(main)> module Kernel
[6] pry(main)*   def hagepika
[6] pry(main)*     'hagepika'
[6] pry(main)*   end
[6] pry(main)* end
=> :hagepika
[7] pry(main)> hagepika
=> "hagepika"

メソッドの実行

レシーバの参照

  • メソッドを呼び出す時, Ruby は 2 つのことを行う(メソッドを探す, メソッドを実行する)
def my_method
  temp = @x + 1
  my_other_method(temp)
end
  • @x も my_other_method もレシーバに属している
  • レシーバとは, my_method が最初に呼び出されたオブジェクトのこと

self キーワード

  • Ruby のコードはオブジェクト(カレントオブジェクト)の内部で実行される
  • カレントオブジェクトは self とも呼ばれる
[1] pry(main)> class MyClass
[1] pry(main)*   def testing_self
[1] pry(main)*     @var = 10
[1] pry(main)*     my_method
[1] pry(main)*     self
[1] pry(main)*   end
[1] pry(main)*
[1] pry(main)*   def my_method
[1] pry(main)*     @var = @var + 1
[1] pry(main)*   end
[1] pry(main)* end
=> :my_method
[2] pry(main)> obj = MyClass.new
=> #<MyClass:0x007fa6bc006198>
[3] pry(main)> obj.testing_self
=> #<MyClass:0x007fa6bc006198 @var=11>

トップレベ

  • オブジェクトのメソッドを呼び出すと, そのオブジェクトが self になる
  • メソッドを呼び出さない時には, 誰が self になるのか?
[1] pry(main)> self
=> main
[2] pry(main)> self.class
=> Object

上記のように Ruby のプログラムを開始すると, main と呼ばれるトップレベルコンテキストが self になる.

クラス定義と self

  • クラスやモジュールの定義の内側(メソッドの外側)では, self の役割はクラスやモジュールそのものになる
[3] pry(main)> class MyClass
[3] pry(main)*   self
[3] pry(main)* end
=> MyClass
  • メソッドを呼び出すと, Ruby は「一歩右へ, それから上へ」のルールに従って, メソッドを探し始める
  • メソッドが見つかると, レシーバを self にしてメソッドを呼び出す

private

private メソッドには, 一つのシンプルなルールが適用される.

明示的なレシーバを付けて, private メソッドを呼び出すことが出来ない

以下, 実行例.

[4] pry(main)> class C
[4] pry(main)*   def public_method
[4] pry(main)*     self.private_method
[4] pry(main)*   end
[4] pry(main)*   private
[4] pry(main)*   def private_method; end
[4] pry(main)* end
=> :private_method
[5] pry(main)> C.new.public_method
NoMethodError: private method `private_method' called for #<C:0x007f8d92b1ef28>
from (pry):8:in `public_method'

上記の例は, private メソッドが 2 つのルール(private ルール)に従っていることを示している.

  • 自分以外のオブジェクトのメソッドを呼び出すには, レシーバを明示的に指定する必要がある
  • private のついたメソッドを呼び出す時はレシーバを指定出来ない

Refinements

苦い記憶

class String
  def to_alphanumeric
    gsub(/[^\w\s]/, '')
  end
end

このクラスの問題点は, 変更がグローバルに及ぶことだった.

Refinements という魔術

以下のように, モジュール定義の中で refine を呼び出す.(String クラスに新しい to_alphanumeric メソッドを refine している.)

module StringExtensions
  refine String do
    def to_alphanumeric
      gsub(/[^\w\s]/, '')
    end  
  end
end

これを呼び出す場合には, using メソッドを使って明示的に有効にする必要がある.

using StringExtensions

以下, 実行例.(using を呼び出した時点から, 呼び出したソースファイルにある全てのコードが変更される.)

[8] pry(main)> module StringExtensions
[8] pry(main)*   refine String do
[8] pry(main)*     def to_alphanumeric
[8] pry(main)*       gsub(/[^\w\s]/, '')
[8] pry(main)*     end
[8] pry(main)*   end
[8] pry(main)* end
=> #<refinement:String@StringExtensions>
# using する前
[9] pry(main)> "foo *bar* baz!".to_alphanumeric
NoMethodError: undefined method `to_alphanumeric' for "foo *bar* baz!":String
from (pry):22:in `__pry__'
# using した後
[14] pry(main)> using StringExtensions; "foo *bar* baz!".to_alphanumeric
=> "foo bar baz"

ちなみに, irb や pry で Refinements したメソッドを呼び出す場合には, 上記のように一行で実行する必要があった.

Ruby 2.1 からは...

  • モジュール定義の中でも using を呼び出せるようになった
  • Refinements はモジュール定義の終わりまで有効になる
[1] pry(main)> module StringExtensions
[1] pry(main)*   refine String do
[1] pry(main)*     def reverse
[1] pry(main)*       "esrever"
[1] pry(main)*     end
[1] pry(main)*   end
[1] pry(main)* end
=> #<refinement:String@StringExtensions>
[2] pry(main)> module StringStuff
[2] pry(main)*   using StringExtensions
[2] pry(main)*   "my_string".reverse
[2] pry(main)* end
=> "esrever"
[3] pry(main)> "my_string".reverse
=> "gnirts_ym"

上記は, String#reverse メソッドにパッチを当てているが, それが有効なのは StringStuff モジュール定義の内部だけである.

Refinements の有効範囲

  • refine ブロックそのもの
  • using を呼び出した場所からモジュールの終わりまで(モジュール定義にいる場合), 又はファイルの終わりまで(トップレベルにいる場合)

Refinements が有効になっている場合

  • 新しいメソッドを定義することが出来る
  • 既存のメソッドを再定義することが出来る
  • モジュールの include や prepend も可能
  • Refinements が有効になっているコードは, リファインされたコードよりも優先される

通常のオープンクラスが出来ることなら何でも出来る.

クイズ:絡み合ったモジュール

絡み合ったモジュール

module Printable
  def print
    # ...
  end
  
  def prepare_cover
    # ...
  end  
end

module Document
  def print_to_screen
    prepare_cover
    format_for_screen
    print
  end
  
  def format_for_screen
    # ...
  end
  
  def print
    # ...
  end
end

class Book
  include Document
  include Printable
  # ...
end

print_to_screen メソッドが, 正しい print メソッドを呼び出していない

Printable モジュールと Document モジュールのどちらの print メソッドを呼び出しているのだろうか...

以下, ancestors 例.

[4] pry(main)> module Printable
[4] pry(main)*   def print
[4] pry(main)*     #
[4] pry(main)*   end
[4] pry(main)*   def prepare_cover
[4] pry(main)*     #
[4] pry(main)*   end
[4] pry(main)* end
=> :prepare_cover
[5] pry(main)> module Document
[5] pry(main)*   def print_to_screen
[5] pry(main)*     prepare_cover
[5] pry(main)*     format_for_screen
[5] pry(main)*     print
[5] pry(main)*   end
[5] pry(main)*   def format_for_screen
[5] pry(main)*     #
[5] pry(main)*   end
[5] pry(main)*   def print
[5] pry(main)*     #
[5] pry(main)*   end
[5] pry(main)* end
=> :print
[6] pry(main)> class Book
[6] pry(main)*   include Document
[6] pry(main)*   include Printable
[6] pry(main)* end
=> Book
[7] pry(main)> Book.ancestors
=> [Book, Printable, Document, Object, PP::ObjectMixin, Kernel, BasicObject]

Document#print を呼び出したい場合...

...
class Book
  include Printable
  include Document
end
...

上記のように Book クラスでモジュールのインクルード順番を入れ替えれば良い.

[10] pry(main)> class Book2
[10] pry(main)*   include Printable
[10] pry(main)*   include Document
[10] pry(main)* end
=> Book2
[11] pry(main)> Book2.ancestors
=> [Book2, Document, Printable, Object, PP::ObjectMixin, Kernel, BasicObject]

まとめ

  • オブジェクトは複数のインスタンス変数とクラスへのリンクで構成される
  • オブジェクトのメソッドはオブジェクトのクラスに住んでいる(インスタンスメソッド)
  • クラスは Class クラスのオブジェクト
  • Class は Module のサブクラス
  • 定数はファイルシステムのようなツリー状に配置されている
  • クラスは BasicObject まで続く継承チェーンを持っている
  • メソッドを呼び出すと, 一歩右へ進み, それから継承チェーンを上へ向かって進む
  • クラスにモジュールを include すると, そのクラスの継承チェーンの真上にモジュールが挿入される
  • モジュールを prepend すると, そのクラスの継承チェーンの真下にモジュールが挿入される
  • メソッドを呼び出す時には, レシーバが self になる
  • モジュール(又はクラス)を定義する時には, そのモジュールが self になる
  • Refinements は using を呼び出したところから, ファイルやモジュールの定義が終わる所までの限られた部分のみで有効になる

以上

メタプログラミング Ruby 第 2 版の第 2 章について, ざっくりと纏めてみました.