In your first example, you want to compose a function f :: a -> m a
with itself. Let's pick a specific a
and m
for the sake of discussion: Int -> Maybe Int
.
Composing functions that can have errors
Okay, so as you point out, you cannot just do f (f x)
. Well, let's generalize this a little more to g (f x)
(let's say we're given a g :: Int -> Maybe String
to make things more concrete) and look at what you do need to do case-by-case:
f :: Int -> Maybe Int
f = ...
g :: Int -> Maybe String
g = ...
gComposeF :: Int -> Maybe String
gComposeF x =
case f x of -- The f call on the inside
Nothing -> Nothing
Just x' -> g x' -- The g call on the outside
This is a bit verbose and, like you said, we would like to reduce the repetition. We can also notice a pattern: Nothing
always goes to Nothing
, and the x'
gets taken out of Just x'
and given to the composition. Also, note that instead of f x
, we could take any Maybe Int
value to make things even more general. So let's also pull our g
out into an argument, so we can give this function any g
:
bindMaybe :: Maybe Int -> (Int -> Maybe String) -> Maybe String
bindMaybe Nothing g = Nothing
bindMaybe (Just x') g = g x'
With this helper function, we can rewrite our original gComposeF
like this:
gComposeF :: Int -> Maybe String
gComposeF x = bindMaybe (f x) g
This is getting pretty close to g . f
, which is how you would compose those two functions if there wasn't the Maybe
discrepancy between them.
Next, we can see that our bindMaybe
function doesn't specifically need Int
or String
, so we can make this a little more useful:
bindMaybe :: Maybe a -> (a -> Maybe b) -> Maybe b
bindMaybe Nothing g = Nothing
bindMaybe (Just x') g = g x'
All we had to change, actually, was the type signature.
This already exists!
Now, bindMaybe
actually already exists: it is the >>=
method from the Monad
type class!
(>>=) :: Monad m => m a -> (a -> m b) -> m b
If we substitute Maybe
for m
(since Maybe
is an instance of Monad
, we can do this) we get the same type as bindMaybe
:
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
Let's take a look at the Maybe
instance of Monad
to be sure:
instance Monad Maybe where
return x = Just x
Nothing >>= f = Nothing
Just x >>= f = f x
Just like bindMaybe
, except we also have an additional method that lets us put something into a "monadic context" (in this case, this just means wrapping it in a Just
). Our original gComposeF
looks like this:
gComposeF x = f x >>= g
There is also =<<
, which is a flipped version of >>=
, that lets this look a little more like the normal composition version:
gComposeF x = g =<< f x
There is also a builtin function for composing functions with types of the form a -> m b
called <=<
:
(<=<) :: Monad m => (b -> m c) -> (a -> m b) -> a -> m c
-- Specialized to Maybe, we get:
(<=<) :: (b -> Maybe c) -> (a -> Maybe b) -> a -> Maybe c
Now this really looks like function composition!
gComposeF = g <=< f -- This is very similar to g . f, which is how we "normally" compose functions
When we can simplify even more
As you mentioned in your question, using do
notation to convert simple division function to one which properly handles errors is a bit harder to read and more verbose.
Let's look at this a little more carefully, but let's start with a simpler problem (this is actually a simpler problem than the one we looked at in the first sections of this answer): We already have a function, say that multiplies by 10, and we want to compose it with a function that gives us a Maybe Int
. We can immediately simplify this a little bit by saying that what we really want to do is take a "regular" function (such as our multiplyByTen :: Int -> Int
) and we want to give it a Maybe Int
(i.e., a value that won't exist in the case of an error). We want a Maybe Int
to come back too, because we want the error to propagate.
For concreteness, we will say that we have some function maybeCount :: String -> Maybe Int
(maybe divides 5 by the number times we use the word "compose" in the String
and rounds down. It doesn't really matter what it specifically though) and we want to apply multiplyByTen
to the result of that.
We'll start with the same kind of case analysis:
multiplyByTen :: Int -> Int
multiplyByTen x = x * 10
maybeCount :: String -> Maybe Int
maybeCount = ...
countThenMultiply :: String -> Maybe Int
countThenMultiply str =
case maybeCount str of
Nothing -> Nothing
Just x -> multiplyByTen x
We can, again, do a similar "pulling out" of multiplyByTen
to generalize this further:
overMaybe :: (Int -> Int) -> Maybe Int -> Maybe Int
overMaybe f mstr =
case mstr of
Nothing -> Nothing
Just x -> f x
These types also can be more general:
overMaybe :: (a -> b) -> Maybe a -> Maybe b
Note that we just needed to change the type signature, just like last time.
Our countThenMultiply
can then be rewritten:
countThenMultiply str = overMaybe multiplyByTen (maybeCount str)
This function also already exists!
This is fmap
from Functor
!
fmap :: Functor f => (a -> b) -> f a -> f b
-- Specializing f to Maybe:
fmap :: (a -> b) -> Maybe a -> Maybe b
and, in fact, the definition of the Maybe
instance is exactly the same as well. This lets us apply any "normal" function to a Maybe
value and get a Maybe
value back, with any failure automatically propagated.
There is also a handy infix operator synonym for fmap
: (<$>) = fmap
. This will come in handy later. This is what it would look like if we used this synonym:
countThenMultiply str = multiplyByTen <$> maybeCount str
What if we have more Maybes
?
Maybe we have a "normal" function of multiple arguments that we need to apply to multiple Maybe
values. As you have in your question, we could do this with Monad
and do
notation if we were so inclined, but we don't actually need the full power of Monad
. We need something in between Functor
and Monad
.
Let's look the division example you gave. We want to convert g'
to use the safeDivide :: Int -> Int -> Either ArithmeticError Int
. The "normal" g'
looks like this:
g' i j k = i / k + j / k
What we would really like to do is something like this:
g' i j k = (safeDivide i k) + (safeDivide j k)
Well, we can get close with Functor
:
fmap (+) (safeDivide i k) :: Either ArithmeticError (Int -> Int)
The type of this, by the way, is analogous to Maybe (Int -> Int)
. The Either ArithmeticError
part just tells us that our errors give us information in the form of ArithmeticError
values instead of only being Nothing
. It could help to mentally replace Either ArithmeticError
with Maybe
for now.
Well, this is sort of like what we want, but we need a way to apply the function "inside" the Either ArithmeticError (Int -> Int)
to Either ArithmeticError Int
.
Our case analysis would look like this:
eitherApply :: Either ArithmeticError (Int -> Int) -> Either ArithmeticError Int -> Either ArithmeticError Int
eitherApply ef ex =
case ef of
Left err -> Left err
Right f ->
case ex of
Left err' -> Left err'
Right x -> Right (f x)
(As a side note, the second case
can be simplified with fmap
)
If we have this function, then we can do this:
g' i j k = eitherApply (fmap (+) (safeDivide i k)) (safeDivide j k)
This still doesn't look great, but let's go with it for now.
It turns out eitherApply
also already exists: it is (<*>)
from Applicative
. If we use this, we can arrive at:
g' i j k = (<*>) (fmap (+) (safeDivide i k)) (safeDivide j k)
-- This is the same as
g' i j k = fmap (+) (safeDivide i k) <*> safeDivide j k
You may remember from earlier that there is an infix synonym for fmap
called <$>
. If we use that, the whole thing looks like:
g' i j k = (+) <$> safeDivide i k <*> safeDivide j k
This looks strange at first, but you get used to it. You can think of <$>
and <*>
as being "context sensitive whitespace." What I mean is, if we have some regular function f :: String -> String -> Int
and we apply it to normal String
values we have:
firstString, secondString :: String
result :: Int
result = f firstString secondString
If we have two (for example) Maybe String
values, we can apply f :: String -> String -> Int
, we can apply f
to both of them like this:
firstString', secondString' :: Maybe String
result :: Maybe Int
result = f <$> firstString' <*> secondString'
The difference is that instead of whitespace, we add <$>
and <*>
. This generalizes to more arguments in this way (given f :: A -> B -> C -> D -> E
):
-- When we apply normal values (x :: A, y :: B, z :: C, w :: D):
result :: E
result = f x y z w
-- When we apply values that have an Applicative instance, for example x' :: Maybe A, y' :: Maybe B, z' :: Maybe C, w' :: Maybe D:
result' :: Maybe E
result' = f <$> x' <*> y' <*> z' <*> w'
A very important note
Note that none of the above code mentioned Functor
, Applicative
, or Monad
. We just used their methods as though they are any other regular helper functions.
The only difference is that these particular helper functions can work on many different types, but we don't even have to think about that if we don't want to. If we really want to, we can just think of fmap
, <*>
, >>=
etc in terms of their specialized types, if we are using them on a specific type (which we are, in all of this).