LoginSignup
0
0

More than 3 years have passed since last update.

メタプログラミングRuby(第I部・水曜日まで)を読んでいく

Last updated at Posted at 2020-11-13

1章 頭文字M

  • メタプログラミングとは、コードを記述するコードを記述することである。

イントロスペクションとは?

プログラミングやRubyでは一般に、イントロスペクションとは実行時にオブジェクト、クラスなどを見てそのことを知る能力です。

cf. Rubyのイントロスペクション

例:

# 以下は、クラス定義です

class A
   def a; end
end

module B
   def b; end
end

class C < A
   include B
   def c; end
end

## イントロスペクション
# Q. Cのインスタンスメソッドとは何ですか?
# A. C.instance_methods # [:c, :b, :a, :to_json, :instance_of?...]

# Q. Cのみ宣言するインスタンスメソッドは何ですか?
# A. C.instance_methods(false) # [:c]

# Q. クラスCの祖先は何ですか?
# A. C.ancestors # [C, B, A, Object,...]

# Q. Cスーパークラス?
# A. C.superclass # A

メタプログラミング

  • ORMのクラスを作りたいとする。
# the_m_word/orm.rb
class Entity
  attr_reader :table, :ident

  def initialize(table, ident)
    @table = table
    @ident = ident
    Database.sql "INSERT INTO #{@table} (id) VALUES (#{@ident})"
  end

  def set(col, val)
    Database.sql "UPDATE #{@table} SET #{col}='#{val}' WHERE id=#{@ident}"
  end

  def get(col)
    Database.sql("SELECT #{col} FROM #{@table} WHERE id=#{@ident}")[0][0]
  end
end

以上のようなクラスを記述しなくても、
ActiveRecord::Base を継承するだけで、実行時にアクセサメソッドが定義されるようなコードを書くことができる。

  • クラスのアトリビュートごとにアクセサメソッドを書くのではなく、 ActiveRecord::Base を継承するだけで、実行時にアクセサメソッドが定義されるようなコードを書く( コードを記述するコードを記述する )ことができる。

第2章 月曜日:オブジェクトモデル

オープンクラス(モンキーパッチ)

  • class の主な仕事は、あなたをクラスのコンテキストに連れて行くこと。

    • クラスの宣言というよりもスコープ演算子のようなもの。
    • 標準クラスを含め、既存のクラスを再オープンして、その場で修正できるということ。
  • 元の同名のメソッドを上書きしてしまった場合など、クラスへの安易なパッチはモンキーパッチという蔑称で呼ばれている。

インスタンス変数

  • Rubyのオブジェクトのクラスとインスタンス変数には何のつながりもない。
    • インスタンス変数は値が代入された時に初めて出現する。
    • インスタンス変数の名前と値は、ハッシュのキーとバリューのようなもの。
      • キーとバリューはどちらもオブジェクトによって異なる可能性がある。
    • インスタンス変数はオブジェクトに住んでおり、メソッドはクラスに住んでいる。

メソッド

  • 同じメソッドであっても、クラスに着目しているときはインスタンスメソッドと呼び、オブジェクトに着目しているときはメソッドと呼ぶ。

クラス

  • クラスはオブジェクト
    • クラスはオブジェクトであり、クラスのクラスはClassクラス。
    • Classクラスのインスタンスはクラスそのもの。
# 引数の"false"は「継承したメソッドは無視せよ」という意味
Class.instance_methods(false) # => [:allocate, :new, :superclass]

モジュール

  • ClassクラスのスーパークラスはModule
    • クラスは、オブジェクトの生成やクラスを継承するための3つのインスタンスメソッド(new, allocalte, superclass)を追加したモジュールである。
Class.superclass # => Module
  • 通常、どこかでインクルードするときはモジュールを選択し、インスタンスの生成や継承をするときはクラスを選択する。

定数

  • 大文字で始まる参照は、クラス名やモジュール名も含めて、すべて定数。

    • 定数の値を変更することもできる。
      • Stringクラスの値を変更して、Rubyをぶっ壊すこともできる。
  • Rubyの定数とファイルの類似性

    • プログラムにあるすべての定数は、ファイルシステムのようにツリー状に配置されている。
      • モジュールおよびクラスがディレクトリ で、定数がファイル。

定数のパス

  • 定数のパスはコロン2つで区切る。
  • 定数ツリーの奥のほうにいるときは、ルートを示すコロン2つで書き始めれば、外部の定数を絶対パスで指定できる。

  • インスタンスメソッド Module#constants

    • 現在のスコープにあるすべての定数を戻す。 # => [:C, :Y]
      • ファイルシステムのlsコマンドのようなもの。
  • クラスメソッド Module.constants

    • 現在のプログラムのトップレベル定数を戻す。
  • クラスメソッド Module.nesting

    • パスを含んだ定数を返す。 # => [M::C::M2, M::C, M]

