(Real World Haskell—実戦で学ぶ関数型言語プログラミング 13.6 データとしての関数を利用する)
Haskell においてリストを連結するための関数 ++ は下に示すように定義されています。xs ++ ys とした場合はリスト xs の長さに比例して再帰する回数が増加します。
(++) :: [a] -> [a] -> [a] (++) [] ys = ys (++) (x:xs) ys = x : xs ++ ys
以下に示したように a ++ b ++ c ++ d と連結すれば再帰するのは 30回ですが、
a = "0123456789" b = "0123456789" c = "0123456789" d = "0123456789"
以下のように連結を繰り返すと、再帰する回数は 60回になってしまいます。
ls1 = a ++ b ls2 = ls1 ++ c ls3 = ls2 ++ d
(a++) のように部分適用した関数を作り、 f1 = (a++).(b++) のように関数を合成すれば、 a ++ b ++ c ++ d と書いたときと同じ再帰回数になります。
a1= "0123456789" b1= "0123456789" c1= "0123456789" d1= "0123456789" f1 :: String -> String f1 = (a1++).(b1++) f2 :: String -> String f2 = f1 .(c1++) f3 :: String -> String f3 = f2 .(d1++) f3 [] -- => 連結した文字列になる -- => "0123456789012345678901234567890123456789"
以下が数えるためのソースです。(+#+)で連結する度に再帰した回数を数えて末尾にセットしています。
-- toTLS "0123456789" -- => [('0',0),('1',0),('2',0),('3',0),('4',0),('5',0),('6',0),('7',0),('8',0),('9',0)] toTLS :: (Num a1) => [a] -> [(a, a1)] toTLS [] = [] toTLS (x:xs) = (x,0):toTLS xs (+#+) :: (Num b) => [(a, b)] -> [(a, b)] -> [(a, b)] (+#+) xs ys = lsJoin xs ys $ getCnt xs + getCnt ys lsJoin :: (Num b) => [(a, b)] -> [(a, b)] -> b -> [(a, b)] lsJoin [] ys n = setToLast ys n lsJoin (x:xs) ys n = x :lsJoin xs ys (n+1) -- setToLast [('0',0),('1',0),('2',0),('3',0)] 9999 -- => [('0',0),('1',0),('2',0),('3',9999)] setToLast :: [(a, b)] -> b -> [(a, b)] setToLast ls n = reverse $(fst(head rls),n):tail rls where rls = reverse ls infixr 5 +#+ getCnt = snd.head.reverse a=toTLS "0123456789" b=toTLS "0123456789" c=toTLS "0123456789" d=toTLS "0123456789" -- 一度に連結した場合 cnt1 = getCnt $ a +#+ b +#+ c +#+ d -- => 30 -- 連結した文字列にさらに加えていく場合 ls1 = a +#+ b -- getCnt ls1 => 10 -- a の長さ:10 ls2 = ls1 +#+ c -- getCnt ls2 => 30 -- ls1 の長さ:20 + ls1を作るコスト:10 ls3 = ls2 +#+ d -- getCnt ls3 => 60 -- ls2 の長さ:30 + ls2を作るコスト:30 -- 部分適用の関数を合成していく場合。 f1 = (a+#+).(b+#+) f2 = f1 . (c +#+) l3 = f2 d cnt3 = getCnt l3 -- => 30
(++) は右結合ですから、a ++ (b ++ (c ++ d)) と処理されます。
試しに左結合にしてみると、 a +#+ b +#+ c +#+ d の再帰回数は60回になります。
infixl 5 +#+ getCnt $ a +#+ b +#+ c +#+ d -- => 60
(++) が右結合なのは、 a ++ b ++ c ++ d と書いたとき、演算コストがリスト長の2乗に比例するのを避けるためです。