Lately I've been writing FFI code that returns a data structure in the IO monad. For example:
peek p = Vec3 <$> (#peek aiVector3D, x) p
<*> (#peek aiVector3D, y) p
<*> (#peek aiVector3D, z) p
Now I can think of four nice ways to write that code, all closely related:
peek p = Vec3 <$> io1 <*> io2 <*> io3
peek p = liftA3 Vec3 io1 io2 io3
peek p = return Vec3 `ap` io1 `ap` io2 `ap` io3
peek p = liftM3 Vec3 io1 io2 io3
Notice that I'm asking about monadic code that doesn't require anything beyond what Applicative
provides. What is the preferred way to write this code? Should I use Applicative
to emphasize what the code does, or should I use Monad
because it might (?) have optimizations over Applicative
?
The question is slightly complicated by the fact that there are only [liftA..liftA3]
and [liftM..liftM5]
but I have several records with more than three or five members, so if I decide to go with lift{A,M}
I lose some consistency because I would have to use a different method for the larger records.
The first thing to remember is that this is slightly more complicated than it ought to be--any Monad
instance should have an associated Applicative
instance such that the liftM
and liftA
functions coincide. As such, here's two guidelines:
If you're writing a generic function for any Monad
, use liftM
&co. to avoid incompatibility with other functions that have only a Monad
constraint.
If you're working with a specific Monad
instance that you know has an accompanying Applicative
instance, use Applicative
operators consistently for any definition or subexpression where you don't need Monad
operations, but avoid mixing them aimlessly.
Should I use
Applicative
to emphasize what the code does, or should I useMonad
because it might (?) have optimizations overApplicative
?
In general, if there is a difference, it will be the other way around. Applicative
only supports a static "structure" of the computation, whereas Monad
permits embedded control flow. Consider lists, for instance--with Applicative
, all you can do is generate all possible combinations and transform each one--the number of result elements is determined entirely by the number of elements in each input. With Monad
, you can generate different numbers of elements at each step based on input elements, allowing you to filter or expand arbitrarily.
A more potent example is is the Applicative
and Monad
instances based on zipping infinite streams--Applicative
can simply zip them together in the obvious way, whereas Monad
has to recalculate lots of stuff that it then throws away.
So, the final issue is of liftA2 f x y
vs. f <$> x <*> y
, or the Monad
equivalents. My advice here would be the following guidelines:
foo = liftA2 bar
rather than foo x y = bar <$> x <*> y
--it's shorter and more clearly expresses what you're doing.And finally, on the issue of consistency, there's no reason you couldn't simply define your own liftA4
and so on, if you need them.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With