読者です 読者をやめる 読者になる 読者になる
スポンサーリンク

Rubyの動かないコード (初級編) ブロックとクロージャの性質

ruby Ruby on Rails 動かないコード

以下のRubyのコードが,意図した動作をしないのはなぜですか。(制限時間1分)

やりたい事:

  • 1から3までの各数値の2乗を計算する。
  • プログラム終了時点での,変数の状態を表示する。
# 「p」で日本語を出力するための設定
$KCODE = "sjis"

# 1から3までの範囲で実行
1.upto(3) do |i|
  # 2乗した値をnに代入
  n = i * i
  
  # その値を出力
  p "#{ i }の2乗は,#{ n }です。"
end

p "プログラム終了時点でのnの値は,#{ n }です。"

発生する問題

>ruby hoge.rb

"1の2乗は,1です。"
"2の2乗は,4です。"
"3の2乗は,9です。"
hoge.rb:12: undefined local variable or method `n' for main:Object (NameError)

最後の部分で,n の値を表示できない。


解決策

1から3までのループの直前に

# n を初期化
n = nil

とすればよい。

なぜか?



その理由を理解するためには,

Rubyの「ブロック」に関する事項を知っておく必要がある。

「ブロック」のおさらい

  1. ブロックとは何か
  2. yieldとは何か
  3. クロージャとは何か

(1)ブロックとは何か

以下のようなものがブロック。一連の処理を表す。

do 
  p "fuga"
end


ブロックは単独で存在することはできない。

関数の引数になることだけが可能。

ブロックを使う2つの方法
http://itpro.nikkeibp.co.jp/article/C...

  • Rubyのブロックはメソッド呼び出しに付加できるコードの塊であって,それ自身はオブジェクトではありません

例えば,以下のようにProcクラスのコンストラクタの引数にする,という場合がある。

ブロックは,Procオブジェクトのインスタンスにしておけば,あとからcallで呼び出せる。

# "hoge"と表示するような処理のブロック
proc1 = Proc.new do 
  p "hoge"
end

proc1.call
  # -> "hoge"


# 引数を受け取ってあいさつするような処理のブロック
proc2 = Proc.new do |s|
  p "Hello, #{ s }!"
end

proc2.call( "World" )
  # -> "Hello, World!"


また,自作の任意の関数の引数にすることもできる。

$KCODE = "sjis"

# ブロックを引数にとる関数
def func( &block )
  p "実行します。"
  block.call
  p "実行しました。"
end


# ブロックを引数に渡して実行
func do 
  p "hoge"
end

出力:

"実行します。"
"hoge"
"実行しました。"


関数定義の引数部分にブロック引数を書く場合,

  • ブロック引数は最後の引数でなければならない。
    • つまり,関数には,ブロック引数を1つしか渡すことはできない。
  • 引数の前に & という接頭辞をつける必要がある。
    • これをつけると,その引数はProcオブジェクトに変換され,関数本体中では .call などとして呼び出せる。


以下は,同じ処理を & という接頭辞無しで記述したもの。

$KCODE = "sjis"

# 引数に & を付けていない事に注意
def func( block )
  p "実行します。"
  block.call
  p "実行しました。"
end

# Procオブジェクトを生成
prc = Proc.new do
  p "hoge" 
end

# Procオブジェクトを引数に渡す
func prc


ブロックのまとめ:

  • do〜endのことをブロックと呼ぶ。
  • 単独では存在できず,関数の引数にしかなれない。
  • ブロックをProc.newでProcオブジェクトに変換すれば,callで呼び出せる。
  • 関数の引数にする場合は,最後の引数に & を付与する。そうするとProcオブジェクトに変換される。

※なお,Proc.newのかわりに「lambda」としてもよい。


(2)yieldとは何か

前述のコードでは,受け取ったブロック引数を関数内で実行する際に「block.call」としていた。

かわりに,単に yield としてもよい。

$KCODE = "sjis"

def func( &block )
  p "実行します。"
  yield # block.callと同じ意味になる
  p "実行しました。"
end


func do
  p "hoge" 
end

関数は,どうせブロック引数は1つしか取れない。

なので,関数中で「どのブロックを実行するのか」というのはわかりきっている。

だから,わざわざ「blockという引数を云々(block.call)」と書く必要は無くて,yieldとだけ書けばよい。


さらに,yieldを使う場合は,「block」という変数を無視できるので,関数の引数部分でもブロック引数を省略する事になる。

$KCODE = "sjis"

def func
  p "実行します。"
  yield # funcに渡されたブロック引数を実行する
  p "実行しました。"
end


func do
  p "hoge" 
end

こうすると,一見funcは何も引数をとらない関数のように見える。

しかし実際には,funcにブロック引数を渡すことが可能なのである。


これはrubyのコードを読むときの1つの定石だ。

何もしなさそうな関数の中にyieldと書いてあるのを見つけたら,その関数はブロック引数を受け取るという事がわかる。

(逆に,関数の引数定義部分だけを見ても,その関数がブロック引数を受け取れるということはわからないのである。)



yieldの次に何か書けば,それは実行対象のブロックに対して渡される引数になる。(ブロック実行時用の引数)

$KCODE = "sjis"

def func( name )
  p "実行します。"
  yield( name )
  p "実行しました。"
end


func( "World" ) do |s|
  p "Hello, #{ s }!" 
end
  # -> "Hello, World!"

ここでは,funcに2つの引数を渡していることに注目。

一つ目は,name。2つめは,ブロック引数。




yieldのまとめ:

  • 関数の引数定義部に,明示的にブロック引数を書かなくても良い。
    • その場合,ブロック引数の呼び出しは「yield」で行なう。


参考:

Rubyのblock、Proc、lambdaを理解する
http://d.hatena.ne.jp/shunsuk/2009010...

  • メソッドの中でblockの名前を特定する必要はありません。かわりにyieldキーワードを使うことができます。
  • 同じblockを何度も使いたいときがあります。そんなときに再利用するためのコードがProcです。blockとProcの唯一の違いは、blockは保存できないということです。
  • Procと違って、lambdaは引数の数をチェックする。Procだと、余分な変数には"nil"が入りますが、lambdaではエラーが発生します。
  • Procはメソッドではなく、コードスニペットです。このため、Proc内のreturnは親メソッドのreturnになります。lambdaはメソッドと同じように動作します。lambdaは、匿名のメソッドを書くのと同じことなのです。

(3)クロージャとは何か

クロージャは以下の性質を持つ。

  • (a)クロージャのすぐ外側(クロージャが定義された環境)で定義された変数は,クロージャ内からも参照できる。
  • (b)クロージャ内で定義された変数は,クロージャ外から参照することはできない。


Rubyのブロックは,すべてクロージャである。

つまり上記を言い換えると

  • (a)Rubyのブロックは,ブロック定義時のコンテキスト(変数とか)を保持する。
  • (b)Rubyのブロック内で始めて宣言された変数は,ブロックローカル変数になる。


冒頭のコードが動作しなかった理由は,この(b)の性質が原因。

そのため,(a)を利用して解決したのである。

Rubyのブロック(クロージャ)はローカル変数をインスタンス変数に変えるマジックだ!
http://d.hatena.ne.jp/keyesberry/2009...

  • ブロックはそれ単独ではメモリ上に存在できない
  • Rubyのブロックはメソッドによる手続きブロックとは異なって,ブロックの外側で定義されたローカル変数を,ブロック内で参照・変更できるという性質を有する
  • ブロックによってローカル変数がインスタンス変数のように働いて(状態を保持する役目を果たして)いる


Ruby の Proc オブジェクトと Method オブジェクトの違い (proc, lambda, ブロック, メソッドについて)
http://d.hatena.ne.jp/vividcode/20100...

  • 変数束縛というのは、引数以外の変数を実行時の環境ではなく自身が定義された環境 (静的スコープ) で解決する、ということを意味しています。 例えば Ruby のブロックは、ブロックの外側の変数にアクセスすることができますが、これはブロックがクロージャだからです。

Ruby on Railsでの似たような例

Ruby on Railsのモデルクラス内に,以下のようなコードがあったとする。

  # 新規ユーザを登録する。
  def self.create_user( user, bookmark )
  
    self.transaction do 
      # ユーザ情報を新規登録する。発番されたIDを控えておく
      user.save
      created_user_id = user.id

      # ブックマーク情報を新規登録する。
      bookmark.user_id = created_user_id
      bookmark.save
    end
    
    # 新規登録されたユーザのIDを返す。
    created_user_id
  end


このコードは動かない。なぜなら,冒頭のコードと同じ原因によって,created_user_idという変数はブロックローカルになってしまうからだ。

メソッドの返り値としてcreated_user_idを返却したい場合は,transactionのブロックよりも前の時点で変数を初期化しておく必要がある。


Railsではブロックが多用されるので,気づかないうちにこういうミスをしていたりする。

補足

このエントリを,かみくだいて更にわかりやすく説明して下さった方がいる。

[Ruby基礎] ブロックとProcをちゃんと理解する - Qiita [キータ]
http://qiita.com/kidachi_/items/15cfe...

関連する記事:

Rubyの動かないコード (中級編) ローカル変数の「暗黙の初期化」に関するエラー(ローカル変数のスコープが事前コンパイルで決まる)
http://language-and-engineering.hatenablog.jp/entry/20101205/p1


Rubyの動かないコード (中級編) Ruby on Railsで,スレッドごとにトランザクションを分離したい
http://language-and-engineering.hatenablog.jp/entry/20101229/p1


あなたが理解できない,たった一行のRubyのコード (動的言語に対する静的解析の限界)
http://language-and-engineering.hatenablog.jp/entry/20120619/p1