[プログラミング演習(Ruby) >  手続きとその活用法]

手続きとその活用法

もくじ

  1. ブロック付きメソッドの定義と利用 --- yield式
    1. yield式
  2. データとしての手続き --- Procオブジェクト
    1. Procオブジェクト
    2. メソッドでのProcの生成

ブロック付きメソッドの定義と利用 --- 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式の値になります. これによってブロックの処理の結果をメソッドで利用できます.

データとしての手続き --- 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した場合は値は変わりません).

counterで生成するProcが返すのは代入式「i = i + n」の値,つまり左辺に代入される値です.

次のプログラムを動かしてみて,その挙動を確認してみて下さい(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

[プログラミング演習(Ruby) >  手続きとその活用法]