2008年7月2日水曜日

Ruby の関数オブジェクト - Proc とブロックの使い方

1. 関数オブジェクトとは

これまで、Ruby のイテレータから、「ブロック付きメソッド」について見てきた。

今回は、他の言語で「関数オブジェクト」と呼ばれるものを生成するための、Proc クラスについて調べる。

Proc - Rubyリファレンスマニュアル によると、

Proc はブロックをコンテキスト(ローカル変数のスコープやスタックフレーム)とともにオブジェクト化した手続きオブジェクトです。

Ruby の解説を初めて読んだとき、Proc クラスは分かりにくかった。同時に、以下の項目も理解しずらかった。

  • イテレータ
  • ブロック付きメソッド
  • Proc オブジェクト
  • proc
  • lambda{}
  • &変数

今思えば、これらを

「Ruby で、メソッドに、関数を渡すための工夫」

という視点で、考えればよかった。

 

2. 関数オブジェクトの生成と呼出し

Ruby で、関数オブジェクトを生成する方法は

  1. proc
  2. lambda

の2つある。

Python, Haskell を触るようになってからは、proc よりも

lambda {}

の方がしっくりくる。(proc, lambda には、微妙な違いがあるが、ここでは立ち入らない。)

# Proc オブジェクトを作成
l = lambda{puts "lambda"}
p = proc{puts "proc"}
procNew = Proc.new{puts "Proc.new"}

# Proc オブジェクトの呼出し
l.call
p.call
procNew.call
l[]

# Proc オブジェクトに引数を渡す
l = lambda{|x| puts x}
l.call(100)
l.call("hoge")

ただし、

  1. コードを書く際の文字数の少なさと、
  2. Proc オブジェクトを呼出するときの、メソッド call との対応を考えると、

Procedure – call

のように対にすると覚えやすい。

proc{}

で書く方がいいのかも。

どちらが無難な書き方なんだろうか?

 

3. ブロックの扱い方を復習

で見たように、

  1. メソッドには、ブロックを渡すことができる
  2. 渡されたメソッドは、yield によって実行される

一番シンプルな形の、ブロック付きメソッドの定義は、以下の通り。

# 与えられたブロックを実行する yield
def testproc
 puts yield
end

testproc{ "call testproc" }

ただし、メソッドのシグニチャを見ただけでは、ブロックが渡されることを前提としたメソッドであるかわからない。

例えば、上記の testproc をブロックなしで呼出した場合、エラーとなる。

