30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

Ruby のモジュールの基礎

使用している Ruby のバージョンは2.5.3

モジュールの定義

以下の構文で定義する。

module モジュール名
end

モジュールはクラスと違い、インスタンスを作ることは出来ない。

module MyModule
end
MyModule.new # undefined method `new' for MyModule:Module (NoMethodError)

ミックスイン

クラスが、あるモジュールを取り入れてそのモジュールのメソッドを使えるようにすることを、ミックスインと呼ぶ。

includeでミックスインすると、モジュールで定義したメソッドをインスタンスメソッドとして使えるようになる。 ミックスインしたクラスのサブクラスでも、そのまま使える。

module SelfIntroduction
  def who_am_i(oneself)
    p "I'm #{oneself}."
  end
end

class Parent
  include SelfIntroduction

  def hello
    print 'Hello! '
    who_am_i self
  end
end

class Child < Parent
  def hey
    print 'Hey! '
    who_am_i self
  end
end

parent = Parent.new
parent.hello # Hello! "I'm #<Parent:0x00007ff97d895868>."
child = Child.new
child.hey # Hey! "I'm #<Child:0x00007ff97d895638>."

extendを使ってミックスインすると、モジュールのメソッドを、そのクラスの特異メソッド(クラスメソッド)として使うことが出来る。

module SelfIntroduction
  def who_am_i(oneself)
    p "I'm #{oneself}."
  end
end

class User
  extend SelfIntroduction
end

User.who_am_i(User) # "I'm User."

include されているモジュールを調べる

include?メソッドの引数にモジュールを渡すと、そのモジュールをincludeしているかどうかが返ってくる。
included_modulesメソッドは、includeしているモジュールを配列で返す。

module SelfIntroduction
end

class Parent
  include SelfIntroduction
end

class Child < Parent
end

p Parent.include?(SelfIntroduction) # true
p Child.include?(SelfIntroduction) # true

p Parent.included_modules # [SelfIntroduction, Kernel]
p Child.included_modules # [SelfIntroduction, Kernel]

Kernel とトップレベル

prequireといったメソッドは、Kernelというモジュールで定義されている。
そして、ObjectクラスがKernelincludeしているため、Objectを継承している全てのクラスで、pなどのメソッドを使える。

p Object.included_modules # [Kernel]

Ruby では、クラス構文やモジュール構文などで囲まれていない一番外側の部分を、「トップレベル」と呼ぶ。
そしてトップレベルのselfは、mainという名前のオブジェクトを指すが、これはObjectクラスのインスタンスである。トップレベルでpなどを使えるのは、このため。

p self # main
p self.class # Object
p self.class.include?(Kernel) # true

モジュールは Module クラスのインスタンス

モジュールは、Moduleクラスのインスタンスである。

module SelfIntroduction
end

p SelfIntroduction.class # Module

そして、Moduleクラスは、Objectクラスを継承している。

p Module.superclass # Object

ClassクラスはModuleクラスを継承している。
そのため、継承関係はClass -> Module -> Object -> BasicObjectになっている。

p Class.superclass # Module
p Module.superclass # Object
p Object.superclass # BasicObject

全てのクラスは、Classクラスのインスタンスでもある。

p Class.class # Class
p Module.class # Class
p Object.class # Class
p BasicObject.class # Class

ここらへんは自分でも理解できていない。循環参照のようになっていないか? ClassBasicObjectObjectを継承しているが、それらスーパークラスは、Classのインスタンスなのだから、Classから生まれてくる。だからまず先にClassが存在しているはず。しかしそのClassBasicObjectなどを継承しておかないといけない。Class定義時にはまだBasicObjectは存在しないにも拘わらず!
以下の状況が成立してしまうのがよく分からない。

p Module.instance_of?(Class) # true
p Class.superclass # Module

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

クラス定義をモジュール構文で囲うことで、名前空間を分けることが出来る。
参照する際はモジュール名::クラス名と記述する。

module Seller
  class Alice
    def role
      p 'I am seller.'
    end
  end
end

module Buyer
  class Alice
    def role
      p 'I am buyer.'
    end
  end
end

seller = Seller::Alice.new
seller.role # "I am seller."

buyer = Buyer::Alice.new
buyer.role # "I am buyer."

モジュールを入れ子にすることも出来る。

module User
  module PremiumUser
    class Alice
      def initialize
        p 'This is premium user.'
      end
    end
  end
end

User::PremiumUser::Alice.new # "This is premium user."

モジュールが既に定義済みの場合、class モジュール名::クラス名という構文でクラスを定義することも可能。

module Seller
end

class Seller::Alice
  def role
    p 'I am seller.'
  end
end

seller = Seller::Alice.new
seller.role # "I am seller."

トップレベルで定義しているクラスを明示的に呼び出す場合は、::クラス名と記述する。

class Alice
end

module Seller
  class Alice
    def initialize
      print Alice
      print ', '
      p ::Alice
    end
  end
end

Seller::Alice.new # Seller::Alice, Alice

関数や定数を提供するためにモジュールを使う

モジュールに特異メソッドを定義すれば、ミックスインすることなくそのメソッドを使える。

module SelfIntroduction
  def self.who_am_i(oneself)
    p "I'm #{oneself}."
  end
end

SelfIntroduction.who_am_i 1 # "I'm 1."

ミックスインとしても特異メソッドとしても使えるメソッドを、モジュール関数という。
モジュール関数はmodule_functionを使って定義する。
モジュール関数は自動的にprivateになるので、レシーバを指定して呼び出すことは出来ない。

module SelfIntroduction
  def who_am_i(oneself)
    p "I'm #{oneself}."
  end

  module_function :who_am_i
end

class User
  include SelfIntroduction

  def hello
    print 'Hello! '
    who_am_i self
  end
end

SelfIntroduction.who_am_i 1 # "I'm 1."
user = User.new
user.hello # Hello! "I'm #<User:0x00007fed11115930>."

user.who_am_i # private method `who_am_i' called for #<User:0x00007fed11115930> (NoMethodError)

モジュールは定数を定義することもでき、モジュール名::定数名で取得できる。

module SelfIntroduction
  SOME_VALUE = 'foo'
end

p SelfIntroduction::SOME_VALUE # "foo"

メソッド探索

クラスに対してancestorsメソッドを使うと、クラスやモジュールの配列が返ってくる。
クラスのインスタンスがメソッドを呼び出すとき、ancestorsの返り値の順番でメソッドを探索し、最初に見つかったメソッドを実行する。
最後まで見つからなかった場合はNoMethodErrorになる。

module A
end

class Parent
  include A

  def foo
    p "Parent's foo"
  end
  def bar
    p "Parent's bar"
  end
end

class Child < Parent
  def foo
    p "Child's foo"
  end
end

p Child.ancestors # [Child, Parent, A, Object, Kernel, BasicObject]
child = Child.new
child.foo # "Child's foo"
child.bar # "Parent's bar"

モジュールをincludeした場合、モジュールのメソッドより先にクラスのインスタンスメソッドを探索するが、prependメソッドでミックスインした場合は、ミックスインしたモジュールのメソッドを先に探索する。

module A
end

class Foo
  include A
end

class Bar
  prepend A
end

p Foo.ancestors # [Foo, A, Object, Kernel, BasicObject]
p Bar.ancestors # [A, Bar, Object, Kernel, BasicObject]

参考資料

gihyo.jp