I figured out encoding of 2 more lens-like constructions in the “van Laarhoven” (functor quantification) style used by Haskell’s Control.Lens
. The first is monadic lenses, which allow your backwards pass to have side effects from a monad, and the second is the far more subtle monadic optics, which allow effects on both the forwards and backwards passes. I was originally surprised to find neither of these in the monolith-with-included-batteries that is Control.Lens
. To summarise my findings:
- Monadic lenses are the optic for right modules of the monad
- Monadic optics are the optic for right modules of the monad that additionally the monad distributes over
Source code for this post can be found here. As usual I didn’t prove correctness of either of these, they’re only conjectures that seem to check out in code. In this post I’ll cover the first one, and then part II will be about the second, which is a lot more complicated. Let’s dive in.
In this post we’re working with a fixed monad, . A monadic lens from to is, by definition, a pair of a “forwards pass” or “get” function and a “backwards pass” or “put” function . This definition first appeared in section 2.3 of the paper Reflections on Monadic Lenses.1 The reason that the monad appears only on the backwards pass is that if we naively add it to the forwards pass (even if we also drop it from the backwards pass!) then lens composition completely fails to be associative. In the second half of this post we discuss how this is overcome.
Let’s open up the bonnet and remind ourselves how van Laarhoven lenses work. An ordinary lens , which is concretely a pair of a forwards pass and a backwards pass , is encoded via quantification over all functors:
type Lens s t a b = forall f. (Functor f) => (a -> f b) -> (s -> f t)
In order to extract the forwards pass from this, we instantiate to be the functor that is constant at , leaving us with , and then applying the identity function. To extract the backwards pass, we instead instantiate to be the identity functor, leaving us with , and then precomposing with the const function to get a function .
Going the other way, in order to turn a concrete lens into a van Laarhoven lens we must produce an from a continuation and an knowing only that is a functor. Using our , we apply the forwards pass to get an followed by the continuation to get an . Then we partially apply to the backwards pass to get a function , which we fmap over our to get an , which is what we needed.
So, in order to encode monadic lenses, we need to add a constraint that allows us to keep using the constant functor but prevents us using the identity functor. Instead of identity, in order to obtain the monadic backwards pass we would like to set to be itself, giving us . From there, we can get a function using the constant pure function .
Going the other way, let’s think about what we would need to know about in order to make an out of a monadic lens. Given we can apply the forwards pass to get an and then the continuation to get an , the same as before. Now if we fmap over the partially applied backwards pass we can get an . But we wanted an , so what we need to know about is that we have a way to do .
It turns out that a functor that allows you to do this (for all ) is exactly what is called a right -module. And everything lines up: constant functors are always right modules trivially, every monad is a right module of itself by the monad multiplication, and the identity functor is not a right module unless you can escape the monad.
If a monad is just a monoid in the category of endofunctors, then a right module is a right action of that monoid… what’s the problem? Right modules of monads do occasionally crop up in functional programming, see for example this paper.
Here’s the key bits of the code:
class RightModule m f where
act :: f (m a) -> f a
instance (Monad m) => RightModule m m where
= join
act
instance RightModule m (Const a) where
= Const . getConst
act
type MonadicLens m s t a b = forall f. (Functor f, RightModule m f) => LensLike f s t a b
monadicGet :: MonadicLens m s t a b -> s -> a
= getConst (l (Const) s)
monadicGet l s
monadicPut :: (Monad m) => MonadicLens m s t a b -> s -> b -> m t
= l (const (return b)) s
monadicPut l s b
monadicLens :: (Monad m) => (s -> a) -> (s -> b -> m t) -> MonadicLens m s t a b
= act (fmap (p s) (k (g s))) monadicLens g p k s
And that’s it! I had to switch on incoherent instances to make this compile, but I think that’s just Haskell being Haskell – I think it’s unable to figure out that the two instances can never overlap because Const a
is not a monad.
This was the easy part. Stay tuned for part 2 where we’ll also add the monad to the forwards pass.
Editor’s note: Jeremy Gibbons commented that the definition appears slightly earlier in this paper and also independently in this paper.↩︎