オブジェクトとクラスのまとめ

オブジェクトとは?

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

クラスとは?

  • オブジェクト(Classクラスのインスタンス)にインスタンスメソッドの一覧とスーパークラスへのリンクがついたもの。
    • ClassクラスはModuleクラスのサブクラス。
    • Classクラスにはインスタンスメソッドがある。
      • 例: new
    • クラス名を使って、クラスを参照する。

ネームスペース

  • クラスとモジュールの名前の衝突を回避するために、クラスをネームスペースにラップする。
    • 安易にモンキーパッチを適用すると、予期しない結果を招いてしまう。

loadとrequire

load

  • ファイルをロードしてコードを実行するために使う。
    • 変数はファイルのロード時にスコープを外れるが、スコープは外れない。
  • 呼び出すたびにファイルを実行する。

require

  • ライブラリをインポートするために使う。
  • ファイルを一度しか読み込まない。

Rubyのオブジェクトモデル

  • MyClass
    • class => Class
    • superclass => Object
  • Object
    • class => Class
  • Class
    • class => Class
    • superclass => Module
  • Module
    • superclass => Object

メソッド探索

  • Rubyがレシーバのクラスに入り、メソッドを見つけるまで継承チェーンを上ること。
  • 「右へ一歩、それから上へ(one step to the right, then up)」ルール
    • レシーバのクラスに向かって右へ一歩進み、メソッドが見つかるまで継承チェーンを上へ進むこと。
MySubClass.ancestors # => [MySubclass, MyClass, Object, Kernel, BasicObject]

レシーバ

  • 呼び出すメソッドが属するオブジェクト

継承チェーン

  • クラスの継承チェーンの例
    • クラス → スーパークラス → スーパークラス ...(BasicObjectまで続ける)

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

  • モジュールをクラス(あるいは別のモジュール)にインクルードすると、Rubyはモジュールを継承チェーンに挿入する。
  • それはインクルードするクラスの 真上 に入る。
module M1
  def my_method
    'My#my_method()'
  end
end

class C
  include M1
end

class D < C; end

D.ancestors # => [D, C, M, Object, Kernel, BasicObject]

prependメソッド

  • include と同じように動作するが、インクルード下クラスの にモジュールが挿入される。

メソッドの実行

  • メソッドが呼び出されたときに、レシーバの参照を覚えておくことでメソッド実行時に誰がレシーバなのかを思い出せる。

selfキーワード

  • Rubyのコードは カレントオブジェクトself の内部で実行される。
  • メソッドを呼び出す時は、メソッドのレシーバがselfになる。
    • その時点から、全てのインスタンス変数はselfのインスタンス変数になる。
    • レシーバを明示しないメソッド呼び出しは全てselfに対する呼び出しになる。
    • 他のオブジェクトを明示してメソッドを呼び出すと、今度はそのオブジェクトがselfになる。

トップレベルコンテキスト

  • メソッドを呼び出していないとき、あるいは呼び出したメソッドが全て戻ってきた時の状態
self # => main
self.class # => Object

クラス定義とself

  • クラスやモジュールの定義の内側(メソッドの外側)では、selfの役割はクラスやモジュールそのものになる。
class MyClass
  self # => MyClass
end

privateキーワード

※ 初めてのRubyから説明を引用。
- privateメソッドは、レシーバ省略形式でしか呼び出すことができません。
- したがってselfに対してしか呼び出すことができません。
- これは、同じクラスに属するオブジェクトであっても、他のオブジェクトに対しては呼び出せないということを意味します。
  • 明示的なレシーバをつけてprivateメソッドを呼び出すことはできない
    • privateメソッドは、暗黙的なレシーバ、selfに対するものでなければいけない。

「privateルール」

  • privateのついたメソッドを呼び出すのは自分しかできない。

    1. 自分以外のオブジェクトのメソッドを呼び出すには、レシーバを明示的に指定する必要がある。
    2. privateのついたメソッドを呼び出すときはレシーバを指定できない。
  • 例1

    • オブジェクトxは同じクラスのオブジェクトyのprivateメソッドを呼び出せない。
      • クラスが何であれ、他のオブジェクトのメソッドを呼び出すには、明示的にレシーバを指定する必要があるため。
  • 例2

    • スーパークラスから継承したprivateメソッドは呼び出せる。
      • 継承したメソッドは自分のところにあるので、呼び出す時に明示的にレシーバを指定する必要がないため。

Refinements

  • モンキーパッチとよく似ているが、変更がグローバルに及ばないようにする方法。
