高階関数はコードをユーザフレンドリーにする
「計算機プログラムの構造と解釈」の
図形言語のところでつまずいているので
もう一度高階関数に対する自分の理解を
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"]
このように高階関数を使えば
任意の抽象手続きを定義できるようになり
その結果下流コードが簡潔になって
それを利用するユーザにやさしいプログラムができる