計算機プログラムの構造と解釈」の図形言語のところでつまずいているので、もう一度高階関数に対する自分の理解をRubyを使って整理してみる。

「高階関数」とはその引数として関数を取ったり、その戻り値として関数を返したりする関数のことである。

今3つの数の和、差、積、商を求めるメソッドを考える。

def plus_abc(a, b, c)
   a + b + c
 end
 
 def minus_abc(a, b, c)
   a - b - c
 end
 
 def mul_abc(a, b, c)
   a * b * c
 end
 
 def div_abc(a, b, c)
   a / b / c unless b == 0 or c == 0
 end
 
 plus_abc(1, 2, 3) # => 6
 minus_abc(10, 5, 3) # => 2
 mul_abc(4, 5, 6) # => 120
 div_abc(12, 3, 2) # => 2

これらのメソッドは使っている演算子は違うけれども、a (演算子) b (演算子) cという制御構造は同じだ。

だから変数と同じように演算を引数として渡せれば、これらのメソッドは一つにまとめられる。

def calc_abc(a, b, c, op)
   op[a, b, c]  #またはop.call(a, b, c)
 end

ここでopは上記演算の抽象である。

これに対して各演算の専用メソッドにおいて、具体的な演算の手続きをcalc_abcメソッドの引数として渡す。

def plus_abc(a, b, c)
   calc_abc(a, b, c, lambda { |x, y, z| x + y + z })
 end
 
 def minus_abc(a, b, c)
   calc_abc(a, b, c, lambda { |x, y, z| x - y - z })
 end
 
 def mul_abc(a, b, c)
   calc_abc(a, b, c, lambda { |x, y, z| x * y * z })
 end
 
 def div_abc(a, b, c)
   calc_abc(a, b, c, lambda { |x, y, z| x / y / z }) unless b ==0 or c == 0
 end
 
 plus_abc(1, 2, 3) # => 6
 minus_abc(10, 5, 3) # => 2
 mul_abc(4, 5, 6) # => 120
 div_abc(12, 3, 2) # => 2

演算を抽象化したcalc_abcメソッドがあれば、後から同種の演算を簡単に追加できる。

def power_abc(a, b, c)
   calc_abc(a, b, c, lambda { |x, y, z| x ** y ** z })
 end
 
 power_abc(2, 2, 2) # => 16

さらに同種の演算子を追加する場合に毎回手続きであるlambda…の入力は煩わしい。演算子を引数にとって手続きを戻り値とするメソッドを定義できれば問題は解決する。

def operator(op)
   lambda { |x, y, z| (x.send(op, y)).send(op, z) }
 end

operatorメソッドは演算子を引数に取って、3つの数に順次その演算子を作用させる手続きを返す、つまりoperatorメソッドは演算手続きを抽象化する。

これを用いることによって各演算メソッドはより簡潔になる。

def plus_abc(a, b, c)
   calc_abc(a, b, c, operator(:+))
 end
 
 def minus_abc(a, b, c)
   calc_abc(a, b, c, operator(:-))
 end
 
 def mul_abc(a, b, c)
   calc_abc(a, b, c, operator(:*))
 end
 
 def div_abc(a, b, c)
   calc_abc(a, b, c, operator(:/)) unless b ==0 or c == 0
 end
 
 def power_abc(a, b, c)
   calc_abc(a, b, c, operator(:**))
 end
 
 plus_abc(1, 2, 3) # => 6
 minus_abc(10, 5, 3) # => 2
 mul_abc(4, 5, 6) # => 120
 div_abc(12, 3, 2) # => 2
 power_abc(2, 2, 2) # => 16

ついでに引数をいくつでも取れるようにすればより汎用性が高まる。

def calc_abc(*i, op)
   op[i]
 end
 
 def operator(op)
  lambda do |x|
    mem = x.shift
    until x.empty?
      mem = mem.send(op, x.shift)
    end
    mem
  end
end
 def plus_abc(*i)
   calc_abc(*i, operator(:+))
 end
 
 def minus_abc(*i)
   calc_abc(*i, operator(:-))
 end
 
 def mul_abc(*i)
   calc_abc(*i, operator(:*))
 end
 
 def div_abc(*i)
   calc_abc(*i, operator(:/))
 end
 
 def power_abc(*i)
   calc_abc(*i, operator(:**))
 end
 
 def join_abc(*i)
   calc_abc(*i, operator(:join))
 end
 class Fixnum
   def join(other)
     self * 10 + other
   end
 end
 
 plus_abc(1, 2, 3, 4, 5) # => 15
 minus_abc(10, 5, 3, 2) # => 0
 mul_abc(4, 5, 6, 7, 8, 9) # => 60480
 div_abc(12, 3, 2) # => 2
 power_abc(2, 2, 2, 2) # => 256
 join_abc(1, 2, 3, 4, 5) # => 12345

高階関数の別の例を示そう。配列の各要素を倍にするメソッドと、配列の各要素を大文字にするメソッドを考える。

def multi_list(list)
   if list.empty?
     []
   else
     data = list.pop
     multi_list(list) << data * 2
   end
 end
 
 def upper_list(list)
   if list.empty?
     []
   else
     data = list.pop
     upper_list(list) << data.upcase
   end
 end
 
 multi_list([1, 2, 3, 4]) # => [2, 4, 6, 8]
 upper_list(['cat', 'dog', 'horse']) # => ["CAT", "DOG", "HORSE"]

これらの制御構造はそっくりで、dataに対する演算のところだけが違う。だからこの演算のところを抽象化できれば汎用メソッドができる。

def mappie(list, op)
   if list.empty?
     []
   else
     data = list.pop
     mappie(list, op) << op[data]
   end
 end

このメソッドを使えば各メソッドは簡潔に書ける。

def multi_list(list)
   mappie(list, lambda { |x| x * 2 })
 end
 
 def upper_list(list)
   mappie(list, lambda { |x| x.upcase })
 end
 
 multi_list([1, 2, 3, 4]) # => [2, 4, 6, 8]
 upper_list(['cat', 'dog', 'horse']) # => ["CAT", "DOG", "HORSE"]

先の例と同様にmappieに渡す手続きもメソッドにしよう。

def operator(op, *args)
   lambda { |x| x.send(op, *args) }
 end

そうすればメソッドは一層簡潔になる

def multi_list(list)
   mappie(list, operator(:*, 2))
 end
 
 def upper_list(list)
   mappie(list, operator(:upcase))
 end
 
 multi_list([1, 2, 3, 4]) # => [2, 4, 6, 8]
 upper_list(['cat', 'dog', 'horse']) # => ["CAT", "DOG", "HORSE"]

メソッドの追加も容易だ。

def reverse_list(list)
   mappie(list, operator(:reverse))
 end
 reverse_list(%w(elppa odnetnin elgoog)) # => ["apple", "nintendo", "google"]

このように高階関数を使えば、任意の抽象手続きを定義できるようになり、その結果下流コードが簡潔になって、それを利用するユーザにやさしいプログラムができる。



blog comments powered by Disqus
ruby_pack8

100円〜で好評発売中!
M'ELBORNE BOOKS