module StringExtensions
  refine String do
    def reverse
      "esrever"
    end
  end
end

module StringStuff
  using StringExtensions
  "my_string".reverse # => "esrever"
end

"my_string".reverse # => "gnirts_ym"
  • Refinementsが有効になるのは2箇所だけ。

    1. refineブロックそのものと、
    2. usingを呼び出した場所からモジュールの終わりまで(モジュール定義にいる場合)、またはファイルの終わりまで(トップレベルにいる場合)
  • Refinementsが有効になっている限定されたスコープの中では、Refinementsはオープンクラスやモンキーパッチと同じ。

    • 新しいメソッドを定義することができるし、既存のメソッドを再定義することもできるし、モジュールのincludeやprependもできる。
  • Refinementsが有効になっているコードは、リファインされた側のクラスのコードよりも優先される。

  • クラスをリファインするというのは、元のコードにパッチを貼り付けるようなもの。

2章まとめ

- オブジェクトは複数のインスタンス変数とクラスへのリンクで構成されている。
- オブジェクトのメソッドはオブジェクトのクラスに住んでいる(クラスから見れば、それはインスタンスメソッドと呼ばれる)。
- クラスはClassクラスのオブジェクトである。クラスメイは単なる定数である。
- ClassはModuleのサブクラスである。モジュールは基本的にはメソッドをまとめたものである。それに加えてクラスは、newでインスタンス化したり、superclassで階層構造を作ったりできる。
- 定数はファイルシステムのようなツリー上に配置されている。モジュールやクラスの名前がディレクトリ、通常の定数がファイルのようになっている。
- クラスはそれぞれBasicObjectまで続く継承チェーンを持っている。
- メソッドを呼び出すと、Rubyはレシーバのクラスに向かって一歩右へ進み、それから継承チェーンを上へ向かって進んでいく。メソッドを発見するか継承チェーンが終わるまでそれは続く。
- クラスにモジュールをインクルードすると、そのクラスの継承チェーンの真上にモジュールが挿入される。モジュールをプリペンどすると、そのクラスの継承チェーンの真下にモジュールが挿入される。
- メソッドを呼び出す時には、レシーバがselfになる。
- モジュール(あるいはクラス)を定義する時には、そのモジュールがselfになる。
- インスタンス変数は常にselfのインスタンス変数とみなされる。
- レシーバを明示的に指定せずにメソッドを呼び出すと、selfのメソッドだと見なされる。
- Refinementsはクラスのコードにパッチを当てて、通常のメソッド探索オーバーライドするようなものである。ただし、Refinementsはusingを呼び出したところから、ファイルやモジュールの定義が終わるところまでの限られた部分でのみ有効になる。

第3章 火曜日:メソッド

重複コードを排除する

  1. 動的メソッド
  2. method_missing

1. 動的メソッド

  • 「メソッドを呼び出すというのは、オブジェクトにメッセージを送っていることなんだ」

  • メソッドを呼び出すには、ドット記法か Object#send を使う。

obj = MyClass.new
obj.my_method(3) # => 6
obj.send(:my_method, 3) # => 6

動的ディスパッチ

  • send を使うことによって、呼び出したいメソッド名が通常の引数になり、コードの実行時に呼び出すメソッドを決められること。

  • Pry#refresh の例

def refresh(options={})
  defaults = {}
  attributes = [ :input, :output, :commands, :print, :quiet,
                 :exception_handler, :hooks, :custom_completions,
                 :prompt, :memory_size, :extra_sticky_locals ]

  attributes.each do |attribute|
    defaults[attribute] = Pry.send attribute
  end
  # ...
  defaults.merge!(options).each do |key, value|
    # memory_size= といったアトリビュートのアクセサを呼び出している。
    send("#{key}=", value) if respond_to?("#{key}=")
  end

  true
end
  • 動的ディスパッチを追加したコード例
class Computer
  def initialize(computer_id, data_source)
    @id = computer_id
    @data_source = data_source
  end

  def mouse
    component :mouse
  end

  def cpu
    component :cpu
  end

  def keyboard
    component :keyboard
  end

  def component(name)
    info = @data_source.send "get_#{name}_info", @id
    price = @data_source.send "get_#{name}_price", @id
    result = "#{name.capitalize}: #{info} ($#{price})"
    return "* #{result}" if price >= 100
    result
  end
end

メソッド名とシンボル

  • シンボルは文字列にある文字と違ってイミュータブル(変更不能)であるため、名前に適している。

動的メソッド

  • 実行時にメソッドを定義する技法。
    • 実行時にメソッド名を決定できる。
Module#define_method
class MyClass
  define_method :my_method do |my_arg|
    my_arg * 3
  end
end

obj = MyClass.new
obj.my_method(2) # => 6
  • define_methodを追加したコード例
