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

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

『プロを目指す人のためのRuby入門 言語仕様からテスト駆動開発・デバッグ技法まで』を読んだ

仕事で多少 Rails を触る機会があったが、そもそも Ruby を理解していないとダメだなと感じて、本書を読んだ。

gihyo.jp

とても分かりやすく、Ruby に対する理解が深まった。

ちゃんと説明しているのがよい。
入門者向けだからと誤魔化さず、言語仕様について程よく踏み込んでいる。
例えばprivateメソッドについて、「そのクラスの中でのみ使える」ではなく「レシーバを指定して呼び出すことが出来ないメソッド」と説明しており、これはとても分かりやすかったし、protectedとの違いも分かりやすくなる。

ほぼ全ての説明にサンプルコードが書かれてあるのも素晴らしかった。何か新しいことを説明する度に、そのサンプルが出てくる。
言葉での説明だけだと伝えにくい概念もあるが、サンプルコードがあることで理解がスムーズになる。
サンプルコードの質も高く、簡潔にまとまっていて、伝えたいことに的を絞った内容になっている。

まともなコードを書くためには、最低限の言語仕様は理解していないと話にならない。テクニックやコーディングスタイル以前の問題。
そういう意味でまさしく「プロを目指すため」の、つまり職業プログラマとしてちゃんとしたコードを書けるようになるための、書籍だと思う。

言語仕様の説明だけを淡々とするのではなく、理解を深めるための練習問題をTDDで進めたり、バックトレースの読み方を解説したり、ネットで見つけた記事を鵜呑みにすることを戒めたりしており、そういったところにも、実際に開発の現場で Ruby を使えるようになってもらおうという意図を強く感じた。

プログラミング自体は初めてではないが Ruby や Rails は触ったことがなく、これから本格的に勉強していきたいという人は、まず本書から入ると学習がスムーズに進むと思う。
非常にオススメ。

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