SchemeとRubyで高階関数を学ぼう
「計算機プログラムの構造と解釈」という本を図書館で借りた プログラマー必読の名著で Amazonによればこれ1冊でコンピュータのすべてがわかるらしい 主著者はLISPの一方言であるSchemeという言語を作った人で 本書もSchemeで書かれている Ruby以外知らない自分には新鮮で大変勉強になる
最初の方に高階手続きによる抽象という章がある 高階手続きというのは手続きを扱う手続き つまり手続きの引数として手続きを取ったり 手続きを値として返す手続きのことだ
Rubyにもそういう表現力があるので 同じことをRubyでもできるか試してみよう
高階手続き
最初に以下の似たような3つの手続きを考える
- aからbまでの整数の和を計算する(sum_integers)
- 与えられた範囲の整数の三乗の和を計算する(sum_cubes)
- 級数の項の並びの和を計算する(pi_sum)
これらをSchemeで表現すると通常次のようになる
(define (sum_integers a b)
(if (> a b)
0
(+ a (sum_integers (+ a 1) b))))
(define (sum_cubes a b)
(if (> a b)
0
(+ (sum_cubes (+ a 1) b))))
(define (pi_sum a b)
(if (> a b)
0
(+ (/ 1.0 (* a (+ a 2))) (pi_sum (+ a 4) b))))
defineの後に手続き名と引数をカッコで括って置き 続けて手続きの実体を置く
Rubyでは以下のように表現される
def sum_integers(a, b)
if a > b
0
else
a + sum_integers(a+1, b)
end
end
def sum_cubes(a, b)
if a > b
0
else
cube(a) + sum_cubes(a+1, b)
end
end
def pi_sum(a, b)
if a > b
0
else
1.0/(a*(a+2)) + pi_sum(a+4, b)
end
end
見ての通り3つの手続きの実体は共通のパターンを持っている 加算するaの関数とaの次の値を計算する関数が違うだけだ これらをterm, nextと記号化し 3つの手続きで共通する総和sumという概念を表現する Schemeでは以下のようになる
(define (sum term a next b)
(if (> a b)
0
(+ (term a)
(sum term (next a) next b))))
手続きsumにはtermとnextがその引数として加わる その取り扱いに特別なものはない Schemeではこのような手続きを引数として取る手続きが 自然な形で書ける
sum手続きを使って先の3つの手続きを完成させるには 以下のように手続きを加える
(define (inc n) (+ n 1))
(define (sum_cubes a b)
(sum cube a inc b))
(sum_cubes 1 10)
3025
(define (identity x) x)
(define (sum_integers a b)
(sum identity a inc b))
(sum_integers 1 10)
55
(define (pi_sum a b)
(define (pi_term x)
(/ 1.0 (* x (+ x 2))))
(define (pi_next x)
(+ x 4))
(sum pi_term a pi_next b))
(* 8 (pi_sum 1 1000))
3.139592655589783
sum_cubesとsum_integersのためにincを定義し pi_sumのためにpi_termとpi_nextを定義している pi_term,pi_nextは汎用性が低いからpi_sumの中で定義している それぞれに固有の手続きをsumのtermとnextに渡すことで 共通のsum手続きを用いて3つの異なる演算が実現できる
これをRubyで表現してみよう まずはsumメソッドから
def sum(term, a, _next, b)
if a > b
0
else
term.call(a) + sum(term, _next.call(a), _next, b)
end
end
Schemeと異なりRubyでは手続きはオブジェクトではない だけど手続きをオブジェクトにすることはできる この手続きオブジェクトの起動にはcallが必要だ
このsumメソッドを使って先の3つの手続きを完成させよう
def cube(n)
n**3
end
def inc(n)
n + 1
end
def sum_cubes(a, b)
sum(method(:cube), a, method(:inc), b)
end
sum_cubes(1, 10) # => 3025
def identity(n)
n
end
def sum_integers(a, b)
sum(method(:identity), a, method(:inc), b)
end
sum_integers(1, 10) # => 55
def pi_sum(a, b)
def pi_term(x)
1.0/(x*(x+2))
end
def pi_next(x)
x + 4
end
sum(method(:pi_term), a, method(:pi_next), b)
end
8 * pi_sum(1, 1000) # => 3.13959265558978
Rubyではメソッドをオブジェクト化するのに Object#methodメソッドを使いメソッド名をシンボルで渡す pi_sumメソッドのように メソッド定義の中にメソッド定義をした場合 Rubyでは外側のメソッドの呼び出し時に 内側のメソッドが定義される
lambda
Schemeに戻ろう 先の高階手続きにおいてその引数として使うためだけに pi_termやpi_nextの手続きを定義するのは煩わしい このような場合lambdaが使える lambdaを使ったpi_sum手続きは以下のようになる
(define (pi_sum a b)
(sum (lambda (x) (/ 1.0 (* x (+ x 2))))
a
(lambda (x) (+ x 4))
b))
;元のpi_sum手続き
(define (pi_sum a b)
(define (pi_term x)
(/ 1.0 (* x (+ x 2))))
(define (pi_next x)
(+ x 4))
(sum pi_term a pi_next b))
元のpi_sum手続きにおける sumの引数pi_term, pi_nextに直接 手続きを埋め込んでいるのがわかる
Rubyもlambdaを持っているので 同様のことができる
def pi_sum(a, b)
sum(lambda { |x| 1.0/(x*(x+2)) }, a, lambda { |x| x + 4 }, b)
end
#元のpi_sumメソッド
def pi_sum(a, b)
def pi_term(x)
1.0/(x*(x+2))
end
def pi_next(x)
x + 4
end
sum(method(:pi_term), a, method(:pi_next), b)
end
Rubyでは手続きはブロックで表現する
メソッドに渡す手続きが一つなら Rubyではブロックが使える ブロックの起動はyieldを呼ぶ
def sum(a, _next, b)
if a > b
0
else
yield(a) + sum(_next.call(a), _next, b){ |a| yield a }
end
end
inc = lambda { |i| i + 1 }
pi_next = lambda { |i| i + 4 }
sum(1, inc, 10){ |i| i } # => 55
sum(1, inc, 10){ |i| i**3 } # => 3025
8 * sum(1, pi_next, 1000){ |i| 1.0/(i*(i+2)) } # => 3.13959265558978
let
Schemeではlambdaは局所変数を作り出すためにも使われる
という関数を計算したい場合 これは以下のように書ける
手続きfにおいてこの途中のa,bも束縛しておきたい そのようなときは補助手続きを定義する
(define (f x y)
(define (f_helper a b)
(+ (* x (square a))
(* y b)
(* a b)))
(f_helper (+ 1 (* x y))
(- 1 y)))
もちろんlambdaが使える
(define (f x y)
((lambda (a b)
(+ (* x (square a))
(* y b)
(* a b)))
(+ 1 (* x y))
(- 1 y)))
更にletというものが使える letを使えば最初にa,bを定義できる
(define (f x y)
(let ((a (+ 1 (* x y)))
(b (- 1 y)))
(+ (* x (square a))
(* y b)
(* a b))))
これら3つのRubyの等価コードは以下のようになる
def f(x, y)
def f_helper(a, b, x, y)
x*a**2 + y*b + a*b
end
f_helper(1+(x*y), 1-y, x, y)
end
def f(x, y)
f_helper = lambda do |a,b|
x*a**2 + y*b + a*b
end
a = 1+(x*y)
b = 1-y
f_helper.call(a, b)
end
def f(x, y)
a = 1+(x*y)
b = 1-y
x*a**2 + y*b + a*b
end
一番上のコードにおいて Rubyではローカル変数はメソッドを透過できないので 明示的に引数で受け渡す必要がある
関連記事:Rubyのブロックはメソッドに対するメソッドのMix-inだ!
ジェラルド・ジェイ サスマン, ジュリー サスマン, and ハロルド エイブルソン (追記:2009/2/1)タイトルを「RubyでSchemeの高階関数を学ぼう」から「SchemeでRubyの高階関数を学ぼう」に変えました (追記:2009/2/5) タイトルを「SchemeでRubyの高階関数を学ぼう」から「SchemeとRubyで高階関数を学ぼう」に変えました
blog comments powered by Disqus