class Computer
  def initialize(computer_id, data_source)
    @id = computer_id
    @data_source = data_source
  end

  def self.define_component(name)
    define_method(name) do
      info = @data_source.send "get_#{name}_info", @id
      price = @data_source.send "get_#{name}_price", @id
      result = "#{name.capitalize}: #{info} ($#{price})"
      return "* #{result}" if price >= 100
      result
    end
  end

  define_component :mouse
  define_component :cpu
  define_component :keyboard
end
  • data_sourceをインストロスペクションしてさらにリファクタリングした例
class Computer
  def initialize(computer_id, data_source)
    @id = computer_id
    @data_source = data_source
    # Array#grepにブロックを渡すと、正規表現にマッチした要素全てに対してブロックが評価される。
    # 正規表現のかっこにマッチした文字列は、グローバル変数$1に格納される。
    # 例: data_sourceがget_cpu_infoとget_mouse_infoの2つのメソッドを持っていれば、
    # Computer.define_componentを2回呼び出すことになる(それぞれ「cpu」と「mouse」の文字列を渡す)
    data_source.methods.grep(/^get_(.*)_info$/) { Computer.define_component $1 }
  end

  def self.define_component(name)
    define_method(name) do
      # ...
    end
  end
end

method_missing

  • メソッド探索をした時、メソッド が見つからなければ method_missing メソッドを呼び出して負けを認める。
    • method_missing は全てのオブジェクトが継承する BasicObjectprivate インスタンスメソッド。
nick.send :method_missing, :my_method
# => NoMethodError: undefined method `my_method` for #<Lawyer:0x007f801b0f4978>
class Lawyer
  def method_missing(method, *args)
    puts "呼び出した:#{method}(#{args.join(', ')})"
    puts "(ブロックも渡した)" if block_given? 
  end
end

bob = Lawyer.new
bob.talk_simple('a', 'b') do
  # ブロック
end
# => 呼び出した:talk_simple(a, b)
#  (ブロックも渡した)
  • BasiObject#method_missing の返答は、 NoMethodErrorになる。
    • 宛先不明なメッセージが最終的に行き着く場所であり、NoMethodErrorの誕生する場所。

method_missingのオーバーライド

ゴーストメソッド
  • method_missingで処理されるメッセージであり、呼び出し側からは通常の呼び出しのように見えるが、レシーバ側には対応するメソッドが見当たらない。

  • Hashieの例

module Hashie
  class Mash < Hashie::Hash
    def method_missing(method_name, *args, &blk)
      # 呼び出されたメソッドの名前がハッシュのキーであれば[]メソッドを呼び出して、対応する値を戻す。
      return self.[](method_name, &blk) if key?(method_name)
      # メソッドの名前の最後が「=」であれば、「=」を削除してアトリビュートの名前を取り出してから、その値を保持する。
      # どちらにも合致しなければ、method_missingはデフォルト値を返す。
      match = method_name.to_s.match(/(.*?)([?=!]?)$/)
      case match[2]
      when "="
        self[match[1]] = args.first
      else
        default(method_name, *args, &blk)
      end
    end
  end
end
動的プロキシ
  • メソッド呼び出しを method_missing に集中させゴーストメソッドを補足して、他のオブジェクトに転送するようなラップしたオブジェクトのこと。

  • Ghee(GitHubのHTTP APIに簡単にアクセスできるライブラリ)の例

class Ghee
  class ResourceProxy
    # ...

    def method_missing(message, *args, &block)
      # my_gist.urlのようなメソッド呼び出しを、Hashie::Mash#method_missingに転送する。
      subject.send(message, *args, &block)
    end

    def subject
      # GitHubオブジェクトをJSONで受け取って、Hashie::Mashに変換する。
      @subject ||= connection.get(path_prefix){|req| req.params.merge!params }.body
    end
  end
end
  • Gheeの2つのポイント

    1. GitHubのオブジェクトを動的なハッシュに保持している。ハッシュのアトリビュートは、 urldescription などのゴーストメソッドの呼び出しでアクセスできる。
    2. これらのハッシュをプロキシオブジェクトでラップしている。プロキシオブジェクトには、追加のメソッドが用意されている。プロキシは2つのことを行っている。
      1. star のような処理が必要なメソッドの実装。
      2. ラップしたハッシュに url などのデータを読み取るだけのメソッドを転送する。
  • Geeはこうした2段階の設計によって、コードを簡潔に保っている。

    • データを読み取る時にはゴーストメソッドがあるので、メソッドを定義する必要がない。
    • star のような特定のコードが必要な時だけメソッドを定義する。
  • これらの動的な手法のもう一つの利点は、GitHub APIの変更に自動的に対応できること。

    • GitHubが新しいフィールドを追加したとしても、それもゴーストメソッドとなるため、Gheeはソースコードに修正を加えることなく、呼び出しをサポートできる。
  • method_missingを利用したComputerクラスのリファクタリング

