M.Hiroi's Home Page

お気楽 Ruby プログラミング入門

第 8 回 イテレータと高階関数

[ PrevPage | R u b y | NextPage ]

はじめに

前回は関数の再帰定義について説明しました。今回は Ruby の特徴であるイテレータとブロックについて詳しく説明します。そして、その応用例として高階関数とクロージャについて説明します。

イテレータ (Iterator) は「繰り返すもの」という意味で、元々は繰り返しを実現するための機能のことです。ところが、Ruby のイテレータはとても便利で強力だったため、繰り返し以外の場面でも使われるようになりました。このため、最近はイテレータとは呼ばずに「ブロック付きメソッド」と呼ばれているようです。たとえば、ファイルをオープンする open() は、繰り返しを行わなくても「ブロック」を受け取ることができます。

●イテレータとブロック

Ruby のイテレータを図 1 に示します。

object.method(a1, a2, ...) do |b1, b2, ...|
  処理A
  処理B
  ...
end

object.method(a1, a2, ...) {|b1, b2, ...|
  処理A
  処理B
  ...
}

        図 1 : イテレータの構造

Ruby では do ... end と { ... } を「ブロック」といいます。一般的なプログラミング言語では、複数の処理を一つにまとめたものをブロックといいます。たとえば、C/C++や Perl では { ... } の中に複数の処理を記述することができます。普通はこれをブロックといいます。

Ruby のブロック [*1] は一般的なものとは大きく異なっていて、簡単にいえばメソッドから呼び出される関数のことです。したがって、Ruby のイテレータとは、与えられたブロックを呼び出すメソッドということになります。

簡単な例を示しましょう。

irb> 5.times {|x| print x, "\n"}
0
1
2
3
4
=> 5

メソッド n.times() は整数 n で指定した回数だけブロックを呼び出します。このとき、ブロックには 0 から n - 1 までの整数が渡されます。ブロックの引数は | ... | の中に記述します。この引数のことを「ブロックのパラメータ」といいます。この例では引数 x に整数が渡されます。このほかに、整数 n から m までを昇順でブロックに渡すメソッド n.upto(m) や降順でブロックに渡すメソッド n.downto(m) があります。

Ruby の場合、配列やハッシュなどのように複数のデータを格納するコレクションにはメソッド each() が定義されています。このメソッドは要素を順番に取り出して、それをブロックに渡して実行します。簡単な例を示しましょう。

irb> a = ["foo", "bar", "baz"]
=> ["foo", "bar", "baz"]
irb> a.each {|x| print x, "\n"}
foo
bar
baz
=> ["foo", "bar", "baz"]

Ruby の for 文は each() を使って実装されています。したがって、each() が定義されているコレクションであれば for 文を使うことができるわけです。

ところで、do ... end と { ... } は、結合規則が異なるので注意してください。たとえば、引数のカッコを省略した場合、do ... end はメソッドに渡されますが、{ ... } は最後の引数に渡されます。

foo a, b do |x| ... end  # foo の引数は a, b とブロック
bar a, b {|x| ... }      # bar の引数は a と b にブロックを渡して実行した結果

bar の場合、引数 b がブロックを受け付けるメソッドでないとエラーになります。ブロックを指定する場合、引数のカッコは省略しないほうがわかりやすいかもしれません。本稿ではブロックを渡すとき、メソッドに引数があるときはカッコを付けることにします。

-- note --------
[*1] Smalltalk にもブロックがあり、Ruby のブロックは Smalltalk に近いものです。

●イテレータの作り方

イテレータを作る場合、関数またはメソッドからブロックを呼び出す方法が必要になります。Ruby にはいくつかの方法が用意されていますが、一番簡単な方法は yield 文を使うことです。次の例を見てください。

irb> def foo(x)
irb>   yield x
irb> end
=> :foo
irb> foo(10) {|x| print x, "\n"}
10
=> nil

yield 文は与えられているブロックを実行します。yield 文に与えた引数は、そのままブロックに渡されます。なお、foo() を呼び出すときにブロックを与えないとエラーになります。ブロックの返り値は一番最後に実行した処理結果になります。なお、ブロックの途中で値を返したい場合は return ではなく next を使います。これはあとで説明します。

簡単な例として、配列を線形探索する関数 find(), position(), count() をイテレータでプログラムしてみましょう。

