filterM :: Applicative m => (a -> m Bool) -> [a] -> m [a]
from this I can tell you conclusively what it does: it takes a monadic function that returns a Bool after performing some "action" as well as a list of values; it then performs this action on each element and look at whether the result is True or False; when True, keep it otherwise discard it.
Now this is already a lot of information on what filterM does. But to use it in an action piece of code, you need to supply your own understanding of what the "action" is in this context. It does not matter whether or not you understand the implementation details of the specific Monad instance. You just need to understand its behavior: with the Reader monad, you know the action in question is just reading a piece of value from a "hidden" environment, so your filtering function has access to this environment when it determines whether or not to keep or retain an element; with the State monad, you know the action in question can possibly modify this environment as well; with the Maybe monad, you know the filtering action can say I don't know and that would result in the entire result to be Nothing as well.
Your argument of functions like filterM being a leaky abstraction is akin to saying, a Java interface is a leaky abstraction because you can't instantiate an interface (you need a class) so you need to understand both the interface as well as the class being used that implements the interface. Instead, think about it, it's just how the nature of combining orthogonal (de-coupled) things requires you to have an understanding of both of the pieces you are combining.
"Conclusively" is probably a little strong. You're relying not just on the type, but also the name - the type alone isn't enough here, even without resorting to bottom, and even when requiring every piece be meaningfully used. For instance, with the same type we can write:
doubleIf :: Applicative m => (a -> m Bool) -> [a] -> m [a]
doubleIf f values = case values of
[] -> pure []
x:xs -> prepend x <$> f x <*> doubleIf f xs
where
prepend :: a -> Bool -> [a] -> [a]
prepend x True xs = x:x:xs
prepend x False xs = x:xs
Maybe a simpler way to illustrate this is two different functions built off of filter:
`keep`: Given a predicate and a collection, filter the collection to retain only items for which predicate evaluates to true.
`discard`: Given a predicate and a collection, filter the collection to discard items for which the predicate evaluates to false.
These are two very reasonable functions that would have the exact same signature, would be appropriate for the same arguments, but have two different behaviors!
> I can tell you conclusively what it does: it takes a monadic function that returns a Bool after performing some "action" as well as a list of values; it then performs this action on each element and look at whether the result is True or False; when True, keep it otherwise discard it.
The function below matches the signature of filterM for List monad, but does not do anything remotely similar to what you described.
foo :: (Int -> [Bool]) -> [Int] -> [[Int]]
foo f xs =
case bool of
[True] -> []
_ -> [xs]
where bool = f $ length xs
This is a contrived example, but I think a large part of your guess on what filterM does comes from the word "filter" in its name. This isn't any different from other mainstream programing language, so it is not a ding on Haskell.
That's actually what I meant: using both the name of the type to figure out what it does. I'm not claiming there's a single implementation given the type. Indeed I don't think that's possible in any signature that's not totally just type variables; here we have Bool and [] as base types so there are operations on them that the function can perform by baking in knowledge of these two types.
Now this is already a lot of information on what filterM does. But to use it in an action piece of code, you need to supply your own understanding of what the "action" is in this context. It does not matter whether or not you understand the implementation details of the specific Monad instance. You just need to understand its behavior: with the Reader monad, you know the action in question is just reading a piece of value from a "hidden" environment, so your filtering function has access to this environment when it determines whether or not to keep or retain an element; with the State monad, you know the action in question can possibly modify this environment as well; with the Maybe monad, you know the filtering action can say I don't know and that would result in the entire result to be Nothing as well.
Your argument of functions like filterM being a leaky abstraction is akin to saying, a Java interface is a leaky abstraction because you can't instantiate an interface (you need a class) so you need to understand both the interface as well as the class being used that implements the interface. Instead, think about it, it's just how the nature of combining orthogonal (de-coupled) things requires you to have an understanding of both of the pieces you are combining.