Reputation: 35
Assume the following (non-functioning) code, that takes a predicate such as (==2)
and a list of integers, and drops only the last element of the list that satisfies the predicate:
cutLast :: (a -> Bool) -> [Int] -> [Int]
cutLast a [] = []
cutLast pred (as:a)
| (pred) a == False = (cutLast pred as):a
| otherwise = as
This code does not work, so clearly lists cannot be iterated through in reverse like this. How could I implement this idea? I'm not 100% sure if the code is otherwise correct - but hopefully it gets the idea across.
Upvotes: 0
Views: 599
Reputation: 32309
Borrowing heavily from myself: the problem with this sort of question is that you don't know which element to remove until you get to the end of the list. Once we observe this, the most straightforward thing to do is traverse the list one way then back using foldr
(the second traversal comes from the fact foldr
is not tail-recursive).
The cleanest solution I can think of is to rebuild the list on the way back up, dropping the first element.
cutLast :: Eq a => (a -> Bool) -> [a] -> Either [a] [a]
cutLast f = foldr go (Left [])
where
go x (Right xs) = Right (x:xs)
go x (Left xs) | f x = Right xs
| otherwise = Left (x:xs)
The return type is Either
to differentiate between not found anything to drop from the list (Left
), and having encountered and dropped the last satisfying element from the list (Right
). Of course, if you don't care about whether you dropped or didn't drop an element, you can drop that information:
cutLast' f = either id id . cutLast f
Following the discussion of speed in the comments, I tried swapping out Either [a] [a]
for (Bool,[a])
. Without any further tweaking, this is (as @dfeuer predicted) consistently a bit slower (on the order of 10%).
Using irrefutable patterns on the tuple, we can indeed avoid forcing the whole output (as per @chi's suggestion), which makes this much faster for lazily querying the output. This is the code for that:
cutLast' :: Eq a => (a -> Bool) -> [a] -> (Bool,[a])
cutLast' f = foldr go (False,[])
where
go x ~(b,xs) | not (f x) = (b,x:xs)
| not b = (False,x:xs)
| otherwise = (True,xs)
However, this is 2 to 3 times slower than either of the other two versions (that don't use irrefutable patterns) when forced to normal form.
Upvotes: 3
Reputation: 531055
One simple (but less efficient) solution is to implement cutFirst
in a similar fashion to filter
, then reverse the input to and output from that function.
cutLast pred = reverse . cutFirst . reverse
where cutFirst [] = []
cutFirst (x:xs) | pred x = xs
| otherwise = x : cutFirst xs
Upvotes: 2