もくじ
ブロック付きメソッドの定義と利用 --- yield式
すでにRubyのプログラムでブロックを伴うメソッドは少なからず利用してきていることでしょう.
# times 8.times do |i| end # upto 1.upto(10) do |j| end # 配列のeach [1,2,3,4].each do |x| end # 配列のcollect [1,2,3,4].collect do |x| end # ファイルのopen File.open("foo.txt") do |f| end
Rubyではブロック付きメソッドを使うだけでなく,新たに定義することもできます. 次に例を示します(each_grid.rb).
# ブロック付きメソッドの例(each_grid) # a0≦a≦a1, b0≦b≦b1を満たす整数の組をすべて生成する def each_grid(a0,a1,b0,b1) return unless block_given? # ブロックがない→直ちに終了 b0.upto(b1) do |b| a0.upto(a1) do |a| yield(a,b) # a,bをブロックに渡す end end end # 9x9の表示 each_grid(1,9,1,9) do |y,x| # y,xにyieldされた(a,b)を受け取る xy = x*y puts "#{x}x#{y}=#{xy}" end puts "" # xn(2≦x≦5,2≦n≦4)を全て表示する each_grid(2,5,2,4) do |x,n| # x,nにyieldされた(a,b)を受け取る y = x**n puts "#{x}^#{n}=#{y}" end
このプログラムを次のように実行してみて下さい.
$ ruby each_grid.rb | less
九九(1×1,1×2,...,1×9,2×1,2×2,...,9×9)を順に表示した後, 22,32,42,52, 23,33,43,53, 24,34,44,54の 値を表示します.
なおこの例でlessを使っているのは,画面に表示されたデータをコントロールで きるようにするためです(ruby each_grid.rbの標準出力をlessの標準入力にパイプで送ります). [↓][↑]でデータをスクロール表示できます. また[q]で表示を終了します.
別の例を示します(sum_up_by.rb).
# aryの各要素をyieldした結果の「総和」(+)をとる def sum_up_by(ary,init_val=0) return init_val unless block_given? s = init_val ary.each { |a| s += yield(a) } s end def sum_str(k,ary) ary.collect { |a| "#{a}^#{k}" }.join(' + ') end n = (ARGV.size > 0) ? ARGV.shift.to_i : 4 m = (ARGV.size > 0) ? ARGV.shift.to_i : 3 a = Array.new(n) { |i| i+1 } # 1,2,...,n 1.upto(m) do |j| y = sum_up_by(a) { |x| x**j } puts "#{sum_str(j,a)} = #{y}" end puts "" c='A'.ord # 'A'の番号 b = Array.new(n) { |j| (c+j).chr } # ['A','B',...,] p b str = sum_up_by(b,"") { |c| c.downcase } # 各文字を小文字に変換して連結 puts str str2 = sum_up_by(b,"") { |c| c.succ } # 各文字の「次」を連結 puts str2
このプログラムは次のように整数n,mを指定して実行します. まず1,2,...,nの和(1乗和),2乗和,...,m乗和を出力します. 続いてA,B,...,の文字で構成される配列を作成して表示した後で A,B,...,を連結して加工した文字列を出力します.
$ ruby sum_up_by.rb 8 5 1^1 + 2^1 + 3^1 + 4^1 + 5^1 + 6^1 + 7^1 + 8^1 = 36 1^2 + 2^2 + 3^2 + 4^2 + 5^2 + 6^2 + 7^2 + 8^2 = 204 1^3 + 2^3 + 3^3 + 4^3 + 5^3 + 6^3 + 7^3 + 8^3 = 1296 1^4 + 2^4 + 3^4 + 4^4 + 5^4 + 6^4 + 7^4 + 8^4 = 8772 1^5 + 2^5 + 3^5 + 4^5 + 5^5 + 6^5 + 7^5 + 8^5 = 61776 ["A", "B", "C", "D", "E", "F", "G", "H"] abcdefgh BCDEFGHI
sum_up_byメソッドは配列aryを引数とします. また「init_val」(初期値)も引数にとります. ただし「init_val」が指定されない場合は「init_val=0」とします(デフォルト値).
yield式
ブロック付きメソッドでは,yield式によってブロックにデータを渡します. 2つ以上のデータを渡すこともできます. ブロックではそれらをブロックパラメタで受け取って処理を行います.
ブロックで最後に評価された式の値がyield式の値になります. これによってブロックの処理の結果をメソッドで利用できます.
- yield(式0,式1,...)
- 引数の式をすべて評価→結果のデータをブロックに渡す
(データ→ブロックのブロックパラメタに代入される) - ブロックの処理が終了→yieldが終了
- yield式の値=ブロックで最後に評価した式の値
データとしての手続き --- Procオブジェクト
さて次のような二つのメソッドを考えてみます.
def c1(m,n) s = 0 n.times do |j| s += f(j)*g(m-j) end s end def c2(m,n) s = 0 n.times do |j| s += u(j)*v(m-j) end s end
メソッドc1,c2はとてもよく似ています. このような計算を一般に畳み込み(convolution)といいます. c1,c2では内部で計算に用いているメソッド(以下,関数)が異なるだけです(f,gかu,vか).
このようにパターンが似ていたとしても,内部で計算する関数が異なればメソッド全体の定義は同一ではなくなります. それでは内部の関数が異なるごとに新しいメソッド(例えばc3,c4,...)を定義しなければならないのでしょうか.
これに対する解決法として,ブロック付きメソッドを導入することも考えられます.
def cb(m,n) s = 0 n.times do |j| s += yield(j,m-j) end s end a0 = cb(4,8) { |x,y| f(x)*g(y) } # == c1(4,8) a1 = cb(4,8) { |x,y| u(x)*v(y) } # == c2(4,8)
これによって確かにメソッドの定義は一つにできますが, このときブロック内の乗算(*)は「処理のパターン」の一部であるにも関わらず, それをブロックで明示的に書かないといけないのが不満です(乗算ぐらいは書いてもいいと考えるかもしれませんが,もっと複雑なパターンの演算が対象となる場合もありえます). またさらにメソッドの定義に「処理のパターン」の一部が書かれていないことから,一般化した定義としては不完全であるとも言えます.
要するに同一パターンの計算をすることは分かっていて,適用する計算する関数が異なるだけですので,関数を抽象化できれば問題を解決できます. このような場合にRubyではProcというオブジェクトを利用できます. Procはメソッドをデータとして扱うことを可能にします.
def conv(f,g,m,n) s = 0 n.times do |j| s += f.call(j)*g.call(m−j) end s end # f(x),g(x)の定義(Procオブジェクト) f = proc { |x| (x+1)**2 } g = proc { |x| Math.exp(-x.abs) } a = conv(f,g,4,8)
Procオブジェクト
Procオブジェクトはブロックで定義される手続きで,データとして扱えます. 数値,文字列,配列などと同様に変数に代入できて,メソッドの引数に使えますし, メソッドの値として返すこともできます.
Procの重要なメソッドとしてcallがあります. Procのcallメソッドを適切な引数とともに呼び出すと, Procのブロックにデータを渡して,ブロックの処理を実行します. ブロックの値(ブロックで最後に評価した式の値)がcallメソッドの値になります.
f = proc { |x| (x+1)**2 } y0 = f.call(1) # y0 == 4 y1 = f.call(-2) # y1 == 1 g = proc { |x,y| 2*x+y } y2 = g.call(3,4) # y2 == 10 y3 = g.call(-1,2) # y3 == 0
メソッドでのProcの生成
次にメソッドでProcを生成して返す例を示します.
def inc_by(k) proc { |x| x+k } end f = inc_by(3) g = inc_by(-1) a = f.call(1) # a == 4 b = f.call(3) # b == 6 c = g.call(5) # c == 4 def counter(i) proc { |n| i = i + n } end c0 = counter(0) c1 = counter(0) t = c0.call(1) # t == 1 t = c0.call(2) # t == 3 t = c0.call(1) # t == 4 t = c1.call(-2) # t == -2
Procはそのブロックの処理に必要になるデータをそれぞれ保持しています. 上の例では,inc_by(k)によって生成されたProcでは変数kの値を(Procごとにそれぞれ)保持しています. またcounter(i)で生成されたProcでは変数iを(Procごとにそれぞれ)保持しています. なおブロックパラメタ(inc_byのProcのx,counterのProcのn)は,実行時に渡されるデータであることに注意してください.
ところでcounterで生成されるProcのブロックでは, 変数iの値を更新しています(i = i + n). これによって保持されていた変数iが更新されます. その結果,上の例で示したように同一の引数でcallしても,そのたびに値が変わることになります(0でcallした場合は値は変わりません).
次のプログラムを動かしてみて,その挙動を確認してみて下さい(more_counters.rb). 「shared」で生成されるProcの組と「pair」で生成されるProcの組でカウンタ(変数i)の値はどう変化するでしょうか.
def generate_counter(i) proc { |n| i = i + n } end def generate_shared_counter(i) c0 = proc { |n| i = i + n } c1 = proc { |n| i = i + n } [c0,c1] end def generate_counter_pair(i) c0 = generate_counter(i) c1 = generate_counter(i) [c0,c1] end def counter_test(title,c0,name0,c1,name1,inc,n) puts "[#{title}]" n.times do v0 = c0.call(inc) v1 = c1.call(inc) puts "#{name0}=>#{v0}, #{name1}=>#{v1}" end puts '' end DEFAULT_N = 3 N = (ARGV.size > 0) ? ARGV.shift.to_i : DEFAULT_N c0 = generate_counter(0) c1 = generate_counter(0) c2,c3 = generate_shared_counter(0) c4,c5 = generate_counter_pair(0) counter_test('solo', c0,'counter0',c1,'counter1',1,N) counter_test('shared',c2,'counter2',c3,'counter3',1,N) counter_test('paired',c4,'counter4',c5,'counter5',1,N)
このプログラムは引数を1つまでとります. 指定された回数だけ内部で定義されているカウンタをそれぞれ動作させます. 回数が指定されない場合は3回ずつ動作させます.
$ ruby more_counters.rb $ ruby more_counters.rb 5