class Computer
  def initialize(computer_id, data_source)
    @id = computer_id
    @data_source = data_source
  end

  # BasicObject#method_missingをオーバーライド
  def method_missing(name)
    super if !@data_source.respond_to?("get_#{name}_info")
    info = @data_source.send("get_#{name}_info", @id)
    price = @data_source.send("get_#{name}_price", @id)
    result = "#{name.capitalize}: #{info} ($#{price})"
    return "* #{result}" if price >= 100
    result
  end

  # ゴーストメソッドがあるかどうかを確認するrespond_to_missing?を、適切にオーバーライドする。
  def respond_to_missing?(method, include_private = false)
    @data_source.respond_to?("get_#{method}_info") || super
  end
end
Module#const_missing
  • 新しいクラスメイトネームスペースのない古いクラスメイを併用できるようにするメソッド。

  • TaskがアップグレードによりRake::Taskという名前に変更された場合の例

class Module
  def const_missing(const_name)
    case const_name
    when :Task
      # 廃止されたクラス名を使っていることに対する警告を出す。
      Rake.application.const_warning(const_name)
      Rake::Task
    when :FileTask
      Rake.application.const_warning(const_name)
      Rake::FileTask
    when :FileCreationTask
      # ...
    end
  end
end

バグのあるmethod_missing

  • 変数 number がブロックの中で定義されていて、 method_missing の最終行でスコープを外れており、最終行の実行時に、Rubyは number が変数だとは気づかず、 self に対するかっこのないメソッド呼び出しだと思ってしまう。
    • 通常であれば、明示的にNoMethodErrorが発生して問題が明らかになるが、ここでは、 method_missing を自分で定義していて、その中で number を呼び出しているため、同じイベントチェーンが何度も繰り返し起き、最終的にはスタックオーバーフローが発生してしまう。
class Roulette
  def method_missing(name, *args)
    person = name.to_s.capitalize
    3.times do
      number = rand(10) + 1
      puts "#{number}..."
    end
    "#{person} got a #{number}"
  end
end
  • 必要もないのにゴーストメソッドを導入しない。
    • 通常のメソッドを書くところから始めて、コードが動いていることを確認してから、method_missingを使うようにリファクタリングする。

ブランクスレート

  • ゴーストメソッド(method_missingで定義されたメソッド)の名前と継承した本物のメソッドの名前が衝突すると、後者が勝ってしまう。

    • 継承したメソッドを削除しておき、最小限のメソッドしかない状態のクラスをブランクスレートと呼ぶ。
  • Rubyのクラス階層のルートであるBasicObjectには、必要最低限のメソッドしか存在せず、Rubyでブランクスレートを手っ取り早く定義するには、BasicObjectを継承すれば良い。

3章まとめ

- ゴーストメソッドは基本的な忠告(常にsuperを呼び出す、常にrespond_to_missing?を再定義する)に従えば、ほとんどの問題は回避できるが、それでも複雑なバグを引き起こす。
- ゴーストメソッドは本物のメソッドではないため、実際のメソッドとは振る舞いが異なる。
- メソッド呼び出しが大量にあったり、呼び出すメソッドが実行時にしかわからなかったりする時など、ゴーストメソッドしか使えない場面もある。
- 「可能であれば動的メソッドを使い、仕方がなければゴーストメソッドを使う」

第4章 水曜日:ブロック

  • ブロックは「呼び出し可能オブジェクト」大家族の一員。
  • ブロックの家族はオブジェクト指向とは血筋が違っていて、LISPなどの「関数型プログラミング言語」の流れをくんでいる。

ブロックの基本

  • ブロックを定義できるのはメソッドを呼び出す時だけ。
    • ブロックはメソッドに渡され、メソッドはyieldキーワードを使ってブロックをコールバックする。
    • ブロックをコールバックするときは、メソッドと同じように引数を渡せる。
    • ブロックは、メソッドと同じように最終行を評価した結果を戻す。
  • メソッドの内部では、 Kernel#block_given? メソッドを使ってブロックの有無を確認できる。

ブロックはクロージャ

  • ブロックのコードを実行するには、ローカル変数、インスタンス変数、selfといった環境が必要になり、これらをまとめてから実行の準備をする。
  • ブロックには、コードと束縛の集まりの両方が含まれる

ブロックと束縛

  • ブロックを定義すると、その時点でその場所にある束縛を取得する。
    • ブロックをメソッドに渡したときは、その束縛も一緒に連れて行く。
