`foldr`を使って`dropWhile`を定義する
Haskellの高階関数のfoldr
でdropWhile
を定義せよ、という問題を見たのでやってみる。
foldr
foldr
の定義は以下のとおり:
foldr :: (a -> b -> b) -> b -> [a] -> b foldr f e [] = e foldr f e (x:xs) = f x (foldr f e xs)
「わかってるねんで?foldrがreduce、foldlがfold。ときどきごっちゃになんねん」 「(PythonやClojureの)reduceはfoldlです」
エスカレーターとエレベーター ……ちょっと時々ごっちゃになるねん
— 春日歩(大阪) (@osakabot) 2017年4月5日
とにかくややこしい。
foldr (+) 0 [1, 2, 3] = 1 + (2 + (3 + 0))) foldl (+) 0 [1, 2, 3] = ((0 + 1) + 2) + 3
でどっちにベースの要素e
がつくかでleft/rightを判断するのがよさそう。
dropWhile
dropWhile
を高階関数を使わずに定義するなら:
dropWhile :: (a -> Bool) -> [a] -> [a] dropWhile p [] = [] dropWhile p ys@(x:xs) = if p x then dropWhile p xs else ys
になる。
foldr
でfilter
まず、より簡単なfilter
をfoldr
で定義してみる:
filter :: (a -> Bool) -> [a] -> [a] filter p = foldr f [] where f x xs = if p x then (x:xs) else xs
filter
がなぜ簡単かというと、x
を含むかどうかはp x
の結果のみで決定できるからだ。
逆に言うとdropWhile
が複雑なのはp x
だけでなく、「以前の要素がp
を満たしていたか」も判断基準になってくるから。
foldr
とreverse
でdropWhile
ということで、一番簡単な解決法はreverse
を使ってリストを逆転させてしまうこと。
dropWhile p = reverse . foldr f [] . reverse where f x xs = if p x && (null xs) then xs else x:xs
これでfoldr f e (x:xs)
の結果を返す時、foldr f e xs
の結果を判断材料にできる。
しかし問題も大きい。そもそもO(n)
の処理を三回やっているし、さらに重要なのは遅延評価の強みを消してしまっていること。dropWhile
の第一要素を計算するためにリストすべてをいったん調べなければいけない。とくにundefined
が含まれるpartial listや無限リストなどは扱えなくなってしまう。
foldr
でdropWhile
というわけで本命:
dropWhile p = fst . foldr f ([], []) where f x xst = case p x of True -> (fst xst, ys) False -> (ys, ys) where ys = x : snd xst
foldr
なのでリストの右側から考えて
- 今後左側の要素がすべてpを満たした場合
- 左側にpを満たさない要素があった場合
の結果リストをタプルで持っている。
これで
Prelude> take 5 $ dropWhile even ([2, 4, 6, 7, 8] ++ [1..]) [7,8,1,2,3]
のように無限リストにも対応出来る。
パターンマッチによる失敗例
ちなみにこれは:
dropWhile p = fst . foldr f ([], []) where f x (a, b) = case p x of True -> (a, c) False -> (c, c) where c = x : b
としてしまうと無限リストなどの場合stack overflow
を起こしてうまくいかない。なんとなく直観的にはわかるのだが説明はできない・・・