メタプログラミングRuby 四章 ブロックの読書録

はじめに

前職の出勤が16日に終わり、しばしニートしております。 最近discordbというボイスチャットアプリのbot作成のラッパーGemが面白くていじっているんですが、このGemを読んでいるとメタプログラミングRubyの4章にこんなのあったなぁという気持ちが強まり、読書録として少し理解を文章に起こしてみました。5章と6章も個人的に何度でも反芻したいので書いていきたい。

ブロックは強力なツール

この章ではブロックがスコープを操る強力なツールだということを解説してくれています。 あと途中で上司が逃げます

ブロックは束縛を包んでくれる

Rubyではdef、class、module (これらはスコープを開く門)でスコープが切り替わってローカル変数なんかは共有できなくなります。

 class Hogeclass
  class_value = 1
  def hoge_method
    p class_value # => Error!
  end
end
 

裏を返すとこの門を開かなければ新しいスコープは開かれません。 class やmoduleであればClass.new {}やModule.new {}としたり、 defをdefine_method { }にしてブロックを渡すことで新しいスコープが開かれなくなります。

 class Hogeclass
  class_value = 1
  define_method :hoge_method do
    p class_value # => 1
  end
end
 

このようにブロックは現在の束縛を包み込んでnewやdefine_methodといったメソッドに渡すことができます。

instance_eval

instance_evalはBasicObjectのインスタンスメソッドでブロックを渡すとレシーバーをselfにして評価してくれます。

 class Hogeclass
  def initialize
    @foo = "value"
  end

  private
  def private_method
    p "秘密"
  end
end

hoge = Hogeclass.new

hoge.instance_eval do
  @foo # => "value"
end

hoge.instance_eval do
  private_method # => "秘密"
end
 

instance_evalを使えば例えばテストを書く際にクラスのインスタンス変数をいじったり、クリーンなオブジェクト(BasicObjectのインスタンス等)を作ってブロックを評価することができます。

呼び出し可能オブジェクト

ブロックの「コードを保管して、あとから実行する」という方式はなにもブロックだけのものではありません。

Procオブジェクト

ブロックはオブジェクトではないので後で呼び出したりしたい時に不便です。 ブロックをオブジェクトにするものとしてrubyにはProcクラスがあります。

 double = Proc.new { |x| x * 2 }

double.call(4) # => 8
 

わかりやすい変数にインスタンスをいれておけるので呼び出しも楽ですね。

ブロックをProcに変換するのに便利なメソッドもあります。lambdaとprocです。

 double = lambda { |x| x * 2 }
# => <Proc:0x00007f9e5~~~
triple = proc { |x| x * 3 }
# => <Proc:0x00007f9e5~~
 

どちらもProcのインスタンスがつくられてるのがわかりますね。 Proc , proc とlamdaの違いとしてreturnの意味合いが違うことと、引数のチェックが違うことが挙げられます。 returnの意味合いが違うというのは以下のコードで確認できます。 (メタプログラミングRubyではProc.newとlamdaの違いしかなかったので今回はprocで試してみました)

 def lambda_ten_double
  lambda_proc = lambda { return 10 }
  result = lambda_proc.call
  return result * 2
end

def proc_ten_double
  proc_proc = proc { return 10 }
  result = proc_proc.call #ここでProcが定義されたスコープから戻ってしまう
  return result * 2 #ここまでたどり着かない
end

lambda_ten_double # => 20
proc_ten_double   # => 10
 

引数のチェック方法にも違いがあり、

 p = Proc.new { |a,b| [a,b] }
l = lambda { |a,b| [a,b] }
p.call(1, 2, 3) # => [1, 2]
l.call(1, 2, 3) #=> ArgumentError (wrong number of arguments~
 

項数にも厳しくreturnの挙動も単なる終了なので特別な理由がなければlambdaを用いたほうが良さそうです。

&修飾でもブロックをProcに変換することができます。 また&修飾をつかえばProcオブジェクトをブロックに戻すこともできます。 どういうことかというと、

 def block_method
  yield
end

def do_block_method(&block) #ここの&でブロックをProcオブジェクトに
  p block.class # => Proc &つけないとProcのまま!
  block_method(&block) # ここの&でProcオブジェクトからブロックに変換、 ないとオブジェクト渡してしまうのでArgumentエラー
end

do_block_method { p "hoge" } # => "hoge"
 

{ p "hoge" } が一度Procオブジェクトになり、またブロックにもどって最終的にblock_methodに渡されていることなります。

Methodオブジェクト

メソッドも呼び出し可能なオブジェクトです。 具体的には

 class Hogeclass
  def initialize
    @x = "hoge"
  end

  def hogemethod
    @x
  end
end

hogeobject = Hogeclass.new
method = hogeobject.method :hogemethod
method.call # => "hoge"
 

メソッドオブジェクトは呼び出した時にオブジェクトのスコープで評価される。(今回はHogeclassクラス)
この束縛は解除して別のオブジェクトに束縛したりもできる。(ただし同じクラスかそのサブクラスのオブジェクトに限る)

 class Fugaclass < Hogeclass
  def initialize
    @x = "fuga"
  end
end

unboundmethod = method.unbind
fugaobject = Fugaclass.new
rebindmethod = unboundmethod.bind(fugaobject)
rebindmethod.call # => "fuga"
 

四章のクイズ

自分は4章最後のクイズでは、サンプルを見る前に以下のようなコードを書いていました。

 def setup(&block)
  @setups << block
end

def event(description, &block)
  @setups.map(&:call)
  puts "ALERT: #{description}" if block.call
end

@setups = []
load 'events.rb'
 

eventごとに実行するならevent内でsetupに渡されたProcよびだしても良いのでは?と思って書きました。少し一つのメソッドでやりすぎかも。
この処理だけ行いたいならeventメソッドの 引数&blockは削除して、後置ifもif yieldにしても動作すると思いますが、ブロックを渡す必要があるメソッドでそんなことをしたらわかりにくすぎますね。
改めて見ると逃げた上司ののグローバル変数をどうして削除しない?が刺さるコードです。
グローバル変数を削除して、

 lambda {
  setups = []

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

  Kernel.send :define_method, :event do |description, &block|
    setups.map(&:call)
    puts "ALERT: #{description}" if block.call 
}.call
load 'events.rb'
 

という形にしてみました。

おわりに

一度読んだ本をいざ文章にまとめてみるといろいろ考えることが多くていいですね。ガシガシ続けて行きたひ。 色んな人からツッコミもらいたいのでQiitaに書くか迷いましたが、このブログの主目的が「本の理解を文におこして再確認する」と考えていますし、先人の知見をQiitaに乗っけるのは違くない?と思ってブログに起こしました。