Schwartz変換 on Haskell
リストのリストを受け取って、リストの長さでソートするsortByLengthを書いてみる。
まずは素直に書いてみる。
import List; sortByLength :: [[a]] -> [[a]] sortByLength xs = sortBy cmp xs cmp :: [a] -> [a] -> Ordering cmp x y | length x < length y = LT | length x == length y = EQ | otherwise = GT
sortByを使うためにListをimportしている。SortByの型は(a -> a -> Ordering) -> [a] -> [a]。つまり、第一引き数に(a -> a -> Ordering)な型の関数、第二引き数に[a]をとり、[a]を返す関数。ここでは第一引き数に関数cmp,第二引き数にxsを渡している。
cmpは、[a],[a]を取りOrdering型を返す関数。ここでは二つの引き数(List)の長さを比べている。
問題点
cmpのパターンマッチングの中で何度もlengthを呼び出している。cmpはソート中に二値を比較するたびに適応されるので、ここで何度もlengthを呼び出すのはCPU効率が悪い(逆にShwartz変換はメモリ空間を犠牲にしているので、適材適所で使うべきだろうけど)。というわけで、Shwartz変換を使ってみます。
Shwartz変換バージョン
import List; sortByLength xs = map untupplize $ sortBy cmp $ map tupplize xs tupplize :: [a] -> ([a],Int) tupplize xs = (xs, length xs) untupplize :: ([a],Int) -> [a] untupplize (xs,_) = xs; cmp :: ([a], Int) -> ([a], Int) -> Ordering cmp (_,x) (_,y) | x < y = LT | x == y = EQ | otherwise = GT
まず、tupplizeはリストを与えると(リスト,リストの長さ)という形のタプルを返す関数。これをmapに適応して、[リスト,リスト,リスト..]というリストを[(リスト,長さ),(リスト,長さ)..]というリストに変換する。
そのリストに対してcmpを適応する。cmpではパターンマッチで(リスト,長さ)から長さの部分をx,yという変数に束縛して、cmpではx,yの長さをチェックするだけ。毎回lengthを呼ばなくなったので効率が良くなった。
あとは、tupplizeされた状態の各要素をuntupplizeしてもとにもどしてあげれば良い。これはtupplizeの逆をやってるだけのなので割愛。
スコープを短く
tupplizeもuntupplizeもcmpも、ソートのためにしか使わないのだから、その中だけのスコープに限定したい
import List sortByLength xs = map untupplize $ sortBy cmp $ map tupplize xs where tupplize xs = (xs, length xs) cmp (_,x) (_,y) | x < y = LT | x == y = EQ | otherwise = GT untupplize (xs,_) = xs
名前つけるまでもないものにはλ使う
tupplizeとuntupplize程度の処理、わざわざ名前付き関数にする必要もなし
import List sortByLength xs = map (\(xs,_) -> xs) $ sortBy cmp $ map (\x -> (xs, length xs)) xs where cmp (_,x) (_,y) | x < y = LT | x == y = EQ | otherwise = GT
lambdaにしたら素直に後ろから読んでいくだけでわかりやすいコードになった。やったねたえちゃん!
関数合成してみる
sortByLengthの引き数がそのままmapの引き数になっててださいので、ポイントフリースタイルで書き直してみる
import List; sortByLength :: [[a]] -> [[a]] sortByLength = map (\(xs,_) -> xs) . sortBy cmp . map (\xs -> (xs, length xs)) where cmp (_,x) (_,y) | x < y = LT | x == y = EQ | otherwise = GT
Haskellでは . で関数を繋ぐことで関数合成ができる。g(f(x))が、g . fで表すことができる、という感じ。
このあたりは結構新しい概念なのでていねいに追う。
まずは、
map (\(xs,_) -> xs) . sortBy cmp . map (\xs -> (xs, length xs))
を詳細に観ていく。評価される優先順位がわかりやすい感じに書きかえる
(map (\(xs,_) -> xs)) . (sortBy cmp) . (map (\xs -> (xs, length xs)))
つまり、(map (\(xs,_) -> xs))で生まれる関数と(sortBy cmp)で生まれる関数と(map (\xs -> (xs, length xs)))で生まれる関数を合成している。
まずは一番最初 (map (\(xs,_) -> xs)) を見る。mapの型は (a -> b) -> [a] -> [b]で、ここでは引き数にラムダ式をひとつ与えている。これによって、mapがカリー化されるので、(map (\(xs,_) -> xs))が返すのは、型が[a] -> [b]な関数となる。
つぎに(sortBy comp)を見る。先ほどの例のおなじように考えて、sortBy の型が (a -> a -> Bool) -> [a] -> [a]なので、(sortBy cmp)は [a] -> [a]な型の関数となる。
最後のmapも同様。
これらのみっつの関数を合成したものを、sortByLengthに束縛している。
今日はここまで。