def my_method
  # メソッドにある束縛はブロックからは見えない。
  x = "Goodbye"
  yield("cruel")
end

x = "Hello"
# xのようなローカル束縛を包み込み、それからブロックをメソッドに渡す。
my_method = {|y| "#{x}, #{y} world"} # => "Hello, cruel world"

スコープ

  • JavaやC#といった言語には、「内部スコープ」から「外部スコープ」の変数を参照する仕組みがあるが、Rubyにはこうした可視性の入れ子構造は存在せず、スコープはきちんと区別されている。
  • 新しいスコープに入ると(プログラムがスコープを変えると)、以前の束縛は新しい束縛と置き換えられる

グローバル変数とトップレベルのインスタンス変数

# グローバル変数はどのスコープからもアクセスできる。
def a_scope
  $var = "some value"
end

def another_scope
  $var
end

a_scope
another_scope # => "some value"
# トップレベルのインスタンス変数は、トップレベルにあるmainオブジェクトのインスタンス変数であり、
# mainがselfになる場所であればどこからでも呼び出せる。
# 他のオブジェクトがselfになれば、トップレベルのインスタンス変数はスコープから外れる。
@var = "トップレベルの変数@var"

def my_method
  @var
end

my_method # => "トップレベルの変数@var"

class MyClass
  def my_method
    @var = "トップレベルの変数@varではない!"
  end
end

スコープゲート

  • プログラムがスコープを切り替えて、新しいスコープをオープンする場所は3つあり、それぞれを印づけるキーワードをスコープゲートと言う。
    1. クラス定義(class)
    2. モジュール定義(module)
    3. メソッド(def)

スコープのフラット化

  • スコープゲートを超えて束縛を渡し、他のスコープの変数が見えるようにする方法。
    • この魔術を、入れ子構造のレキシカルスコープとも呼ぶ。
    • 2つのスコープを一緒の場所に押し込めて、変数を共有する魔術をフラットスコープと呼ぶ。
my_var = "成功"

MyClass = Class.new do  # class MyClass
  puts "クラス定義のなかは#{my_var}!"

  define_method :my_method do  # def my_method
    "メソッド定義の中も#{my_var}!"
  end
end

共有スコープ

  • 変数をスコープゲートで守りつつ、変数を共有する方法。
def define_methods
  shared = 0

  Kernel.send :define_method, :counter do
    shared
  end

  Kernel.send :define_method, :inc do |x|
    shared += x
  end
end

define_methods

counter # => 0
inc(4)
counter # => 4

クロージャのまとめ

- Rubyのスコープには多くの束縛がある。
    - スコープはclass、module、defといったスコープゲートで区切られている。
- スコープゲートを飛び越えて、束縛にこっそり潜り込みたい時には、ブロック(クロージャ)が使える。
    - ブロックを定義すると、現在の環境にある束縛を包み込んで、持ち運ぶことができる。
        - スコープゲートをメソッド呼び出しで置き換え、現在の束縛をクロージャで包み、そのクロージャをメソッドに渡す。
- classはClass.new、moduleはModule.newと、defはModule.define_methodと置き換えることが可能。
    - これがフラットスコープであり、クロージャに関する基本的な魔術。
- 同じフラットスコープに複数のメソッドを定義して、スコープゲートで守ってやれば、束縛を共有できる。
    - これは、共有スコープと呼ばれる。

instance_eval

  • BasicObject#instance_eval は、オブジェクトのコンテキストでブロックを評価する
  • instance_eval に渡したブロックのことをコンテキスト探索機と呼ぶ。
    • オブジェクトの内部を探索して、そこで何かを実行するコードだから。
class MyClass
  def initialize
    @v = 1
  end
end

obj = MyClass.new

# instance_evalに渡したブロックは、レシーバをselfにしてから評価されるので、
# レシーバのprivateメソッドや@vなどのインスタンス変数にもアクセスできる。
obj.instance_eval do
  self  # => #<MyClass:0x3340dc @v=1>
  @v    # => 1
end

instance_evalとinstance_execの違い

class C
  def initialize
    @x = 1
  end
end

# instance_eval
class D
  def twisted_method
    @y = 2
    # instance_evalがselfをレシーバに変更すると、
    # 呼び出し側(D)のインスタンス変数はブロックから抜け落ちてしまう。
    C.new.instance_eval { "@x: #{@x}, @y: #{@y}" }
  end
end

D.new.twisted_method # => "@x: 1, @y: "

# instance_exec
class D
  def twisted_method
    @y = 2
    # Cのインスタンス変数とDのインスタンス変数を同じスコープに入れることができる。
    C.new.instance_exec(@y) {|y| "#{@x}, @y: #{y}" }
  end