リスト 1 : データの探索 (イテレータ版)

# ブロックが真となる要素を探す
def find(ary)
  for x in ary
    return x if yield x
  end
  false
end

# ブロックが真となる要素の位置を返す
def position(ary)
  for x in 0...ary.size
    return x if yield ary[x]
  end
  false
end

# ブロックが真となる要素の個数を求める
def count(ary)
  c = 0
  for x in ary
    c += 1 if yield x
  end
  c
end

簡単な実行例を示しましょう。

irb> a = [1, 2, 3, 4, 5]
=> [1, 2, 3, 4, 5]
irb> find(a) {|x| x % 2 == 0}
=> 2
irb> position(a) {|x| x % 2 == 0}
=> 1
irb> count(a) {|x| x % 2 == 0}
=> 2

このように、ブロックを使うと探索する条件を簡単に指定することができます。

●イテレータの制御

イテレータは繰り返しを実現するための機能なので、ブロックの中で次に示す制御文が使用できます。

break [val] : イテレータメソッドの実行を終了する。メソッドは nil (または val) を返す。
next  [val] : そのブロックの実行を終了する。yeild は nil (または val) を返す。
redo        : そのブロックの実行を先頭からやり直す。

簡単な実行例を示します。

irb> 10.times {|x| 
irb>   print x, "\n"
irb>   break if x == 3
irb> }
0
1
2
3
=> nil
irb> 10.times {|x|
irb>   next if x == 3
irb>   print x, "\n"
irb> }
0
1
2
4
5
6
7
8
9
=> 10

メソッド 10.times() でブロックを 10 回繰り返します。ブロックの中で break を実行すると、times() の繰り返しを終了して nil を返します。これが times() の返り値になります。next を実行すると、ブロックの処理を終了して nil を返します。これが yield の返り値になります。times() の繰り返しは終了しないことに注意してください。

ブロックの中で return を呼び出すと次のようになります。

irb> [1,2,3,4,5].each {|x|
irb>   return if x == 3
irb>   puts x
irb> }
1
2
=> unexpected return (LocalJumpError)

return は関数の呼び出し先から呼び出し元に戻るための命令です。戻るところがないのでエラーになるわけです。もし、次のようにメソッド foo() を定義すると、ブロックの中の return は foo() を終了して値を返すことになります。

irb> def foo(a)
irb>   a.each {|x|
irb>     return "NG" if x == 3
irb>     puts x
irb>   }
irb>   "OK"
irb> end
=> :foo
irb> foo [2,4,6,8]
2
4
6
8
=> "OK"
irb> foo [1,3,5,7]
1
=> "NG"

●高階関数

Lisp / Scheme などの関数型言語の場合、関数は他のデータと同等に取り扱うことができます。つまり、関数を変数に代入したり、引数として渡すことができるのです。また、値として関数を返すこともできるので、関数を作る関数を定義することが簡単にできます。関数を引数として受け取る関数を「汎関数 (functional)」とか「高階関数 (higher order function)」と呼びます。

Ruby は手続き型言語なので、関数型言語のように関数やメソッドをそのまま変数に代入したり、引数として渡すことはできません。そのかわり、ブロックを使うことで高階関数と同様の処理を行うことができます。イテレータはブロックを受け取るメソッドなので、高階関数の特別な形式と考えることができます。また、ブロックを「手続きオブジェクト」に変換することで高階関数を実現することもできます。

●マッピング

簡単な例として、ブロックに配列の要素を渡して呼び出し、その結果を配列に格納して返す関数を作ってみましょう。このような操作を「マッピング (写像)」といいます。なお、関数に引数を与えて呼び出すことを、関数型言語では「適用」といいます。本稿でも関数呼び出しの意味で適用を使うことにします。プログラムをリスト 2 に示します。

リスト 2 : マッピング

def mapcar(ary)
  a = []
  for x in ary
    a.push(yield x)
  end
  a
end

Ruby には同じ機能を持つメソッド map() や collect() が定義されているので、関数名は mapcar() としました。名前は Common Lisp から拝借しました。プログラムは簡単です。for 文で配列 ary の要素を順番に取り出し、それをブロックに渡して評価します。その結果を配列 a に追加します。最後に a を返します。

それでは実行例を示しましょう。

