メタプログラミング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に乗っけるのは違くない?と思ってブログに起こしました。