やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

「オブジェクト」「クラス」を読む2

 疑問に思ったことをググってコード書いて試してみた。

成果物

情報源

ソースコードで書いてみる

 前回で概念はなんとなくわかった。でも実際に書いてみないとよくわからない。以下を参考にするとわかりやすい。

 というわけでやってみる。

  1. クラスを定義する
    1. インスタンスを生成する
    2. メソッドを呼ぶ
  2. クラスを継承する
    1. 親メソッドを呼ぶ
    2. 自メソッドを呼ぶ
    3. 同名なら自メソッドが呼ばれる
  3. 特異クラス
  4. モジュールを定義する
    1. 定数を定義する
    2. インスタンスメソッドを定義する
    3. モジュール関数を定義する
    4. クラスメソッドを定義する
    5. 優先順位
  5. モジュールを名前空間として使う
    1. 定数を定義する
    2. 定数を参照する
  6. モジュールをクラスへMixinする
    1. include
    2. prepend
    3. extend
  7. 継承リストModule#ancestors
  8. モジュールをネストする

1. クラスを定義する

class C
end
class C; end

1-1. インスタンスを生成する

class C; end
c = C.new

1-2. メソッドを呼ぶ

class C; def m; :m; end; end
C.new.m

2. クラスを継承する

class C; def m; :m; end; end
class D < C; end

2-1. 親メソッドを呼ぶ

class C; def m; :m; end; end
class D < C; end
D.new.m

2-2. 自メソッドを呼ぶ

class C; def m; :m; end; end
class D < C; def n; :n; end; end
D.new.n

2-3. 同名なら自メソッドが呼ばれる

class C; def m; :m; end; end
class D < C; def m; :n; end; end
D.new.m #=> :n

3. 特異クラス

 特異クラスとは特異メソッドをもつクラスである。各オブジェクトが必ず1つ持っている。singleton_classで参照できる。

 特異メソッドとは、特定のインスタンスだけがもつメソッドである。

class C; def m; :m; end; end
c = C.new
d = C.new
c.m #=> :m
d.m #=> :m
def d.m; :n; end; # 特異メソッド
c.m #=> :m
d.m #=> :n
> C.singleton_class
=> #<Class:C>
> c.singleton_class
=> #<Class:#<C:0x02450670>>
> d.singleton_class
=> #<Class:#<C:0x02464020>>

 特異クラスは後述する継承リストModule#ancestorsを持っている。その優先度は最優先である。