irb> a = [1, 2, 3, 4, 5]
=> [1, 2, 3, 4, 5]
irb> mapcar(a) {|x| x * x}
=> [1, 4, 9, 16, 25]

引数 x を 2 乗する処理をブロックに定義します。このブロックを mapcar() に渡すと、要素を 2 乗した新しい配列を返します。Ruby のメソッド map() や collect() を使っても同じことができます。

irb> [1,2,3,4,5].map {|x| x * x}
=> [1, 4, 9, 16, 25]
irb> [1,2,3,4,5].collect {|x| x * x}
=> [1, 4, 9, 16, 25]

このように、ブロックを使うと Ruby でも高階関数を簡単に定義することができます。

●フィルター

フィルター (filter) は配列の要素に関数 (ブロック) を適用し、ブロックが真を返す要素を配列に格納して返す関数です。ここでは簡単な例題として、ブロックが真を返す要素を削除する関数 remove_if() を作ってみましょう。関数名は Common Lisp から拝借しました。

リスト 3 : 要素の削除

def remove_if(ary)
  a = []
  for x in ary
    a.push(x) unless yield x
  end
  a
end

mapcar() と同様に remove_if() も簡単です。ブロックが偽を返したならば x を配列 a に加えるだけです。

簡単な実行例を示します。

irb> remove_if(a) {|x| x % 2 == 0}
=> [1, 3, 5]

配列を破壊的に修正してもよければ、Ruby のメソッド delete_if() や reject!() を使うことができます。

irb> b = [1,2,3,4,5]
=> [1, 2, 3, 4, 5]
irb> b.delete_if {|x| x % 2 == 0}
=> [1, 3, 5]
irb> b
=> [1, 3, 5]

もちろん、フィルターも簡単に定義することができます。remove_if() とは逆に、ブロックが真を返すとき要素を配列に追加し、偽を返すときは配列に加えません。なお、Ruby には同様の動作を行うメソッド select() があります。

リスト 4 : フィルター

def filter(ary)
  a = []
  for x in ary
    a.push(x) if yield x
  end
  a
end

簡単な実行例を示しましょう。

irb> filter(a) {|x| x % 2 == 0}
=> [2, 4]
irb> a.select {|x| x % 2 == 0}
=> [2, 4]

●畳み込み

2 つの引数を取る関数 f() と配列を引数に受け取る関数 reduce() を考えます。reduce() は配列の各要素に対して関数 f() を図 2 のように適用します。

(1) [a1, a2, a3, a4, a5] => f( f( f( f( a1, a2 ), a3 ), a4 ), a5 )

(2) [a1, a2, a3, a4, a5] => f( a1, f( a2, f( a3, f( a4, a5 ) ) ) )

            図 2 : reduce() の動作

関数 f() を適用する順番で 2 通りの方法があります。図 2 (1) は配列の先頭から f() を適用し、図 2 (2) は配列の後ろから f() を適用します。たとえば、関数 f() が単純な加算関数とすると、reduce() の結果はどちらの場合も配列の要素の和になります。

f(x, y) = x + y の場合
reduce() => a1 + a2 + a3 + a4 + a5

このように、reduce() は配列のすべての要素を関数 f() を用いて結合します。このような操作を「縮約」とか「畳み込み」といいます。また、reduce() の引数に初期値 g を指定することがあります。この場合、reduce() は図 3 に示す動作になります。

(1) [a1, a2, a3, a4, a5] => f( f( f( f( f( g, a1 ), a2 ), a3 ), a4 ), a5 )

(2) [a1, a2, a3, a4, a5] => f( a1, f( a2, f( a3, f( a4, f( a5, g ) ) ) ) )

            図 3 : reduce() の動作 (2)

Ruby のメソッド inject() と reduce() は、初期値を省略すると図 2 (1) の動作になり、初期値を指定すると図 3 (1) の動作になります。ここでは簡単な例題として、図 3 (1) の動作を行う関数 reduce_left() と、図 3 (2) の動作を行う関数 reduce_right() を作ってみましょう。リスト 5 を見てください。

リスト 5 : 畳み込み

def reduce_left(ary, init)
  a = init
  for x in ary
    a = yield(a, x)
  end
  a
end

def reduce_right(ary, init)
  a = init
  ary.reverse_each {|x|
    a = yield(x, a)
  }
  a
end

