Here is filter
and its monadic cousin filterM
:
This is my attempt to understand the relevance of the differences between these two functions. Please leave a comment to let me know about anything I’ve misunderstood. :)
Filtering with Maybe
We can use the non-monadic filter
to get a list of even numbers, or use fmap
to apply this filter inside a monad (or any functor):
The filterM
form gives us something fmap
and filter
can’t, and that’s allowing us to interact with a monadic context.
For example, say we want to filter out even numbers from an [Int]
, but we don’t want to return anything if any number is less than or equal to 0 (perhaps we want to call a function that only works with positive integers).
filter
takes a predicate (a -> Bool)
which can only evaluate to True
or False
for each element – there is no way for it to interact with a monadic environment, even if it is mapped into one. Nor can fmap
for that matter, as it works with functions (a -> b)
and preserves the structure it is mapping over.
filterM
doesn’t have this restriction due to its (a -> m Bool)
argument, which in this case can produce Just True
, Just False
, or stop the computation by evaluating to Nothing
.
Parser example
We can apply a similar filter function to a monadic parser.
If we have an instance of a Parser [Int]
that parses comma-separated numbers, we can filter even numbers like in our previous example:
Now we want to filter even numbers from our parser, but we want to stop parsing if we get 0 or a negative number. We can use filterM
to construct a new parser from our csv
parser that does this:
State example
Another example from Tony Morris’s Haskell exercises is getting a distinct list of elements using the State monad with a Data.Set
to keep track of which elements have been seen:
filterM
ensures that the state (the contents of the Data.Set
) is preserved as each element in the source list is traversed.
IO
example
Say we want to filter a list of files paths to only include files that exist. This requires checking the file system, which requires a FilePath -> IO Bool
function called doesFileExist
:
Conclusion
What hadn’t really twigged for me previously is that filterM
’s first argument (a -> m Bool)
lets us do standard filtering like filter
, but gives us the additional option to keep some monadic effect while filtering, such as canceling a computation with Nothing
or by failing parsing, or updating some state. In contrast, mapping filter
inside a monad does not give us this ability to interact with that effect1.
I think this limitation can also be a big advantage. Using
fmap
means we know we aren’t going to interacting with a monadic context, so we can safely transform anm a
without being concerned about ill effects.↩