ブロック

C#にはusingと呼ばれる構文が存在し、引数で受けたオブジェクトに対して処理を行ったあと、またはその処理の中で例外が発生したあと、必ずdisposeというメソッドを呼び出し、引数で受けたオブジェクトの廃棄を行う。

これをRuby風に書くと以下のようになる。

module Kernel
  def with(resouce)
    begin
      yield if block_given?
    ensure
      resouce.dispose
    end
  end
end

class Running
  def raise_error
    raise "Opps!!"
  end

  def pause(sec)
    puts "Now I am having rest."
    sleep(sec)
  end

  def dispose
    puts "Bye!!"
  end
end

runner = Running.new

with(runner){
  runner.pause(3)
  # runner.raise_error
}

withメソッドに対して、Runnerオブジェクトと、それに対して行いたい操作をブロックで渡している。 コメントアウトしたメソッドはRunnerオブジェクトに対して例外を発生させるメソッドであるが、これをコメントアウトしてもしなくてもdisposeメソッドが実行される。

irb(main):004:0> require "./using.rb"
Now I am having rest.
Bye!!
Traceback (most recent call last):
        8: from /home/pyons/.rbenv/versions/2.5.0/bin/irb:11:in `<main>'
        7: from (irb):4
        6: from /home/pyons/.rbenv/versions/2.5.0/lib/ruby/2.5.0/rubygems/core_ext/kernel_require.rb:59:in `require'
        5: from /home/pyons/.rbenv/versions/2.5.0/lib/ruby/2.5.0/rubygems/core_ext/kernel_require.rb:59:in `require'
        4: from /mnt/c/Users/broad/OneDrive/products/METAprogramming/Wednesday/using.rb:28:in `<top (required)>'
        3: from /mnt/c/Users/broad/OneDrive/products/METAprogramming/Wednesday/using.rb:4:in `with'
        2: from /mnt/c/Users/broad/OneDrive/products/METAprogramming/Wednesday/using.rb:30:in `block in <top (required)>'    
        1: from /mnt/c/Users/broad/OneDrive/products/METAprogramming/Wednesday/using.rb:13:in `raise_error'
RuntimeError (Opps!!)

例外発生をコメントアウトしても全く同様の動きになる。

irb(main):005:0> require "./using.rb"
Now I am having rest.
Bye!!
=> true

ここまではブロックの使い方のおさらい。

ブロックに関しては既にSilverの勉強で扱っていて、その時「拘束」という概念も一応理解はしているつもりだった。

メソッドを呼び出すときに、ブロック内で初期化された変数はメソッド内での処理が終了すると消滅し、参照することは出来ない。自身が定義された(呼び出されたメソッドの内部という)環境でしか解決できないような「処理生成時の環境を拘束する」ことから「束縛」と呼ばれ、これを可能にするものを「クロージャ―」と呼ぶ。

Ruby Silver 試験対策 Day3 (ブロックとProcと例外編) - Pyons Tech Blog

けどこれは、あまり正しい理解ではない気がする。拘束しているのは「ブロックの内部」というよりも、ブロックの前までで定義されていたローカル変数を束縛して、処理先に連れて行っている、と考えるほうが自然なようだ。

個人的には「ローカル変数のスナップショットを処理へ連れていく」というイメージを持っているが、検索した限り同じような説明をしている人はいなかった。

def, module, classといった変数のスコープが変化するようなコード内での区切りを「スコープゲート」と呼ぶ。スコープゲートを超えて変数を渡すにはどうすればよいか。

スコープゲートを用いづにクラス定義、メソッド定義を行えばよい、というのが答えになる。

my_var = "スコープを超えたよ。"

MyClass = Class.new do
  puts "Inside Class definition: #{my_var}"

  define_method  :my_method do
    puts "Inside Method definition: #{my_var}"
  end
end

m = MyClass.new
p '&&&&&&&&&&'
m.my_method

特殊なクラス定義、メソッド定義を用いて、各定義をブロックの内部で行った。これにより、ブロック以前の変数を束縛して内部に連れていくことが可能になった。

pyons@LAPTOP-SF87NLCB:/mnt/c/Users/broad/OneDrive/products/METAprogramming/Wednesday$ ruby flat_scope.rb 
Inside Class definition: スコープを超えたよ。
"&&&&&&&&&&"
Inside Method definition: スコープを超えたよ。

この手法を「フラットスコープ」と呼ぶ。

フラットスコープを用いるとこのようなことが可能になる。

def hello
  shared = 0
  
  define_method :counter do
    shared
  end

  # def counter 
  #   shared
  # end

  define_method :incriment do |x|
    shared += x
  end

  # def incriment(x)
  #   shared += x
  # end
end

hello

p counter
incriment(4)
p counter

フラットスコープを使って定義したメソッド同士では変数が共有されている。define_methodは単に``def increments(x)```に書き直すだけではなく、定義したメソッドそのものも特殊なスコープを持つようになるのだ。

pyons@LAPTOP-SF87NLCB:/mnt/c/Users/broad/OneDrive/products/METAprogramming/Wednesday$ ruby shared_scope_copy.rb
0
4

この他にブロックを用いた例としてinstance_evalがある。instance_evalはブロックを使って、クラスやメソッドの中のローカル変数あプライベート変数にアクセスすることが出来る。ブロックの中で、指定したクラスやメソッドのレシーバーをselfにできるため、あたかもクラスやメソッドの内部のコンテキストにいるように感じられる。

class MyClass
  variable = "This cannnot be accessed from out side."
  def initialize
    @v = 1
  end
  p variable
  p self
  # p self.variable # undefined error
end

obj = MyClass.new

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

MyClass.instance_eval do
  p self # MyClass
  p @v #nil
  # p variable # undefined error (クラス内でselfのレシーバーを付けても同様)
end

obj2 = MyClass.new 

obj2.instance_eval do
  @v = "Hello"
  p @v
end

ここで気づいたのは「クラス内で宣言したローカル変数はprivateメソッドのように振る舞う」ということである。instance_evalを用いずにクラス内でレシーバーを付けて同じクラス内のローカル変数にアクセスしようとしてもundefinedになってしまう。

Rubyのprivateメソッドはかなり特殊で以下のようなこともできない。

class MyClass
  variable = "This cannnot be accessed from out side."
  def initialize
    @v = 1
  end
  p variable
  p self
  p hello 

  private
 
  def hello
    p "hello"
  end
end

プライベートメソッドであるhelloにクラス内からアクセスすることが出来ない。このプライベートメソッドにアクセスしたければ、以下のようにクラス内のメソッドから呼び出す必要がある。

def call_hello
    hello
  end

クラス内でprivateメソッドを呼び出しているとき、そのselfは「クラス」そのものである。

一方メソッドから呼び出しているときは、selfは「オブジェクト」になっている。メソッドはクラス内で直接呼び出すことは出来ない。selfがクラスになっている限り、メソッドは呼び出せない。

  def call_hello
    hello
  end

  p call_hello
insttance_eval.rb:8:in `<class:MyClass>': undefined local variable or method `call_hello' for MyClass:Class (NameError)      
Did you mean?  caller

Silverの勉強で扱ったように、privateメソッドはレシーバー付きで呼び出すことが出来ない。呼び出すには、オブジェクト>publicメソッド>privateメソッドのように扱う必要がある。

ところが、クラス内のローカル変数は更に奇妙なアクセスバリデーションを設けているようだ。研究すると長くなりそうなので、この件について後日追記したい。