reduce_left() の引数 ary が配列、init が初期値です。最初にローカル変数 a を init で初期化します。次に、for ループで ary の要素を一つずつ取り出してブロックに渡して実行します。その結果を変数 a にセットします。reduce() は変数 a の値をブロックの返り値で更新することで、図 3 (1) の動作を実現しています。

たとえば、配列が [1, 2, 3] で init が 0 とします。最初は f(0, 1) が実行され、その返り値が a にセットされます。次は f(a, 2) が実行されますが、これは f(f(0, 1), 2) と同じことです。そして、その結果が a にセットされます。最後に f(a, 3) が実行されますが、これは f(f(f(0, 1), 2), 3) となり、図 3 (1) と同じ動作になります。

reduce_left() の場合、配列の要素がブロックの第 2 引数になり、第 1 引数にはこれまでの処理結果が渡されます。reduce_right() の場合は逆になり、ブロックの第 1 引数に配列の要素が渡されて、これまでの処理結果は第 2 引数に渡されます。reverse_each() は配列の後ろから順番に要素を取り出すメソッドです。

それでは実行例を示します。

irb> a = [1, 2, 3, 4, 5]
=> [1, 2, 3, 4, 5]
irb> reduce_left(a, 0) {|x, y| x + y}
=> 15
irb> reduce_left(a, 0) {|x, y| [x, y]}
=> [[[[[0, 1], 2], 3], 4], 5]
irb> reduce_right(a, 6) {|x, y| [x, y]}
=> [1, [2, [3, [4, [5, 6]]]]]
irb> a.reduce(0) {|x, y| x + y}
=> 15
irb> a.inject(0) {|x, y| x + y}
=> 15
irb> a.reduce(0) {|x, y| [x, y]}
=> [[[[[0, 1], 2], 3], 4], 5]
irb> a.inject(0) {|x, y| [x, y]}
=> [[[[[0, 1], 2], 3], 4], 5]
irb> a.reduce {|x, y| [x, y]}
=> [[[[1, 2], 3], 4], 5]
irb> a.inject {|x, y| [x, y]}
=>: [[[[1, 2], 3], 4], 5]

ブロックの引数を配列に格納して返すと、畳み込みの動作がよく理解できると思います。畳み込みは関数型言語でよく用いられる高階関数で、プログラミング言語によっては fold(), foldl(), foldr() などと呼ばれることもあります。畳み込みは 2 引数の関数と組み合わせることにより、いろいろな処理を簡単に実現することができます。

●手続きオブジェクト

Ruby のイテレータはとても便利な機能ですが、メソッドに渡すことができるブロックは一つだけです。これに対し、ブロックを手続きオブジェクトに変換すると、複数のブロックをメソッドに渡すことができます。手続きオブジェクトは Ruby が扱うことができるデータ (オブジェクト) なので、変数に代入したり、関数の引数に渡すことができます。また、関数の返り値として手続きオブジェクトを返すこともできます。

手続きオブジェクトは Proc.new() で作成します。次の例を見てください。

irb> add = Proc.new {|x , y| x + y}
=> #<Proc: ... >
irb> add.call(1, 2)
=> 3
irb> mul = proc {|x, y| x * y}
=> #<Proc: ... >
irb> mul.call(11, 12)
=> 132
irb> sub = lambda {|x, y| x - y}
=> #<Proc: ... >
irb> sub.call(10, 5)
=> 5

手続きオブジェクトは Proc.new() にブロックを付けて呼び出すことで生成することができます。Proc は手続きオブジェクトを表すクラスです。なお、Proc.new() には lambda [*2] と proc という別名が用意されています。Proc.new(), proc, lambda はどれもクラス Proc のオブジェクトを生成しますが、その動作はちょっとした違いがあります。詳細は Ruby のリファレンスマニュアル 手続きオブジェクトの挙動の詳細 をお読みください。

手続きオブジェクトを実行するにはメソッド call() を使います。call() に渡された引数はそのままブロックのパラメータに渡されます。上の例では変数 add に手続きオブジェクトをセットし、add.call(1, 2) でブロックを実行します。このとき、call() の引数 1, 2 がブロックのパラメータ x, y に渡されて、実行結果が 3 となります。

●ブロック引数

イテレータはブロックを手続きオブジェクトに変換して、引数として受け取ることができます。この引数を「ブロック引数」といいます。ブロック引数は名前の先頭に & をつけて表します。ブロック引数は仮引数の一番最後に定義しないといけません。

