気軽に楽しくプログラムと遊ぶ

自分が興味があってためになるかもって思う情報を提供しています。

メタプログラミング Ruby 第3章 ブロック

関数型言語よりの概念であるブロックについて、理解を深めて行く。

ブロックの基本をおさらい

ブロックをメソッド内部で活用する

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

次はクラス定義 メタプログラミング Ruby 第4章 クラス定義 - 気軽に楽しくプログラムと遊ぶ