メタプログラミングRuby第2版:4章:ブロック

スポンサーリンク

メタプログラミングRubyとは?

出版社である O’Reilly Japanのサイトからの引用です。

本書はRubyを使ったメタプログラミングについて解説する書籍です。メタプログラミングとは、プログラミングコードを記述するコードを記述することを意味します。前半では、メタプログラミングの背景にある基本的な考えを紹介しながら、動的ディスパッチ、ゴーストメソッド、フラットスコープといったさまざまな「魔術」を紹介します。後半では、ケーススタディとしてRailsを使ったメタプログラミングの実例を紹介します。今回の改訂では、Ruby 2とRails 4に対応し、ほぼすべての内容を刷新。Rubyを使ったメタプログラミングの魔術をマスターし、自由自在にプログラミングをしたい開発者必携の一冊です。

引用:https://www.oreilly.co.jp/books/9784873117430/

4章:ブロックを写経し終えたので内容を振り返ってまとめてみました。

ブロック

スコープを制御する強力なツール

  • クロージャとして変数のスコープを超える
  • instance_evalにブロックを渡してスコープを操作する
  • Procやlambdaなどの呼び出し可能オブジェクトに変換して呼び出す

ブロックを定義できるのはメソッド呼び出し時のみ。
ブロックはメソッドに渡され、メソッドはyieldキーワードでブロックをコールバックする。

def a_method(a, b)
  a + yield(a, b)
end

a_method(1, 2) { |x, y| (x + y) * 3 } # => 10

ブロックはクロージャ

スコープ

ブロックを作成すると同階層のローカル変数を束縛してブロック内部に持ち込める

def my_method
  x = 'Goodbye'
  yield('cruel')
end

x = 'Hello'
my_method { |y| "#{x}, #{y} world"} # => Hello, cruel world
# => Hello, cruel worldとはならない

ブロックの中で新しい束縛を定義できるが、ブロックが終了した時点で消える。
この束縛のことをスコープと呼ぶ。

def just_yield
  yield
end

top_level_variable = 1

just_yield do
  top_level_variable += 1
  local_to_block = 1
end

top_level_variable # => 2
local_to_block # =>  undefined local variable or method `local_to_block' for main:Object (NameError)

スコープゲート

スコープが切り替えられる場所(スコープゲート)は3つある

  • クラス定義
  • モジュール定義
  • メソッド
v1 = 1

class MyClass # => スコープゲート:class入り口
  v2 = 2
  local_variables # => [:v2]

  def my_method # => スコープゲート:def入り口
    v3 = 3
    local_variables # => [:v3]
  end # => スコープゲート:def出口
  local_variables # => [:v2]
end # => スコープゲート:class出口

obj = MyClass.new
obj.my_method
obj.my_method
local_variables # => [:v1, obj]

フラットスコープ

フラットスコープとは、スコープゲートを超えて束縛を渡す手法

  • classのスコープゲート:Class.newのブロックによるメソッド呼び出しで超える
  • defのスコープゲート:Module#define_methodによる動的メソッド呼び出しで超える
my_var = '成功'

MyClass = Class.new do
  puts "クラス定義の中は#{my_var}"

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

puts MyClass.new.my_method
# => クラス定義の中は成功
# メソッド定義の中も成功

instance_eval

instance_eval

  • instance_evalに渡したブロックは、レシーバをselfにして評価される
  • そのため、レシーバのprivateメソッドやインスタンス変数にもアクセスが可能
  • ブロックなのでクロージャとしても機能する
class MyClass
  def initialize
    @v = 1
  end
end

obj = MyClass.new

obj.instance_eval do
  self # => <MyClass:0x00007fb0d9246468 @v=1>
  @v # => 1
end

v = 2
obj.instance_eval do
  @v = v
  @v # => 2
end

instance_exec

レシーバがselfになることで呼び出し側のインスタンス変数がスコープから抜け落ちてしまう。
これを防ぐためにはinstance_exec(*args)で@yの値をブロックに渡す。

class C
  def initialize
    @x = 1
    @y = 'Cクラスだよ'
  end
end

class D
  def twisted_method_eval
    @y = 'Dクラスだよ'
    C.new.instance_eval do
      "@x: #{@x}, @y: #{@y}"
    end
  end

  def twisted_method_exec
    @y = 'Dクラスだよ'
    C.new.instance_exec(@y) do |y|
      "@x: #{@x}, @y: #{y}"
    end
  end
end

D.new.twisted_method_eval # => @x: 1, @y: Cクラスだよ
D.new.twisted_method_exec # => @x: 1, @y: Dクラスだよ

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

ブロック以外にもProcオブジェクトは「コードを保管して後で呼び出す」ことができる

Procオブジェクト

  • ブロックはオブジェクトではない
  • しかし、一度コードを保管して後で呼び出すためにはオブジェクトであることが必要
  • Procはブロックをオブジェクトにしたもの
  • Proc.new, proc, lambda, アロー関数で作れる
  • 呼び出す際にはProc#callで呼び出す。これを遅延評価と呼ぶ
# Procの作成方法
# ①:Proc.new
inc = Proc.new { |x| x + 1 }
inc.call(2) # => 3
inc.class # => Proc

# ②:proc
dec = proc { |x| x - 1 }
dec.call(2) # => 1
dec.class # => Proc

# ③:lambda
dec = lambda { |x| x - 1 }
dec.call(2) # => 1
dec.class # => Proc

# ④:アロー関数
dec = ->(x) { x - 1 }
dec.call(2) # => 1
dec.class # => Proc

&修飾

  • 他のメソッドにブロックを渡す
  • ブロックをProcに変換する
  • Procをブロックに戻す

ときに使う。

# 他のメソッドにブロックを渡したい
def math(a, b)
  yield(a, b)
end

def do_math(a, b, &operation)
  math(a, b, &operation)
end

do_math(2, 3) { |x, y| x * y } # => 6

# ブロックをProcに変換したい
def my_method(&the_proc)
  the_proc
end

p = my_method { |name| "Hello, #{name}!" }
p.class # => Proc
p.call('Bill') # => Hello, Bill!

# Procをブロックに変換したい
def my_method(greeting)
  "#{greeting}, #{yield}!"
end

my_proc = proc { 'Bill' }
my_method('Hello', &my_proc) # => Hello, Bill!

Proc 対 lambda

  • returnキーワードの意味が違う
    • lambda:単純にlambdaから戻る
    • Proc:Procが定義されたスコープから戻る
  • 引数の数の解釈が違う
    • lambda:引数の数が合わないとAugumentError
    • Proc:引数の数が多いと切り落とし、少ないとnilになる

以上のことから、単に終了してくれ、メソッドに似て直感的であるlambdaが使われやすい

# 単純にlambdaから戻る
def double(callable_object)
  callable_object.call * 2
end

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

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


p = proc { |a, b| [a, b] }
p.call(1, 2, 3) # => [1, 2]
p.call(1, 2) # => [1, 2]
p.call(1) # => [1, nil]

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

スポンサーリンク

  
Scroll Up