たとえば、ブロック変数を使って mapcar() を書き直すと次のようになります。

リスト 6 : マッピング (その2)

def mapcar(ary, &func)
  a = []
  for x in ary
    a.push(func.call(x))
  end
  a
end

mapcar() に渡されたブロックは手続きオブジェクトに変換されて引数 func に渡されます。ブロックが指定されていない場合、func には nil がセットされます。ブロック引数は、受け取ったブロックを他の関数 (メソッド) へ渡すときに使うと便利です。

それでは実行してみましょう。

irb> a = [1,2,3,4,5]
=> [1, 2, 3, 4, 5]
irb> mapcar(a) {|x| x * x}
=> [1, 4, 9, 16, 25]

●手続きオブジェクトとブロック

手続きオブジェクトはブロックのかわりに使うことができます。その場合は、先頭に & を付けてメソッドに渡します。簡単な例を示しましょう。

irb> square = Proc.new {|x| x * x}
=> #<Proc: ...>
irb> mapcar(a, &square)
=> [1, 4, 9, 16, 25]
irb> a.map &square
=> [1, 4, 9, 16, 25]
irb> mapcar(a, &Proc.new {|x| x * x})
=> [1, 4, 9, 16, 25]

手続きオブジェクトを変数 square にセットします。mapcar() や map() に渡す場合は、変数名の前に & を付けて渡します。Proc.new() の前に & を付けて渡すこともできます。

ブロックのかわりにメソッドを渡すこともできます。次の例を見てください。

irb> mapcar(a, &:to_s)
=> ["1", "2", "3", "4", "5"]
irb> a.map &:to_s
=> ["1", "2", "3", "4", "5"]

ブロックの引数 (オブジェクト) のメソッドを呼び出したい場合は、メソッド名 (シンボル) の前に & を付けて渡します。上記の場合、&:to_s はブロック {|x| x.to_s} を渡したことと同じになります。

ブロックの引数をメソッドの引数に渡す場合、次に示すような方法もあります。

irb> def cube(x) = x * x * x
=> :cube
irb> cube(10)
=> 1000
irb> m = method(:cube)
=> #<Method: Object#cube>
irb> m.call(10)
=> 1000
irb> a.map &m
=> [1, 8, 27, 64, 125]
irb> mapcar(a, &m)
=> [1, 8, 27, 64, 125]

メソッド method() はクラス Method のオブジェクトを生成します。method() にはメソッド名を表すシンボルを渡します。このオブジェクトに & を付けてメソッド (イテレータ) に渡すと、ブロックのかわりにメソッドを呼び出すことができます。これは {|x| cube(x)} を渡したことと同じです。

Ruby の場合、メソッドを呼び出すだけでもいろいろな方法があります。詳しい説明は Ruby のリファレンスマニュアル メソッド呼び出し(super・ブロック付き・yield) をお読みくださいませ。

-- note --------
[*2] lambda は Lisp / Scheme のラムダ式 (lambda expression) のことです。

●レキシカルスコープ

ここで、もう少し詳しくローカル変数のスコープについて説明しましょう。変数 x を表示する関数 foo() を定義します。

irb> def foo()
irb>   print x
irb> end
=> :foo
irb> x = 10
irb> foo()
=> エラー

foo() はローカル変数 x に値を代入していないので、foo() を実行するとエラーになります。それでは foo1() という関数から foo() を呼び出す場合を考えてみましょう。foo1() にはローカル変数 x を定義します。この場合、foo() は x の値を表示するのでしょうか。実際に試してみましょう。

irb> def foo1()
irb>   x = 100
irb>   foo()
irb> end
=> :foo1
irb> foo1()
=> エラー

この場合もエラーになります。このように、foo1() で定義したローカル変数 x は、foo() からアクセスすることはできません。図 4 を見てください。

図 4 : ローカル変数のスコープ
        図 4 : ローカル変数のスコープ

図 4 では、変数の有効範囲を枠で表しています。foo1() で定義したローカル変数 x は、関数 foo1() の枠の中でのみ有効です。もしも、この枠でローカル変数が見つからない場合は、一つ外側の枠を調べます。foo() の場合、関数定義の枠しかないので、ここで変数が見つからない場合はエラーになります。

