一覧へ戻る

更新:

カテゴリ: 技術
タグ:
  • Ruby
  • 備忘録

メタプログラミングRuby 4章 〜ブロック〜

『メタプログラミングRuby』4章のブロックについてまとめた備忘録。スコープゲートやフラットスコープ、instance_eval、Procとlambdaの違いなどをコード例とともに分かりやすく解説します。

こんにちは。いとじゅんと申しまする。

やっとメタプログラミングRubyを再開したので、まとめてみました。

ブロックさん

ブロックさんは波括弧または do...endキーワードで定義できる。ブロックはメソッド呼び出し時にだけ定義できる。

code
# これはできない
do
  "hoge"
end
 
# これはいける
hoge_method do
  "hoge"
end

メソッドに渡されたブロックは、メソッド内部でyieldキーワードによって、任意のタイミングで実行できる。yieldの意味がやっと理解できた気がする。

code
def sunny
  puts "今から晴れるよ"
  yield("快晴")
end
 
sunny do |word|
  puts "今日の天気は...#{word}!"
end
# 今から晴れるよ
# 今日の天気は...快晴!

ブロックを定義した時、参照した変数などの束縛を持っていくため、クロージャと言える。クロージャってなんですか。

自分で作られたスコープの環境を記憶して持ち運べる処理の塊 by Gemini

ほへ〜。意識せずに使っていた。

スコープ

Rubyのスコープは、下位のスコープから上位のスコープの変数などに参照することができない。トップレベルなどに定義した変数は、下位に定義されたクラスやメソッドなどで参照できない。

code
look_me = "loooooook me!!!"
 
def fuga
  p look_me
end
 
fuga # raise NameError

スコープゲート

def, class, moduleキーワードはスコープを切り替えて、新しいスコープを生成する。これらのキーワードは先ほど書いたスコープの性質によって、上位スコープへのアクセスが不可となる。

スコープゲートから逃れるためには、スコープゲートのキーワードを使わずに、メソッドやクラス、モジュールを定義すれば良い。

code
look_me = "loooooook me!!!"
 
define_method :fuga do
  p look_me
end
 
fuga # => "loooooook me!!!"

クラスならClass.new, モジュールならModule.newで行ける。このような、スコープを均して上位スコープに参照する方法をフラットスコープと呼ぶ。

#instance_eval

BasicObject#instance_evalに渡したブロック内では、レシーバをselfにして評価される。よって、そのオブジェクトの束縛にアクセスできるようになる。これ何に使うんだろうと思ったら、デバッグとかテストで使うことがあるそう。なるほどね。ここで渡されたブロックのことをコンテキスト探査機と呼ぶ。強そう。

code
class Hoge
  def initialize
    @fuga = "piyo"
  end
end
 
Hoge.new.instance_eval do
  self 	# => #<Hoge:0x130e9d98 @fuga="piyo">
  @fuga # => "piyo"
end

また、オブジェクトの束縛へアクセスできるため、カプセル化が破壊される。ひえ〜。

code
class Hoge
  private
  def private_method
    "いやーん、プライベートよー"
  end
end
 
Hoge.new.instance_eval do
  private_method # => "いやーん、プライベートよー"
end

クリーンルーム

よくわからなかった。上位スコープに定義された束縛から隔離するために使う?また今度聞く。

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

Rubyにはブロック以外にコードを保管して後から呼び出せる場所が少なくとも以下の3つがある。

  • Procオブジェクト
  • lambda(Procのオブジェクトではあるが、微妙に違う)
  • メソッド

ちなみに、ただのブロックはオブジェクトではない。

Procオブジェクト

先ほど言った通り、ただのブロックはオブジェクトではないが、これだとコードを保管して後から呼び出す時に困る。そこで現れた救世主こそ、Procだ!!!!!これはブロックをオブジェクトにしたもの。

以下のように定義して実行できる。Proc.newで定義、Proc#callで呼び出すことができる。

code
yaoyoro = Proc.new {|user|
  "#{user}のみんなー!ヤオヨロー!"
  }
 
