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 _ = x
This 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 offind
usingfoldMap
, 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
foldMap
by 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 = y
This 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 elementx
that matchespred
is that the branch with pattern guardpred x
simply returnsJust x
without inspectingy
. Given lazy evaluation, this means thaty
is not evaluated at all. Buty
is produced by folding the "tail" of the container, so this folding simply doesn't happen.Now, why don't we use this implementation of
find
viafoldr
. The answer is that, once we have theFirst
monoid, the implementation viafoldMap
andFirst
is yet more concise than the one usingfoldr
. TheFirst
monoid 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. ↩