関数型言語よりの概念であるブロックについて、理解を深めて行く。
ブロックの基本をおさらい
ブロックをメソッド内部で活用する
yieldでブロック処理を内部で呼び出す。
def my_method(who, greeting, count) who + yield(greeting, count) end my_method("Bob ", "hello! ", 2) { |greet, count| greet * count} # "Bob" + yield("hello!", 2) => "Bob hello! hello! "
ブロックが存在しない場合、エラーとなる。
def my_method(who, greeting, count) who + yield(greeting, count) end my_method("Bob ", "hello! ", 2) LocalJumpError: no block given (yield
block_given?を活用する。
def my_method(who, greeting, count) who + yield(greeting, count) if block_given? who end my_method("Bob ", "hello! ", 2) # ブロックがない場合はblock_given?がfalseとなり、表示されない。次の行のwhoのみ表示 => "Bob "
ruby版のusing(C#の関数)を作る
正常終了、例外発生時に関わらず、リソースをクローズしてくれる関数
usingを作成する。
module Kernel def using(resource) begin yield ensure resource.dispose end end end using(resource) { puts "正常です"}
上記、ソースについて
・usingは関数。どのクラスからも使えるようにKernelへ定義
・正常時はブロック内処理実行。
・正常・異常に関わらず、ensureでdispose(リソースクローズ処理)を実行
クロージャー
ブロックはローカル変数(以下ソース内のx)を束縛する
束縛とはブロック内でのxの書き換えがローカル変数に影響するということ。
変数を束縛(閉包)するものということでクロージャーというらしい。なるほど。
x = "Hello" my_method { "#{x}, world" } #=> "Hello, world"
ブロックローカル変数
ブロック内で新たな変数を定義可能。ブロック内でしか参照できない。
def my_method yield end var = "a" my_method do var = "b" block_var = "c" end var #=> "b" block_var # NameError: undefined local variable or method `block_var' for main:Object
スコープ
ローカル変数は同スコープ内しか参照できない。
v1 = 1 class MyClass v2 = 2 # v1はトップレベルスコープのため、参照不可 local_variables # =>[:v2] def my_method v3 = 3 local_variables # => [:v3] end # v3はメソッド内スコープのため、参照不可 local_variables # =>[:v2] end c = MyClass.new() c.my_method # => [:v3] # トップレベルのローカル変数 local_variables # => [:v1,obj]
スコープゲート
スコープの入り口、出口
境界線は3つある。
- class
- module
- def
このキーワードが出て来た際にスコープが変わる。
スコープのフラット化
class,module,defのスコープを超えて、変数を共有する方法がある。
ブロック(クロージャー)を使う。その為にメソッドでそれぞれを定義する。
v1 = 1 MyModule = Module.new do define_method :my_modu_method do puts "#{v1}をモジュール定義内から参照" end end MyClass = Class.new do puts "#{v1} をクラス定義内から参照" include MyModule define_method :my_cla_method do puts "#{v1} をメソッド定義内から参照" end end MyClass.new.my_modu_method MyClass.new.my_cla_method 1をモジュール定義内から参照 1をクラス定義内から参照 1をメソッド定義内から参照
スコープの共有
スコープゲート内でフラットスコープでメソッド間の変数を共有する。
スコープ外からは見えなくなる。
class MyClass v1 = 1 define_method :my_method1 do puts "#{v1} をメソッド1から参照" end define_method :my_method2 do puts "#{v1} をメソッド2から参照" end end c = MyClass.new c.my_method1 c.my_method2 puts "#{v1}" 1をメソッド1から参照 1をメソッド2から参照 NameError: undefined local variable or method `v1' for main:Object
instance_eval()
オブジェクトの中にいるがごとく振る舞えるブロックを提供するメソッド
なんて自由でパワフル。めちゃ便利。果たして使いこなせるかどうか。
class MyClass v1 = 1 private def my_method puts "マイメソッド" end end c = MyClass.new # ブロック内部処理を実行 c.instance_eval do # 内部を書き換える v1 = 2 # obj指定なしで内部コンテキストでメソッド呼び出し可能 puts v1 # => 2 my_method # => マイメソッド end
呼び出し可能オブジェクト
Procオブジェクト
ブロック処理を保管できるオブジェクト。
この技術を遅延評価と呼ぶ。
一般的な評価はコンパイル時だが、実行時に遅れて評価される。
Procの生成
inc = Proc.new {|x| x + 1} inc.call(5) # =>6
Procの生成(カーネルメソッド)
inc = lambda {|x| x + 1} inc.class inc.call(6) # =>7 inc = proc {|x| x + 1} inc.class inc.call(7) # =>8
Procとブロックの変換
ブロックをProcクラスへ変換。
def calc(a, b) yield(a, b) end def my_method(x, y, &block) # blockがProcオブジェクト puts block.class # =>Proc # &をつけるとブロックを格納する変数となる puts calc(x, y, &block) end my_method(1, 2) {|x, y| x + y } # => 3
Procクラスをブロックへ変換
def my_method(a, b) puts "#{a} #{b} #{yield}" end #Proc クラスを作成し、ブロックとして評価させる my_block = proc { "Bob" } my_method( "Good", "morning", &my_block) #=> Good morning Bob
メソッドを呼び出し可能オブジェクトとして受け取る
class MyClass def my_method puts "call method" end end c = MyClass.new() # オブジェクトスコープ内で実行可能のため、クラスオブジェクトを生成 m = c.method :my_method m.call # =>call method
ドメイン特化言語(DSL)
あるイベントが発生した場合にアラートを表示するDSL
redFlag.rb
#以下のDir.globの行でloadされるfile内で呼び出されるeventメソッド def event(name) puts "ALERT #{name}" if yield end # 複数のeventを呼び出すためのファイルを読み込み Dir.glob("*events.rb").each{|file| load file}
下記で実際にイベントが実行される
test_events1.rb
# redFlag.rbのload fileで実行され、redFlag.rb内のevent内処理が実行される event "イベント発生" do true end # ブロック内部がfalseの場合、if yieldでfalseとなりアラートは表示なし event "イベント発生しない" do false end
イベント間でデータ保持するテストイベント
test_events2.rb
# name = "イベント1"、do〜endでブロック定義(yieldで処理する) event "イベント1" do @v1 < 3 end event "イベント2" do @v1 < @v2 end setup do puts "v1を設定" @v1 = 1 end setup do puts "v2を設定" @v2 = 2 end
上記に対応したDSLに作り替える
# 一旦、eventとsetupをインスタンス変数に貯めて一気に実行 def event(name, &block) #ハッシュにnameをkeyとしてProcオブジェクト(ブロック処理)を格納 @events[name] = block end def setup(&block) # eventに存在するnameがないので配列に追記代入して行く @setup << block end Dir.glob("*events.rb").each do |file| @setups = [] @events = {} # event、setupをハッシュ、配列に読み込む load file @events.each_pair do |name, &block| # まっさらなObjectでsetup、eventのインスタンス変数を他のイベントと分離 env = Object.new # クリーンルームを生成 @setups.each |setup| # env内にsetupのブロック内処理(インスタンス変数の設定)を実行 env.instance_eval &setup end # Object内でevent内のブロック内処理を実行し、trueならアラート表示 puts "ALERT #{name}" if env.instance_eval &block end end