find
The final generalization I will discuss is
find :: Foldable t => (a -> Bool) -> t a -> Maybe a
It generalizes elem. Where elem asks whether there exists an element in the
container that is equal to an element we are searching for, find asks whether
there exists an element in the container that satisfies a given predicate. For
example,
>>> find even [1,2,3]
Just 2
>>> find even [1,3,5]
Nothing
find does not only answer True or False depending on whether an element
that matches the predicate is in the container, it also returns the first
element matching the predicate that it finds, wrapped in Just. If none of the
elements in the container match the predicate, then the result is Nothing.
Here's how we implement find:
find :: Foldable t => (a -> Bool) -> t a -> Maybe a
find pred = getFirst . foldMap (\x -> First (if pred x then Just x else Nothing))
newtype First a = First { getFirst :: Maybe a }
instance Semigroup (First a) where
First Nothing <> First y = First y
First x <> _ = First x
instance Monoid (First a) where
mempty = First Nothing
The strategy of find is to map every element x in the container to Nothing
if it does not satisfy the predicate, and to Just x if it does. Then find
reports the first Just value it finds, or Nothing if all values are
Nothing.
To implement this idea of finding the first element that is not Nothing, we
once again need a newtype wrapper around Maybe, called
First.1 We cannot make Maybe itself a monoid here because
there already exists a Monoid instance for Maybe that behaves differently.
The First wrapper implements the idea that "multiplication" of two Maybe
values picks the left value if it is not Nothing, even if the right value also
isn't Nothing. If the left value is Nothing, then multiplication picks the
right value. It is not hard to see that folding a sequence of Maybe values in
this fashion produces Nothing if all the elements in the sequence are
Nothing, and it produces the leftmost non-Nothing value in the sequence if
there is such a value. It also isn't hard to see that First a satisfies the
monoid laws.
Again, here is an application to trees:
>>> tree = Node Empty 5 (Node (Node Empty 3 Empty) 1 (Node Empty 2 Empty))
>>> find even tree
Just 2
>>> find (\x -> x `mod` 4 == 0) tree
Nothing
-
If you're like me, you may wonder why we can't just left-fold the container, as in
find :: Foldable t => (a -> Bool) -> t a -> Maybe a find pred = foldl pickLeft Nothing where pickLeft Nothing y | pred y = Just y | otherwise = Nothing pickLeft x _ = xThis achieves the same effect without all the wand waving using yet another custom monoid. The reason why we don't choose this implementation is that it's less efficient. The tail-recursive implementation of
foldl, which usually leads to more efficient code, hurts us here. In the real implementation offindusingfoldMap, the search stops as soon as we find the first element matching the predicate, thanks to laziness. Withfoldl, the tail recursion means that the final result will be known only after traversing the entire container.We could of course have obtained an equally efficient implementation as the one using
foldMapby usingfoldr, and this would still avoid the monoid magic usingFirst:find :: Foldable t => (a -> Bool) -> t a -> Maybe a find pred = foldr pickLeft Nothing where pickLeft x y | pred x = Just x | otherwise = yThis is even more concise than the implementation using
foldl. The reason why this implementation effectively stops searching the container as soon as it finds an elementxthat matchespredis that the branch with pattern guardpred xsimply returnsJust xwithout inspectingy. Given lazy evaluation, this means thatyis not evaluated at all. Butyis produced by folding the "tail" of the container, so this folding simply doesn't happen.Now, why don't we use this implementation of
findviafoldr. The answer is that, once we have theFirstmonoid, the implementation viafoldMapandFirstis yet more concise than the one usingfoldr. TheFirstmonoid takes some typing to implement, but it is useful in its own right, to be used in functions other thanfind. Thus, this effort amortizes itself. ↩