end
D.mew.twisted_method # => "@x: 1, @y: 2"

カプセル化の破壊について

  • コンテキスト探索機(instance_evalに渡したブロック)を使うと、カプセル化を破壊できてしまうため、以下のような場合にユースケースが限られる。

    • irbからオブジェクトの中身を見たい場合
    • テストの場合
  • Padrinoのテストの例

describe "PadrinoLogger" do
  context 'for logger functionality' do
    context "static asset logging" do
      ...
      should 'allow turning on static assets logging' do
        Padrino.logger.instance_eval { @log_static = true } 
        # ...
        get "/images/something.png"
        assert_equal "Foo", body
        assert_match /GET/, Padrino.logger.log.string
        Padrino.logger.instance_eval { @log_static = false }
      end
    end
  end
end

クリーンルーム

  • ブロックを評価するためだけにオブジェクトを生成すること。ブロックを評価する環境。

ブロック以外でコードを保管できるところ

  • 呼び出し可能オブジェクト
    • Rubyで、「コードを保管しておいて、あとで呼び出す」方式
      • Procのなか。これはブロックがオブジェクトのになったもの。
      • lambdaのなか。これはProcの変形。
      • メソッドのなか。

Procオブジェクト

  • Rubyではほぼ全てがオブジェクトだが、ブロックは違う。
  • Proc
    • ブロックをオブジェクトにしたもの。
    • Procを生成するには、Proc.newにブロックを渡す。
      • オブジェクトになったブロックをあとで評価(遅延評価)するには、 Proc#call を呼び出す。
inc = Proc.new {|x| x + 1 }
inc.call(2) # => 3

ブロックをProcに変換する

  • ブロックをProcに変換する2つのカーネルメソッド
    • lambda
    • proc
dec = lambda {|x| x - 1 }
# dec = ->(x) { x - 1 }
dec.class # => Proc
dec.call(2) # => 1

&修飾

  • ブロックはメソッドに渡す無名引数のようなもの。
  • yieldでは足りないケース。

    • 他のメソッドにブロックを渡したいとき
    • ブロックをProcに変換したいとき
  • ブロックに束縛を割り当てる(ブロックを指し示す「名前」をつける) &

    • メソッドの引数列の最後に置いて、名前の前に & の印をつける。
    • & を付けると、メソッドに渡されたブロックを受け取って、それをProcに変換したい、という意味になる。
  • ブロックをProcに変換する例

def math(a, b)
  yield(a, b)
end

# &修飾を行うと、メソッドに渡されたブロックを受け取って、それをProcに変換する!
def do_math(a, b, &operation) 
  math(a, b, &operation)
end

do_math(2, 3) {|x, y| x * y } # => 6
def my_method(&the_proc)
  the_proc
end

p = my_method {|name| "Hello, #{name}!" }
p.class # => "Proc"
p.call("Bill") # => "Hello, Bill!"
  • Procをブロックに戻す例
    • Procをブロックに変換する &
def my_method(greeting)
  "#{greeting}, #{yield}"
end

my_proc = proc { "Bill" }
my_method("Hello", &my_proc)
  • &によるProc変換は「Proc coercion」(coercion: 強制、強要など)と呼ぶ。
    • &の、一つの演算子が与える側と受け取る側の両方で使えて,ちょうど逆の働きをする,という点は多重代入における * とよく似ている。

lambda

  • lambdaで作ったProcは、他のProcとは違う。

    • lambdaで作られたProcオブジェクトはlambdaと呼ばれる。
    • もう一方は、単純にProcと呼ばれる。
      • Procがlambdaかどうかは、Proc#lambda?メソッドで確認できる。
  • Procとlambdaの2つの違い

    1. returnキーワードに関すること
    2. 引数のチェックに関すること

Procとlambdaとreturn

  • lambdaとProcでは、returnキーワードの意味が違う。
# lambdaの場合は、returnは単にlambdaから戻るだけ。
def double(callable_object)
  callable_object.call * 2
end

1 = lambda { return 10 }
double(1) # => 20

# Procの場合は、Procが定義されたスコープから戻る。
def another_double
  p = Proc.new { return 10 }
  result = p.call
  return result * 2 # ここまでは来ない!
end

another_double # => 10

def double(callable_object)
  callable_object.call * 2
end

p = Proc.new { return 10 }
double(p)  # => LocalJumpError

p = Proc.new { 10 }
double(p) # => 20

Procとlambdaと項数

  • 一般的にlambdaの方がProc(や普通のブロック)よりも引数の扱いに厳しい
    • 違った項数でlambdaを呼び出すと、ArgumentErrorになる。
    • 一方、Procは引数列を期待に合わせてくれる。
