Reputation: 489
I'm looking for a cleaner way to write a function that adds an element to a list if the list does not contain it. Or otherwise removes it if the list does contain it, I'm using an if
clause now and the function is working.
But I'm trying to find a more haskell-ish way to right this.
This is my code:
removeElemOrAdd :: Eq a => a -> [a] -> [a]
removeElemOrAdd elem list = if (List.elem elem list)
then (filter(\x -> x /= elem) list)
else (elem:list)
Upvotes: 2
Views: 176
Reputation: 153102
I like the other approaches, but don't like that they behave differently than the specification. So here is an approach that:
The basic idea is that we will have a single value which tracks both whether all values so far have been a mismatch as well as the resulting filtered list. The injectNE
operation will perform this operation for a single element of the list, and we will then use foldMap
to expand from one element to the whole input list.
import Data.Monoid
injectNE :: Eq a => a -> a -> (All, [a])
injectNE old new = (All ne, [new | ne]) where
ne = old /= new
removeElemOrAdd :: Eq a => a -> [a] -> [a]
removeElemOrAdd x xs = case foldMap (injectNE x) xs of
(All nex, noxs) -> [x | nex] ++ noxs
In the final pattern, you should read nex
as "no element was equal to x
", and noxs
as "the list without any copies of x
" (get it? "no x
s"? ba-dum-tsh).
It is slightly unfortunate that the spec was written the way it was, though: in particular, one of the selling points of beautiful folding is that its resulting one-pass folds can be friendlier to the garbage collector. But the spec makes that quite hard, because we must traverse the entire input list before deciding what the first element of the result list should be. We can improve the friendliness to the garbage collector significantly by relaxing point (2) above (but leaving (1) and (3)); and moreover the difference is merely swapping the arguments to (++)
, a nicely semantic diff to see in your revision history:
-- <snipped identical code>
removeElemOrAdd x xs = case ... of
... -> noxs ++ [x | nex]
Upvotes: 1
Reputation: 477230
Note: a small ambiguity in your question is what to do when x
already occurs multiple times in the original list. I assumed this won't happen and in case it does, only the first occurrence is removed. Meaning that removeElemOrAdd 2 [4,2,5,2,7]
will result in [4,5,2,7]
. Furthermore it is unspecified where the item should be added. Because it has some advantages, I've opted to do this at the end of the list.
An implementation without using any library methods is the following:
removeElemOrAdd :: Eq a => a -> [a] -> [a]
removeElemOrAdd x (y:ys) | x == y = ys
| otherwise = y : removeElemOrAdd x ys
removeElemOrAdd x _ = [x]
Or a shorter version:
removeElemOrAdd :: Eq a => a -> [a] -> [a]
removeElemOrAdd x = reoa
where reoa (y:ys) | x == y = ys
| otherwise = y : reoa ys
reoa _ = [x]
or an equivalent implementation (see discussion below):
removeElemOrAdd :: Eq a => a -> [a] -> [a]
removeElemOrAdd x = reoa
where reoa (y:ys) | x == y = ys
| otherwise = y : reoa ys
reoa [] = [x]
The function works as follows: in case we are talking about a list with at least one item (y:ys)
, we compare x
with y
and if they are equal, we return ys
: in that case we have removed the element and we are done.
Now in case the two are not equal, we return a list construction (:)
with y
in the head since we need to retain y
and in the tail, we will do a recursive call removeElemOrAdd
with x
and ys
. Indeed: it is possible that there is an x
somewhere in the tail ys
to remove, and furthermore we still need to add x
to the list if it does not occur.
That clause will loop recursively through the list. From the moment it finds an y
such that x == y
it will remove that y
. It is however possible that we reach the end of the list, and still have not found the element. In that case we will call the final clause. Here we know the list is empty (we could have written removeElemOrAdd x []
) but to make the function definition syntactically total, I have opted to use an underscore. We can only reach this state if we have failed to find x
in the list, so then we add it to the tail of the list by returning [x]
.
An advantage of this approach over using the if-then-else
is that this does all tasks at once (checking, removing and adding) making it more efficient.
Another advantage is that this can run on an "infinite" list (like for instance the list of prime numbers). The list is evaluated lazily, so if you want to take the first three items, this function will only check the equality of the first three items.
Upvotes: 5
Reputation: 48611
I'd use a fold to remove all copies:
removeOrAdd x xs = foldr go (bool [x] []) xs False where
go y r found
| y == x = r True
| otherwise = y : r found
To remove just one, a paramorphism seems to be in order:
removeOrAdd x = para go [x] where
go y ys r
| y == x = ys
| otherwise = y : r
Upvotes: 0