> c.singleton_class.ancestors
=> [#<Class:#<C:0x02450670>>, C, Object, PP::ObjectMixin, Kernel, BasicObject]
> d.singleton_class.ancestors
=> [#<Class:#<C:0x02464020>>, C, Object, PP::ObjectMixin, Kernel, BasicObject]

4. モジュールを定義する

module M
end
module M; end

4-1. 定数を定義する

module M
  MAX = 100
end
M::MAX

4-2. インスタンスメソッドを定義する

module M
  def m; :m; end
end
module M; def m; :m; end; end
M::m
: undefined method `m' for M:Module (NoMethodError)
M.new.m
: undefined method `new' for M:Module (NoMethodError)
M.m
: undefined method `m' for M:Module (NoMethodError)

 モジュールのインスタンスメソッドは呼び出せない。というよりモジュールはインスタンス生成できない。

 だったらインスタンスメソッドという名前がおかしい気がするのだが。

4-3. モジュール関数を定義する

module M
  def m; :m; end
  module_function :m
end
module M; def m; :m; end; module_function :m; end

 module_functionに渡したメソッドは呼び出せるようになる。

M::m #=> :m
M.m #=> :m
M.new.m
: undefined method `new' for M:Module (NoMethodError)

 ほかのプログラミング言語でいうところのスタティック・メソッドだろう。

4-4. クラスメソッドを定義する

module M
  def self.m; :m; end
end
module M; def self.m; :m; end; end

 メソッド定義するとき名前の前にself.を付与する。

M::m #=> :m
M.m #=> :m
M.new.m
: undefined method `new' for M:Module (NoMethodError)

4-5. 優先順位

 同名のモジュール関数とクラス関数があったとき、先に呼ばれるのはどちらか。答えは後に定義したほう。

module M
  def self.m; p 'c'; end
  def m; p 'm'; end
  module_function :m
end
M.m #=> "m"
module M
  def m; p 'm'; end
  module_function :m
  def self.m; p 'c'; end
end
M.m #=> "c"

5. モジュールを名前空間として使う

5-1. 定数を定義する

module M
  MAX = 100
end

5-2. 定数を参照する

module M
  MAX = 100
end
M::MAX

6. モジュールをクラスへMixinする

6-1. include

module M
  def m; :m; end
end
class C
  include M
end
C.m #=> undefined method `m' for C:Class (NoMethodError)
C::m #=> undefined method `m' for C:Class (NoMethodError)
C.new.m #=> :m

 クラス内でinclude モジュール名とすると、そのモジュールのインスタンスメソッドがクラスに組み込まれる。

 複数includeしたときは、後にincludeされたほうが優先して呼ばれる。

module M
  def m; :m; end
end
module N
  def m; :n; end
end
class C
  include M
  include N
end
C.new.m #=> :n

 includeと自クラスなら自クラスが優先される。

module M
  def m; :m; end
end
class C
  include M
  def m; :c; end
end
C.new.m #=> :c

6-2. prepend

 自分の同名メソッドよりもモジュールのメソッドを優先する。

module M
  def m; :m; end
end
class C
  prepend M
  def m; :c; end
end
C.new.m #=> :m

 includeprependのときでも同じくprependが優先。

module M
  def m; :m; end
end
module N
  def m; :n; end
end
class C
  prepend M
  include N
end
C.new.m #=> :m
module M
  def m; :m; end
end
module N
  def m; :n; end
end
class C
  include N
  prepend M
end
C.new.m #=> :m

 さらにクラスのインスタンスメソッドがあってもprependが最優先。

module M
  def m; :m; end
end
module N
  def m; :n; end
end
class C
  prepend M
  include N
  def m; :c; end
end
C.new.m #=> :m
C.m  #=> : undefined method `m' for C:Class (NoMethodError)
C::m #=> : undefined method `m' for C:Class (NoMethodError)

6-3. extend

 クラスメソッドとして組み込まれる。

 includeprependインスタンスメソッドとして組み込まれた。それに対してextendはクラスメソッドになる。

module M
  def m; :m; end
end
class C
  extend M
end
p C.ancestors #=> [C, Object, Kernel, BasicObject]
p C.singleton_class.ancestors #=> [#<Class:C>, M, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, Kernel, BasicObject]
C::m    #=> :m
C.m     #=> :m
C.new.m #=> : undefined method `m' for #<C:0x024b0b68> (NoMethodError)

 自クラスとextendならextendが優先される。

class D
  extend M
  def m; :d; end
end
p D.ancestors #=> [D, Object, Kernel, BasicObject]
p D.singleton_class.ancestors #=> [#<Class:D>, M, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, Kernel, BasicObject]
p D::m    #=> :m
p D.m     #=> :m

class E
  def m; :e; end
  extend M
end
p E.ancestors #=> [E, Object, Kernel, BasicObject]
p E.singleton_class.ancestors #=> [#<Class:E>, M, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, Kernel, BasicObject]
p E::m    #=> :m
p E.m     #=> :m

 後述する継承リストの順としてはextendされたモジュールのほうが自クラスより低いように見えるのだが? なのになぜextendされたモジュールが優先して呼び出されるのだろう。謎。

 extendは自クラスのancestorsでなくsingleton_classancestorsに追加される。それが関係しているのだろうか。でもsingleton_classancestorsをみてもそのリストの最初は自クラスにみえるんですけど? モジュールはその次にみえるんですけど? なのになぜモジュールのメソッドが優先されたの? わからん。

 以下、公式ドキュメントを読んでもわからんかった。

引数で指定したモジュールのインスタンスメソッドを self の特異メソッドとして追加します。 Module#include は、クラス(のインスタンス)に機能を追加しますが、extend は、ある特定のオブジェクトだけにモジュールの機能を追加したいときに使用します。 引数に複数のモジュールを指定した場合、最後の引数から逆順に extend を行います。

 「特異メソッドとして追加する」というのがポイント。そしてselfはクラスのことだと思われる。ほかのプログラミング言語だとインスタンスを指すので紛らわしい。よくわからなかったのが以下の「特定のオブジェクト」というところ。

機能 概要
include クラスのインスタンスに追加する
extend 特定のオブジェクトに追加する

 「特定のオブジェクト」ってなんぞ? 最初に「特異メソッドとして追加します。」って書いているよね? それってつまり特異クラスに追加するってことじゃないの?

 extendって「指定したクラスオブジェクトがもつ特異クラスの特異メソッドとして、モジュールのインスタンスメソッドを追加する」だと解釈しているのだが。違うのかな? だってソースコードC.singleton_class.ancestorsでモジュールが追加されているじゃん。

 そのことを「特定のオブジェクトに追加する」と表現しているだけなのかな? 特異クラスはオブジェクトごとに1つだけ持っているものだから、そう表現できなくもないか。以下のようにも書いていたし。

extend の機能は、「特異クラスに対する Module#include」と言い替えることもできます。ただしその場合、フック用のメソッドが Module#extended ではなく Module#included になるという違いがあります。

 フック用のメソッドがーとかいう後半はなにをいっているのかわからんけども。ていうかextendの説明なのに「extendではなく」とか言われてもね。何言ってるかわからんですわ。

 で、「クラスがもつ特異クラス」ってどれ? 以下のリストの中になくない? 最初のは単なる自クラスだし、2番目はモジュール。特異クラスは最優先で1番目にあるはずだけど、なくね?

p E.singleton_class.ancestors #=> [#<Class:E>, M, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, Kernel, BasicObject]

 インスタンスの特異クラスならあったけど。

p E.new.singleton_class.ancestors #=> [#<Class:#<E:0x0060f5b8>>, E, Object, Kernel, BasicObject]

 でも参照しているのはクラスはなず。インスタンスではない。

p E::m    #=> :m
p E.m     #=> :m

 だからクラスオブジェクトの特異クラスを参照しているはず。そしてクラスオブジェクトの特異クラスは、ancestorsの中にないようにみえる。なのに呼ばれているのはextendされたモジュール。お前は2番目だろうに。なぜだ。自クラスにも同名メソッドmがある。そして自クラスはancestorsの先頭。ならば自クラスのmメソッドが呼ばれるべきなんじゃないの? わからん!

7. 継承リストModule#ancestors

クラス、モジュールのスーパークラスとインクルードしているモジュールを優先順位順に配列に格納して返します。

 ancestorsは「祖先」という意味。

 継承したりMixin(include,prepend,extend)したらこのancestorsにその型が入る。継承された順である。

p Integer.ancestors
[Integer, Numeric, Comparable, Object, PP::ObjectMixin, Kernel, BasicObject]
BasicObject
↑
Kernel
↑
PP::ObjectMixin
↑
Object
↑
Comparable
↑
Numeric
↑
Integer

7-1. メソッド呼出優先順序

 Module#ancestorsは継承順にしてメソッド呼出優先順序でもある。

 リストの先頭から同一名のメソッドを探索し、最初にみつかったメソッドを呼び出す。なので、同じ名前のメソッドが複数あったとき、リストの前の方にあるやつが呼び出される。

7-2. 優先度=特異クラス<prepend<自クラス<include

class C; end
C.ancestors
[C, Object, PP::ObjectMixin, Kernel, BasicObject]
class C; def m; :c; end; end
C.ancestors
C.new.m #=> :c

 mメソッドを探す。最初はModule#ancestorsの先頭にあるCクラスから。まさにそのクラスで定義されているので、それを実行する。

[C, Object, PP::ObjectMixin, Kernel, BasicObject]

 次にincludeしてみる。

module M; def m; :m; end; end 
class C; include M; def m; :c; end; end
C.ancestors
C.new.m #=> :c

 mメソッドを探す。最初はModule#ancestorsの先頭にあるCクラスから。そのクラスで定義されているので、それを実行する。:cを返す。

=> [C, M, Object, PP::ObjectMixin, Kernel, BasicObject]

 次にprependしてみる。

module M; def m; :m; end; end 
class C; prepend M; def m; :c; end; end
C.ancestors
C.new.m #=> :c

 mメソッドを探す。最初はModule#ancestorsの先頭にあるMモジュールから。そのモジュールで定義されているので、それを実行する。:mを返す。

=> [M, C, Object, PP::ObjectMixin, Kernel, BasicObject]

 ようするにinclude,prepend,継承は、単にModule#ancestorsへクラスを挿入する操作であるともいえる。

 もし優先順序がわからなくなったらModule#ancestorsを見ればいい。

 ではこれまでの全要素をぶちこんで同名メソッドを作ってみよう。

  • 自クラス
  • 親クラス
  • 特異クラス
  • Module(include,prepend,extend
#!/usr/bin/env ruby
module M
  def m; :mi; end
  def self.m; :mc; end
end
module N
  def m; :ni; end
  def self.m; :nc; end
end
module O
  def m; :oi; end
  def self.m; :oc; end
end
class C
  def m; :c; end
  def self.m; :cc; end
end
class D < C
  prepend M
  include N
  extend O
  def m; :d; end
  def self.m; :cd; end
end
p C.ancestors
p C::m
p C.m
p C.new.m
p D.ancestors
p D::m
p D.m
p D.new.m
d = D.new
e = D.new
p d.singleton_class.ancestors
p e.singleton_class.ancestors
p d.m
p e.m
def e.m; :e; end
p e.singleton_class.ancestors
p d.m
p e.m
p e.singleton_class.ancestors[0].methods
p e.singleton_class.ancestors[0].method :m
p (e.singleton_class.ancestors[0].method :m).call
p e.singleton_class.m

e.extend O
p e.singleton_class.ancestors
p e.m
p e::m
[C, Object, Kernel, BasicObject]
:cc
:cc
:c
[M, D, N, C, Object, Kernel, BasicObject]
:cd
:cd
:mi
[#<Class:#<D:0x00e0f280>>, M, D, N, C, Object, Kernel, BasicObject]
[#<Class:#<D:0x00e0f250>>, M, D, N, C, Object, Kernel, BasicObject]
:mi
:mi
[#<Class:#<D:0x00e0f250>>, M, D, N, C, Object, Kernel, BasicObject]
:mi
:e
[:m, :allocate, :superclass, :new, :<=>, :<=, :>=, :==, :===, :included_modules, :include?, :ancestors, :attr, :attr_reader, :attr_writer, :attr_accessor, :instance_methods, :public_instance_methods, :protected_instance_methods, :private_instance_methods, :constants, :const_get, :const_set, :const_defined?, :freeze, :inspect, :const_source_location, :const_missing, :class_variable_get, :class_variable_defined?, :remove_class_variable, :class_variable_set, :class_variables, :public_constant, :private_constant, :deprecate_constant, :singleton_class?, :<, :prepend, :>, :include, :module_eval, :class_eval, :remove_method, :undef_method, :to_s, :module_exec, :class_exec, :alias_method, :protected_method_defined?, :public_class_method, :method_defined?, :public_method_defined?, :private_method_defined?, :private_class_method, :public_instance_method, :name, :autoload?, :instance_method, :define_method, :autoload, :taint, :tainted?, :untaint, :untrust, :untrusted?, :trust, :methods, :singleton_methods, :protected_methods, :private_methods, :public_methods, :instance_variables, :instance_variable_get, :instance_variable_set, :instance_variable_defined?, :remove_instance_variable, :instance_of?, :kind_of?, :is_a?, :class, :frozen?, :then, :public_send, :method, :public_method, :singleton_method, :tap, :define_singleton_method, :extend, :clone, :yield_self, :to_enum, :enum_for, :=~, :!~, :nil?, :eql?, :respond_to?, :object_id, :send, :display, :hash, :singleton_class, :dup, :itself, :!, :!=, :equal?, :instance_eval, :instance_exec, :__id__, :__send__]
#<Method: #<Class:#<D:0x00f2b338>>(D).m() ./c.rb:23>
:cd
:cd
:cd
[#<Class:#<D:0x005bb240>>, O, M, D, N, C, Object, Kernel, BasicObject]
:e
:e

 インスタンスメソッドの優先度は次のとおりである。

[#<Class:#<D:0x00e0f250>>, M, D, N, C, Object, Kernel, BasicObject]
  1. 特異クラス
  2. prepend
  3. 自クラス
  4. include
  5. 親クラス

 extendがよくわからない。クラス定義内でextendしたらancestorsに入ってなかった。クラスメソッドとしてMixinされるようだが、D::mのように呼び出しても:cdとなり自クラスメソッドが呼び出されている。Oモジュールのメソッドは呼ばれないのか? 優先度が低い? いや、そもそもancestorsの中にOモジュールがない。

class C
  extend O
end
# O がないよ!
[#<Class:#<D:0x00e0f250>>, M, D, N, C, Object, Kernel, BasicObject]

 インスタンスからextendメソッドを呼び出すとancestorsに入っていた。

e.extend O

 どゆこと? 上記コードは同じことをやっているんじゃないの? e.extend Oしたときのancestorsは以下。

[#<Class:#<D:0x005bb240>>, O, M, D, N, C, Object, Kernel, BasicObject]
  1. 特異クラス
  2. expand
  3. prepend
  4. 自クラス
  5. include
  6. 親クラス

 以下をみてみると特異クラスの中にMixinされるようだが。

 わからん!

8. モジュールをネストする

#!/usr/bin/env ruby
module Name
  module M
    def m; :m; end
  end
end
class C; include Name::M; end
p C.new.m

所感

 特異クラス、特異メソッド、extendあたりが怪しい。とくにextendについて。

  • どんな記述方法があるのか
  • その効果に違いはあるのか

 まさかと思うが、includeprependについてもメソッドがあって、class内に書いたときと動作が違ったりするんじゃないの?

 もう疲れたよパトラッシュ。たのむから断片的な情報でなくまとめてくれや。ググッて断片情報みつけて試してこれまでのルールぶち壊されての繰り返しとか、勘弁して。ちゃんと勉強させてよ公式ドキュメント。

対象環境

$ uname -a
Linux raspberrypi 5.10.52-v7l+ #1441 SMP Tue Aug 3 18:11:56 BST 2021 armv7l GNU/Linux