p = Proc.new
p.call(1, 2, 3) # => [1, 2]
p.call(1)  # => [1, nil]

Proc vs lambda

  • 一般的に言えば、lambdaの方がメソッドに似ているので、Procよりも直感的であると言われている。
  • また、項数に厳しく、returnを呼ぶと単に終了してくれる。
  • Procの機能が必要でない限り、Rubyistの多くは最初にlambdaを選んでいる。

Methodオブジェクト

  • メソッドもlambdaのような呼び出し可能オブジェクトになる。
class MyClass
  def initialize(value)
    @x = value
  end

  def my_method
    @x
  end
end

object = MyClass.new(1)
m = object.method :my_method  # メソッドそのものをMethodオブジェクトとして取得できる
m.call  # => 1  # Object#methodで取得したオブジェクトをMethod#callを使って実行できる
  • MethodMethod#to_procProc に変換できるし、ブロックは define_method でメソッドに変換できる。

  • lambdaは定義されたスコープで評価される(クロージャである)が、Methodは所属するオブジェクトのスコープで評価される。

UnboundMethod

  • 元のクラスやモジュールから引き離されたメソッドのようなもの。
module MyModule
  def my_method
    42
  end
end

unbound = MyModule.instance_method(:my_method)
unbound.class  # => UnboundMethod

呼び出し可能オブジェクトのまとめ

- ブロック(「オブジェクト」ではないが「呼び出し可能」):定義されたスコープで評価される
- Proc:Procクラスのオブジェクト。ブロックのように、定義されたスコープで評価される。
- lambda:これもProcクラスのオブジェクトだが、通常のProcとは微妙に異なる。ブロックやProcと同じくクロージャであり、定義されたスコープで評価される。
- メソッド:オブジェクトに束縛され、オブジェクトのスコープで評価される。オブジェクトのスコープから引き離し、他のオブジェクトに束縛することもできる。

- メソッドとlambdaでは、returnで呼び出し可能オブジェクトから戻る。
    - 一方、Procとブロックでは、呼び出し可能オブジェクトの元のコンテキストから戻る。
- メソッドは呼び出し時の項数の違いに対して厳密であり、lambdaもほぼ厳密である。
    - Procとブロックは寛容である。
- 上記のような違いは、 `Proc.new` 、 `Method#to_proc` 、 `&修飾` などを使って、
ある呼び出し可能オブジェクトから別の呼び出し可能オブジェクトに変換することができる。

ドメイン特化言語

  • 例1
def event(description)
  puts "ALERT: #{description}" if yield
end
loda 'events.rb'

# events.rb
event '常に発生するイベント' do
  true
end

event '絶対に発生しないイベント' do
  false
end

# => ALERT: 常に発生するイベント
  • 例2
def setup(&block)
  @setups << block
end

def event(description, &block)
  @events << {:description => description, :condition => block }
end

@setups = []
@events = []
load 'events.rb'

@events.each do |event|
  @setups.each do |setup|
    setup.call
  end
  puts "ALERT: #{event[:description}" if event[:condition].call
end

グローバル変数の削除

  • 例2・改
lambda {
  # setupsとeventsはlambadのローカル変数のため、外部から見えない
  setups = []
  events = []

  Kernel.send :define_method, :setup do |&block|
    setups << block
  end

  Kernel.send :define_method, :event do |description, &block|
    events << {:description => description, :condition => block}
  end

  Kernel.send :define_method, :each_setup do |&block|
    setups.each do |setup|
      block.call event
    end
  end

  Kernel.send :define_method, :each_event do |&block|
    events.each do |event|
      block.call event
    end
  end
}.call

each_event do |event|
  each_setup do |setup|
    setup.call
  end
  puts "ALERT: #{event[:description]}" if event[:condition].call
end

# あるいはObjectのコンテキストにおけるクリーンルームを使って、event同士でインスタンス変数を共有させないようにできる。

each_event do |event|
  env = Object.new
  each_setup do |setup|
    env.instance_eval &setup
  end
  puts "ALERT: #{event[:description]}" if env.instance_eval &(event[:condition])
end

4章まとめ

- スコープゲートと、Rubyはどのようにスコープを管理しているのか。
- フラットスコープと共有スコープを追加、スコープを横断して束縛を見えるようにする方法。
- オブジェクトのスコープで(instance_evalやinstance_execを使って)コードを実行する方法や、クリーンルームでコードを実行する方法。
- ブロックとオブジェクト(Proc)を相互に変換する方法。
- メソッドとオブジェクト(MethodやUnboundMethod)を相互に変換する方法。
- 呼び出し可能オブジェクト(ブロック、Proc、Lambda)の種類とその違い。通常のメソッドとの違い。
- 独自の小さなDSLの書き方。
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0