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
Monadinstance should have an associatedApplicativeinstance such that theliftMandliftAfunctions coincide. As such, here’s two guidelines:If you’re writing a generic function for any
Monad, useliftM&co. to avoid incompatibility with other functions that have only aMonadconstraint.If you’re working with a specific
Monadinstance that you know has an accompanyingApplicativeinstance, useApplicativeoperators consistently for any definition or subexpression where you don’t needMonadoperations, but avoid mixing them aimlessly.In general, if there is a difference, it will be the other way around.
Applicativeonly supports a static “structure” of the computation, whereasMonadpermits embedded control flow. Consider lists, for instance–withApplicative, 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. WithMonad, 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
ApplicativeandMonadinstances based on zipping infinite streams–Applicativecan simply zip them together in the obvious way, whereasMonadhas to recalculate lots of stuff that it then throws away.So, the final issue is of
liftA2 f x yvs.f <$> x <*> y, or theMonadequivalents. My advice here would be the following guidelines:foo = liftA2 barrather thanfoo 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
liftA4and so on, if you need them.