このように、foo() から foo1() の枠を超えて変数 x にアクセスすることはできないのです。このような規則を「レキシカルスコープ (lexical scope)」といいます。レキシカルには文脈上いう意味があり、変数が定義されている範囲内 (枠内) でないと、その変数にアクセスすることはできません。

●ブロックのスコープ

それでは、ブロックの場合はどうなるのでしょうか。リスト 7 を見てください。

リスト 7 : 配列の要素を n 倍する

def times_element(n, ary)
  mapcar(ary) {|x| x * n}
end

mapcar() に渡すブロックのパラメータは x だけですから、変数 n をアクセスするとエラーになると思われるかもしれません。ところが、変数 n は関数 times_element() の引数 n にアクセスできるのです。図 5 を見てください。

図 5 : ブロックのスコープ

        図 5 : ブロックのスコープ

ポイントは、ブロックが関数 times_element() 内で定義されているところです。変数 n は関数の引数として定義されていて、その有効範囲は関数の終わりまでです。ブロックはその範囲内に定義されているため、変数 n にアクセスすることができるのです。つまり、関数内で定義されたブロックは、そのとき有効なローカル変数にアクセスすることができるのです。

もう一つ簡単な例題を示しましょう。指定した文字から始まる文字列を配列から削除する関数を作ってみましょう。最初に実行例を示します。

irb> remove_string("a", ["abc", "def", "agh", "ijk"])
=> ["def", "ijk"]

配列に格納された文字列の中で a から始まる文字列を削除します。この処理は filter() とブロックを使うと簡単に定義できます。

リスト 8 : 先頭文字が c の文字列を削除

def remove_string(c, ary)
  filter(ary) {|x| x[0] != c}
end

ブロックの中で remove_string() の引数 c をアクセスできるので、このような定義が可能になります。繰り返しを使うと、リスト 9 のようなプログラムになります。

リスト 9 : 先頭文字が c の文字列を削除

def remove_string(c, ary)
  a = []
  for x in ary
    a.push(x) if x[0] != c
  end
  a
end

繰り返しを使う場合、配列から要素を取り出す処理をプログラムする必要があります。ブロックと高階関数をうまく組み合わせると、複雑な処理でも簡単にプログラムすることができます。

●クロージャ

ここで関数型言語でよく用いられるテクニックを紹介しましょう。Lisp / Scheme などの関数型言語では、関数を生成する関数を簡単に作ることができます。このとき使われる機能が「クロージャ (closure)」です。

クロージャは実行する関数とアクセス可能なローカル変数をまとめたものです。クロージャは関数のように実行することができますが、クロージャを生成するときにアクセス可能なローカル変数を保存するところが異なります。アクセス可能なローカル変数の集合を「環境」と呼ぶことがあります。

Ruby でクロージャを生成するには手続きオブジェクトを使います。たとえば、「引数を n 倍する関数」を生成する関数は、手続きオブジェクトを使うと次のようになります。

irb> def foo(n)
irb>   Proc.new {|x| n * x}
irb> end
=> :foo
irb> foo10 = foo(10)
=> #<Proc: ... >
irb> foo10.call(100)
=> 1000
irb> foo5 = foo(5)
=> #<Proc: ... >
irb> foo5.call(11)
=> 55

関数 foo() は引数を n 倍する関数 (手続きオブジェクト) を生成して返します。変数 foo10 に foo(10) の返り値をセットします。すると、foo10 は引数を 10 倍する関数として使うことができます。同様に、変数 foo5 に foo(5) の返り値をセットすると、foo5 は引数を 5 倍する関数になります。

Proc.new() でブロックを生成するとき、実行する処理のほかに、そのときアクセス可能なローカル変数、つまり環境もいっしょに保存されます。この場合、アクセス可能なローカル変数は foo() の引数 n です。そして、クロージャを実行するときは、保存されているローカル変数にアクセスすることができるのです。

foo(10) を実行して手続きオブジェクトを生成するとき、定義されているローカル変数は n で、その値は 10 です。この値がクロージャに保存されているので、foo10 の関数は引数を 10 倍した結果を返します。foo(5) を評価すると n の値は 5 で、それがクロージャに保存されているので、foo5 の関数は引数を 5 倍した結果を返すのです。

●ジェネレータ