testproc   -> `testproc': no block given (LocalJumpError)

実行してみないと、メソッドの挙動がわからないというのは不便。 (@_@;)

この点について、「まつもとゆきひろ×結城浩,Rubyを語る:ITpro」 で、次のように述べられている。

最初にブロックをデザインしたときには,こんなに応用できるとは思いませんでした。もし最初から暗黙の引数みたいなデザインをしていたら,今許しているアンパサンド「&」の文法は必須にして,yieldはなくして,「&」が付いていないのにブロックを渡したらエラーになるとか…*23

はいはい! それ欲しい!(笑)

(笑)そういうデザインにできたと思うんですけど,そうはならなかったのが,僕の発想の限界です。

(装飾は、引用者による)

 

block_given? でブロックの有無を判定

もし、ブロック付きの呼出しと、ブロックなしでの呼出しの両方に対応したい場合、メソッドでは block_given? を使ってありなしを判定し、処理を場合分けする。

def testproc2
 if block_given?
  puts "block_given"
 else
  puts "no block"
 end
end

testproc2
testproc2{}

このメソッドは、いかにも後付な感じがするメソッドなので、好きになれない。 (@_@;

 

引数における &変数 の使い方

ブロックを明示的にメソッドの引数に記述するには、引数の最後で &変数を記述する。

def testproc3(a, b, &block)
 print a, ",", b, "\n"
 block.call
end

ブロックを他のメソッドに渡すときは、&変数 という形で渡す。

def testproc4(&block)
 testproc3(100, 200, &block)
end

testproc4{puts "testproc4"}

メソッドにおいて、ブロックに対応する引数は、

&変数

であることを忘れずに。 &が変数の接頭辞にないと、それをブロックと認識してくれない。

 

4. メソッドに複数の関数オブジェクトを渡す

まつもとゆきひろ×結城浩,Rubyを語る:ITpro」によると、

ブロックって二つは渡せないんですか?

渡せないですね。そのへんがRubyの制約なんですね。で,まぁ正直自分も驚いているのは,1個しかブロックを渡せない高階関数*22を簡単に書けたら,これだけ応用範囲が広がったということ。

関数を二つ以上渡したいときは、メソッドにブロックではなく関数オブジェクトを与える。

def testproc5(a, b)
 a.call
 b.call
 yield
end

testproc5(lambda{puts "first"}, lambda{puts "second"}){puts "block"}

メソッドに、ブロックを渡すには、変数の前に & を付ける必要があったが、関数オブジェクトトの場合、普通の変数と変わらない。メソッドを渡す前には、関数オブジェクトにする必要がある。

ちなみに、上記のコードの最後には、 yield の呼び出しがある。つまり、このメソッドは、ブロックも受け取る。

 

5. 本質的な処理と、周辺的な処理の分割

ある処理を実現するために、本質的な処理と、それを支えるための周辺的な処理とに別れることがある。例えば、ファイルの内容を処理するとき、ファイルを開いたり、閉じたりすることは、問題の周辺的な処理とみなすことができる。

Ruby では、ブロックに本質的な処理を記述し、メソッドに渡すような定義ができる。

(cf. MF Bliki: Closure)

def testproc6
 puts "前処理"
 yield
 puts "後処理"
end

testproc6{puts "本質的な処理"}

やっていることは、ストラテジーパターンと同じこと。

 

6. 文脈の保持

Proc オブジェクトの特徴は、作成したときの文脈を保持していること。

a = "before lambda"
l = lambda{puts a}
l.call
a = "after lambda"
l.call

 

7. Proc オブジェクトを返すメソッド

MF Bliki: Closure には、クロージャの使い方として、Proc  オブジェクトを返すメソッドが紹介されている。

記述されているのは、

  • Employee クラス : 給料を持つ「従業員」を表す。
  • paidMore メソッド :  「従業員」の給料が、ある金額よりも上か判定するクロージャ。
class Employee
 attr_accessor :salary
end

def paidMore(amount)
  return Proc.new {|e| e.salary > amount}
end

highPaid = paidMore(150)

john = Employee.new
john.salary = 200
print highPaid.call(john)

paidMore メソッドは、「ある一定の給与以上を得ている人を判定する」ための手続きを、Proc オブジェクトにして返す。返された Proc オブジェクトを使い、実際の従業員に対して判定を行う。paidMore メソッドは、 Employee を分類するための分割基準を作る。

ちなみに、この関数がクロージャである理由は、Proc オブジェクトのブロック変数 e が、paidMore メソッドのローカル変数でも、引数でもないため。 (cf. JavaScript のクロージャ と オブジェクト指向2. クロージャの意味 )

では、下図のように分割基準を paidMore メソッドを使って作成してみる。

080604-3

以下のコードでは、それぞれに分類される Employee を生成しておき、 Proc オブジェクトを使って抽出してみた。

tarou = Employee.new
tarou.salary = 130
jirou = Employee.new
jirou.salary = 50
emps = []
emps << john << tarou << jirou
p emps.select{|x| highPaid.call(x)}

middlePaid = paidMore(100)
lowPaid = paidMore(0)

p emps.select{|x| middlePaid.call(x) and not highPaid.call(x)}
p emps.select{|x| lowPaid.call(x) and not middlePaid.call(x)}

 

8. ファーストクラスとは?

Rubyで関数プログラミング Part 6 【ファーストクラスの関数】 によると、

Rubyの関数オブジェクトは、どうやら「関数」オブジェクトではないそうです。つまり、proc(lambda)で作ることのできるオブジェクトとは、(Smalltalk的な)ブロックであるらしく、|x,y|と引数を指定しても、それは仮引数ではなく、ブロック外の同名の変数(つまり、ブロック外のx,yという名の変数)を上書きしてしまうのだそうです。従って、これを使用してはファーストクラスの関数を表現できないことになります。ですので、 Rubyでファーストクラスの関数を表現し得るかという議論は、この点からは否定されることとなるのではないかと思います。

うーむ、ファーストクラスか... (@_@;)

Haskell を触るようになってから、チラホラ見かけるこの言葉。 QQQ

追記(2012.2.1) : First-class function - Wikipedia によると、

In computer science, a programming language is said to have first-class functions if it treats functions as first-class objects. Specifically, this means that the language supports passing functions as arguments to other functions, returning them as the values from other functions, and assigning them to variables or storing them in data structures.[1][2]

第6回 Ruby 1.9に起きた変化 - O'Reilly Japan Community Blog によると、

Rubyにはブロックというものがあります。縦棒(|)の中に変数があって、(直前に書かれたイテレータの各要素が)渡されパラメータとして代入される。これはもともとループの抽象化として誕生したので、棒の間はループの各要素が代入される場所だったんです。(ブロックパラメータは)任意の変数、つまりグローバル変数でも、ローカル変数でも、配列でも大丈夫。何でも置けたんです。

ところが1.9からはちょっと変わっていて、グローバル変数を置こうとするともうダメ。これまではオブジェクトの属性に代入できたり、配列のスライスを呼び出せたのも、できなくなりました。それと同時に、(ブロックパラメータに指定された)ローカル変数のスコープは、ブロックの範囲内にとどまるということ。

 

関連記事