yaoyoro.call("神々") # => "神々のみんなー!ヤオヨロー!"
yaoyoro.class	# => Proc

lambdaはProcのオブジェクトだが、いろいろ違う。まずは以下のように定義できる。

code
kaguya = lambda { "かぐやっほー!" }
 
kaguya.call() # => "かぐやっほー!"
kaguya.class	# => Proc

lambdaと他のProcの大きな違いは2つある。

1つ目にreturnの挙動が異なる。lambda内のreturnは単にlambdaから返るだけだが、Proc内のreturnはProcが定義されたスコープから返る。みた方が早いと思う。

code
# 通常のProcの場合
def kaguya_hello
  kaguya = Proc.new { return "かぐやっほー!" } # ここのreturnでもう返っちゃう
  result = kaguya.call
  return result * 2 # ここまで来ない!
end
 
# lambdaの場合
def yachiyo_hello
  yachiyo = lambda { return "ヤオヨロー!" }
  result = yachiyo.call
  return result * 2 # ここで返される
end
 
kaguya_hello # => "かぐやっほー!"
yachiyo_hello # => "ヤオヨロー!ヤオヨロー!"

2つ目の違いは引数のチェック方法。Procは引数の数が多くても少なくても、 ArgmentErrorを返さない。柔軟すぎるかもね。それに対して、 lambdaは引数の数に厳格。

code
p = Proc.new { |a, b| [a, b] }
p.call(1) # => [1, nil]
 
p = lambda { |a, b| [a, b] }
p.call(1) # => raise ArgumentError

lambdaの方が通常のメソッドに近いと覚えておいたら良い。

&修飾

yieldを使うことでメソッド内でブロックに与えた処理を実行できるが、 yieldじゃ足りないケースがある。例えば、他のメソッドにブロックを渡したいときやブロックをProcに変換したいときなど。

&修飾を使うことでメソッドに与えたブロックを束縛し、引数として受け取ることができる。逆にProcに対して &修飾を付加してメソッドに与えると、ブロックに戻すことができる。

code
def hello(word, num)
  yield(word, num)
end
 
def do_hello(word, num, &block) # 引数としてブロックをProcに変換して受け取っている
  block.class # => Proc
  hello(word, num, &block) # Procに対して&を使ってブロックに戻して#helloを呼び出している
end
 
do_hello("かぐやっほー!", 3) {|word, num| word * num } # => かぐやっほー!かぐやっほー!かぐやっほー!

Methodオブジェクト

以下のコードのようにオブジェクトに対して、 #methodを使うことでMethodオブジェクトとして取得できる。これは Method#callを使うことで実行できる。

code
class CPK
  def initialize(value)
    @x = value
  end
 
  def hello
    @x
  end
end
 
object = CPK.new("かぐやっほー")
method = object.method :hello # => #<Method: CPK#hello() eval:6>
method.call # => "かぐやっほー"

Procなどとの違う点として評価されるスコープが異なる点がある。Procは定義された時のスコープで評価されるが、Methodは所属するオブジェクトのスコープで評価される。

UnboundMethod

クラスやモジュールから引き離したメソッドは UnboundMethodクラスのオブジェクトになる。呼び出し不可だが、 #bindを使うことでオブジェクトへ束縛でき、呼び出せるようになる。ただし、メソッドの元々の束縛先がクラスの場合は元のクラスまたはそのサブクラスにしか束縛できない。モジュールの場合は特に制限はない。

code
class CPK
  def initialize(value)
    @x = value
  end
 
  def hello
    @x
  end
end
 
object = CPK.instance_method(:hello)
object.class # => UnboundMethod

ActiveSupportなどで使われているらしいが、正直どこで使うべきか理解していないのでこれは宿題です。

おわりに

ここまでやった私は以下のリポジトリの演習問題をやることで、理解したような気がしています。私と同じようにメタプログラミングを学んでいる同志におすすめです。

この章を通して、Rubyのスコープやブロックの取り扱い方を学べました。スコープゲート周りとかはメタプログラミング関係なく覚えておくと業務に役立ちそうですね。

ではまたー!