最後に、クロージャの応用例として「ジェネレータ (generator)」というプログラムを紹介しましょう。ジェネレータは、呼び出されるたびに新しい値を生成していきます。たとえば、Ruby の関数 rand() は実行するたびに乱数を返します。つまり、rand() は乱数列を発生する「ジェネレータ」と考えることができます。

簡単な例題として、フィボナッチ数列 (0, 1, 1, 2, 3, 5, 8, ...) を発生するジェネレータを作ってみます。関数名は make_fibo() としましょう。ジェネレータはグローバル変数を使って実現することができますが、クロージャを使った方がスマートです [*3]。まず、ジェネレータを作る関数を定義します。

リスト 10 : ジェネレータ生成 (1)

def make_fibo
  a = 0
  b = 1
  Proc.new {
    c = a
    a, b = b, a + b
    c
  }
end

関数 make_fibo はクロージャを返します。そして、このクロージャがジェネレータの役割を果たすのです。それでは、実際に実行してみましょう。

irb> load "makefibo.rb"
=> true
irb> fibo = make_fibo
=> #<Proc:...>
irb> 20.times {|x| print fibo.call, " "}
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 => 20
irb> fibo1 = make_fibo
=> #<Proc:...>
irb> 20.times {|x| print fibo1.call, " "}
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 => 20

make_fibo() で作成したクロージャを変数 fibo にセットし、fibo.call で実行します。0, 1, 1, 2, 3, 5 とフィボナッチ数列を生成していますね。新しいクロージャを変数 fibo1 にセットし、このクロージャを実行すれば、新しいフィボナッチ数列が生成されます。

クロージャで保存される環境は、make_fibo() で定義されたローカル変数 a, b です。これらの変数は make_fibo() が実行されたときに初期化される、つまり、クロージャを生成するときに初期化されることに注意してください。

環境はクロージャによって異なります。fibo のクロージャが実行されると、そのクロージャの環境が更新されるのであって、ほかのクロージャに影響を与えることはありません。したがって、あるジェネレータが発生するフィボナッチ数列が、ほかのジェネレータに影響を与えることはないのです。あとは必要な数だけジェネレータを make_fibo() で作り、生成したクロージャを変数に格納しておけばいいわけです。

-- note --------
[*3] Ruby の場合、このような数列の生成はクラス Enumerator を使ったほうが簡単に実装することができます。Enumerator には多数の高階関数が用意されていて、とても便利に使うことができます。ここでは Ruby の学習ということで、あえてクロージャでプログラムを作ってみました。

●ジェネレータをリセットする

次はフィボナッチ数列を最初に戻す、つまり、ジェネレータをリセットすることを考えましょう。この場合、クロージャ内の変数を書き換えるしか方法はありません。そこで、make_fibo() の返り値を 2 つに増やすことにします。最初の返り値はフィボナッチ数列を発生するジェネレータで、2 番目の返り値はジェネレータをリセットする関数とします。プログラムは次のようになります。

リスト 11 : ジェネレータ生成(2)

def make_fibo1
  a = 0
  b = 1
  value = Proc.new {
    c = a
    a, b = b, a + b
    c
  }
  reset = Proc.new {
    a = 0
    b = 1
  }
  return value, reset
end

変数 reset にジェネレータをリセットする処理を、value にフィボナッチ数列を発生する処理をセットします。どちらの関数も手続きオブジェクトを使って簡単に定義することができます。あとは、2 つの関数を return で返すだけです。

それでは実行してみましょう。

irb> f, r = make_fibo1
=> [#<Proc:...>, #<Proc:...>]
irb> 20.times {|x| print f.call, " "}
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 => 20
irb> r.call
=> 1
irb> 20.times {|x| print f.call, " "}
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 => 20

正常に動作していますね。

●おわりに

イテレータと高階関数について説明しました。M.Hiroi は関数型言語 (Lisp / Scheme など) に興味があります。M.Hiroi の趣味でクロージャまで説明しましたが、関数型言語の機能をここまでサポートしている Ruby には大変驚きました。Ruby で関数プログラミングを試してみるのも面白いと思います。興味のある方は挑戦してみてください。次回からはいよいよ Ruby のオブジェクト指向機能について説明します。


初版 2008 年 11 月 15 日
改訂 2017 年 1 月 15 日
改訂二版 2023 年 1 月 15 日
Copyright (C) 2008-2023 Makoto Hiroi
All rights reserved.