Reputation: 323
To clarify : I want all the elements but the last n
Reading some answers on stackoverflow I get the impression that using length on lists is inadvisable. So is there better way to do this than take (length xs - n) xs
?
Upvotes: 9
Views: 187
Reputation: 476729
Yes. You first drop n
elements from the list, then walk over the two lists concurrently and when the you hit the end of the list with the dropped version of the list, then you return the list of the "iterator" over the entire list:
takeLast :: Int -> [a] -> [a]
takeLast n ls = go (drop n ls) ls
where go [] ls = ls
go (_:xs) ~(_:ys) = go xs ys
This thus works because the first "iterator" is n steps ahead. So if it hits the end of the list, the second iterator is n steps behind the end of the list.
For example:
Prelude> takeLast 2 [1,4,2,5,1,3,0,2]
[0,2]
Prelude> takeLast 3 [1,4,2,5,1,3,0,2]
[3,0,2]
Prelude> takeLast 4 [1,4,2,5,1,3,0,2]
[1,3,0,2]
Prelude> takeLast 5 [1,4,2,5,1,3,0,2]
[5,1,3,0,2]
We can also drop the last n
elements in a similar way:
dropLast :: Int -> [a] -> [a]
dropLast n ls = go (drop n ls) ls
where go [] _ = []
go (_:xs) ~(y:ys) = y : go xs ys
For example:
Prelude> dropLast 2 [1,4,2,5,1,3,0,2]
[1,4,2,5,1,3]
Prelude> dropLast 3 [1,4,2,5,1,3,0,2]
[1,4,2,5,1]
Prelude> dropLast 4 [1,4,2,5,1,3,0,2]
[1,4,2,5]
Prelude> dropLast 5 [1,4,2,5,1,3,0,2]
[1,4,2]
If we operate on an infinite list, then dropLast
will still yield elements, whereas if we would use take (length ls - n) ls
, it would get stuck in an infinite loop.
We can, as @DanielWagner says use zipWith
for this:
dropLast :: Int -> [a] -> [a]
dropLast n xs = zipWith const xs (drop n xs)
Here we let zipWith
iterate over both lists, and we use const
to each time return the element of the first list.
Upvotes: 8