フィボナッチ数列を素朴な再帰で実装したコードを思い出そう。
my_fib :: Integer -> Integer
my_fib 0 = 0
my_fib 1 = 1
my_fib n = my_fib (n - 2) + my_fib (n - 1)
このコードは美しいが、実際に動かしてみると耐えられないくらい遅いことが分かる。
> :set +s
> my_fib 33
3524578
(12.38 secs, 1100894476 bytes)
遅い理由は、下の図のように同じ計算を何度も繰り返すからだ。
これまで学んだように、フィボナッチ数列は簡単に末尾再帰の形に変更できる。
my_fib_iter :: Integer -> Integer
my_fib_iter a = iter a 0 1
where
iter :: Integer -> Integer -> Integer -> Integer
iter 0 x _ = x
iter n x y = iter (n - 1) y (x + y)
そのコードは無駄な再計算をしないので速い。
> my_fib_iter 33
3524578
(0.00 secs, 512436 bytes)
残念ながら、あらゆる再帰が末尾再帰に簡単に直せる訳ではないし、簡単に高速化できる訳でもない。
再帰の形を変えずに高速化する常套手段はメモ化である。メモ化とは、計算した結果を辞書に登録してキャッシュし、再計算を防ぐことである。動的計画法と呼ばれることもある。
ここからは Haskell 固有の話なので、興味がない方は読まなくても構わない。
Haskell では、純粋な実装を目指したくなる。そのためには、辞書を引数として持ち回せばいい。
import Data.Map (Map)
import qualified Data.Map as M
type Table = Map Integer Integer
my_fib_table :: Integer -> Integer
my_fib_table n = fst (my_fib_tbl n M.empty)
my_fib_tbl :: Integer -> Table -> (Integer,Table)
my_fib_tbl 0 tbl = (0,tbl)
my_fib_tbl 1 tbl = (1,tbl)
my_fib_tbl n tbl = case M.lookup n tbl of
Nothing -> (xn,tbln)
Just x -> (x,tbl)
where
(x1,tbl1) = my_fib_tbl (n - 2) tbl
(x2,tbl2) = my_fib_tbl (n - 1) tbl1
xn = x1 + x2
tbln = M.insert n xn tbl2
育てた辞書を引き継いで行く必要があるので、答えと組にして辞書を返す必要がある。このため、美しさに欠けることは否めない。
実際に動かして、メモ化の効果を見てみよう。
> my_fib_table 33
3524578
(0.00 secs, 548312 bytes)
他の言語と同様に副作用を用いてメモ化を実装すると、以下のようなコードになる。
import Data.IORef
import Data.Map (Map)
import qualified Data.Map as M
my_fib_io :: Integer -> IO Integer
my_fib_io a = do
ref <- newIORef $ M.fromList [(0,0),(1,1)]
fib a ref
where
fib :: Integer -> IORef (Map Integer Integer) -> IO Integer
fib n ref = do
memo <- readIORef ref
case M.lookup n memo of
Just z -> return z
Nothing -> do
x <- fib (n - 2) ref
y <- fib (n - 1) ref
let z = x + y
modifyIORef ref (M.insert n z)
return z
こちらもメモ化の効果を見てみよう。
> my_fib_io 33
3524578
(0.00 secs, 0 bytes)
memoFib の返り値が IO Integer であるのは、Haskeller としてはくやしい。そこで、禁断の unsafePerformIO を使いたくなる。
import Data.IORef
import Data.Map (Map)
import qualified Data.Map as M
import System.IO.Unsafe
my_fib_unsafe :: Integer -> Integer
my_fib_unsafe a = unsafePerformIO $ do
ref <- newIORef $ M.fromList [(0,0),(1,1)]
fib a ref
where
fib :: Integer -> IORef (Map Integer Integer) -> IO Integer
fib n ref = do
memo <- readIORef ref
case M.lookup n memo of
Just z -> return z
Nothing -> do
x <- fib (n - 2) ref
y <- fib (n - 1) ref
let z = x + y
modifyIORef ref (M.insert n z)
return z
ただ、unsafePerformIO を使うと後ろ髪を引かれる思いになることは確かだろう。
幸いにも、純粋にメモ化する方法が知られている。深い説明はこのドリルの範囲を超えるので、今回は単に memoize パッケージで定義されている memoize 関数を使おう。
まず、memoize パッケージをインストールしていただきたい。
% cabal install memoize
memoize を使うには、以下のようにする。
- 再帰関数fooが呼び出す関数を foo から foo_memo に変える。
- foo_memo = memoize foo と定義する。(メモ化する関数(memoizeなど)は、foo の引数の数によって適切な関数を選ぶこと。)
fib のコードには、以下のようにメモ化の実装に変更できる。
import Data.Function.Memoize
my_fib_memo :: Integer -> Integer
my_fib_memo = memoize my_fib
my_fib :: Integer -> Integer
my_fib 0 = 0
my_fib 1 = 1
my_fib n = my_fib_memo (n - 2) + my_fib_memo (n - 1)
実際に動かしてメモ化の効果を確かめてみよう。
> my_fib_memo 33
3524578
(0.00 secs, 518892 bytes)
以下が経路の総数を計算するコードであった。
my_catalan :: Integer -> Integer
my_catalan x = my_cat x x
my_cat :: Integer -> Integer -> Integer
my_cat _ 0 = 1
my_cat m n
| m == n = my_cat m (n - 1)
| otherwise = my_cat m (n - 1) + my_cat (m - 1) n
これをメモ化を使って高速にせよ。
my_catalan_memo :: Integer -> Integer
my_catalan_memo n = my_cat_memo n n
my_cat_memo :: Integer -> Integer -> Integer
my_cat_memo = undefined
my_cat :: Integer -> Integer -> Integer
my_cat = undefined
以下が二分木の総数を計算するコードであった。
my_catalan2 :: Integer -> Integer
my_catalan2 0 = 1
my_catalan2 n = sum (zipWith (*) xs ys)
where
xs = map my_catalan2 [0 .. n - 1]
ys = map my_catalan2 [n - 1, n - 2 .. 0]
これをメモ化を使って高速にせよ。
my_catalan2_memo :: Integer -> Integer
my_catalan2_memo = undefined
my_catalan2 :: Integer -> Integer
my_catalan2 = undefined
以下がコインの両替を解くコードであった。
my_coin :: Integer -> [Integer] -> Integer
my_coin 0 _ = 1
my_coin _ [] = 0
my_coin n (c:cs)
| n < 0 = 0
| otherwise = my_coin n cs + my_coin (n - c) (c:cs)
これをメモ化を使って高速にせよ。
my_coin_memo :: Integer -> [Integer] -> Integer
my_coin_memo = undefined
my_coin :: Integer -> [Integer] -> Integer
my_coin = undefined