LoginSignup
6
3

More than 5 years have passed since last update.

Ruby:ブロックを用いて、スコープを思い通りに飛び越える。

Last updated at Posted at 2018-07-07

ブロックとスコープ

rubyにおいて、スコープを思い通りに飛び越えて変数を共有、変更するために、
ブロックは引数+αの要素として、メソッドに渡すイメージ。無名引数のようなもの。yieldで渡されたブロックを呼び出すことができる。

通常のスコープ

sample.rb
def mymethod
  x = "Goodbye"
  yield("cruel")
end
x = "Hello"
my_method {|y| "#{x},#{y} world" } # => "Hello, cruel world"

def just_yield
  yield
end 

ブロックを定義する時、その時点でその場にある束縛を所得する。
メソッド内にも別でxという変数は定義されているので、この場合、goobyeの方でxが書き換えられてしまいそうだが、実際にはブロックが定義された時のxを見ている。メソッドにある束縛はブロックからは見えない。

sample.rb
top_level_variable = 1

just_yield do
  top_variable += 1
  local_to_block = 1 #新しい束縛
end

top_level_variable # => 2
local_to_block # => Error!

このようにblockの中で変数を定義することは可能ではあるが、ブロックの外に出ることはできない。

sample.rb
v1 = 1
class MyClass
  v2 = 2
  local_variables # => [:v2]
  def my_method
    v3 = 3
    local_variables
  end
  local_variables
end

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

また、rubyでは新しいスコープに入る度に以前の束縛は新しい束縛に置き換えられる。よって、MyClass内に入ると、v1はスコープを外れて見えなくなる。
ただし、インスタンス変数はそのインスタンスが保持するものであるため、メソッドから、、同じオブジェクトのメソッドを呼び出したとしても、変わることはない。

スコープゲート

スコープが新しくスコープをオープンする場所は3つある。
・クラス定義
・モジュール定義
・メソッド
プログラムはスコープをclass,defなどのキーワードによって、判別するため、これらのスコープゲートを超えてローカル変数を渡したい場合には、単にclassやdefなどを使わない定義の仕方をすれば良い。

sample.rb
my_var = "成功"
MyClass = Class.new do

  puts "クラス定義の中は#{my_var}"

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

このように記述することで、スコープゲートを飛び越えることができる。

instance_eval

sample.rb
class MyClass
  def initialize
    @v = 1
  end
end

obj = MyClass.new

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

インスタンスに渡されたブロックはレシーバをselfにしてから評価されるため、レシーバのprivateメソッドや、@vのようなインスタンス変数にもアクセスすることができる。
また

sample.rb
v = 2
obj.instance_eval{ @v = v }
obj.instance_eval{ @v } # => 2

このようにインスタンス変数をローカル変数で書き換えることもできる。

instance_exec

instance_evalのもう少し柔軟になったバージョンにinstance_execがある。

sample.rb
class C
  def initialize
    @x = 1
  end
end

class D
  def twisted_method
    @y = 2
    C.new.instance_eval{ "@x: #{@x}, @y: #{@y}" }
  end
end 

D.new.twisted_method #=> "@x: 1, @y: "

instance_evalはselfをレシーバにするため、呼び出し側のインスタンス変数はスコープから溢れてしまう。
これを防ぐためにinstance_execで@yの値をブロックに渡せば良い。

sample.rb
class D
  def twisted_method
    @y = 2
    C.new.instance_eval(@y){ |y| "@x: #{@x}, @y: #{y}" }
  end
end 

Procオブジェクト

Procはブロックをオブジェクトにしたもの。

sample.rb
inc = Proc.new {|x| x + 1}
inc.call(2) # => 3

このように、ブロックをオブジェクトとして、保存して、後から評価することができる。

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

メソッドに特別な引数を用意して、ブロックに束縛を割り当てることもできる。

sample.rb
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

メモ

ensure

エラーが起きようがおきまいがどっちみち実行する。

6
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3