高階関数はコードをユーザフレンドリーにする
「計算機プログラムの構造と解釈」の図形言語のところでつまずいているので、もう一度高階関数に対する自分の理解を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