I'm intrigued by the construction described here for determining a monad transformer from adjoint functors. Here's some code that summarizes the basic idea:
{-# LANGUAGE MultiParamTypeClasses #-}
import Control.Monad
newtype Three g f m a = Three { getThree :: g (m (f a)) }
class (Functor f, Functor g) => Adjoint f g where
counit :: f (g a) -> a
unit :: a -> g (f a)
instance (Adjoint f g, Monad m) => Monad (Three g f m) where
return = Three . fmap return . unit
m >>= f = Three $ fmap (>>= counit . fmap (getThree . f)) (getThree m)
instance (Adjoint f g, Monad m) => Applicative (Three g f m) where
pure = return
(<*>) = ap
instance (Adjoint f g, Monad m) => Functor (Three g f m) where
fmap = (<*>) . pure
Given that Adjoint ((,) s) ((->) s)
, Three ((->) s) ((,) s)
appears equivalent to StateT s
.
Very cool, but I am puzzled by a couple things:
How can we upgrade a monadic
m a
into a monadicThree g f m a
? For the specific case ofThree ((->) s) ((,) s)
, it's of course obvious how to do this, but it seems desirable to have a recipe that works for anyThree g f
provided thatAdjoint f g
. In other words, it seems like there should be an analog oflift
whose definition only requiresunit
,counit
, and thereturn
and>>=
of the input monad. But I cannot seem to find one (I have seen a definition usingsequence
, but this seems a bit like cheating since it requiresf
to beTraversable
).For that matter, how can we upgrade
g a
into aThree g f m a
(providedAdjoint f g
)? Again, for the specific case ofThree ((->) s) ((,) s)
it's obvious how to do this, but I'm wondering if there's an analog ofgets
that only requiresunit
,counit
, and thereturn
and>>=
of the input monad.
lift
, in Benjamin Hodgson's answer, is set up as:As you know, that is not the only plausible strategy we might use there:
Whence the
Traversable
requirement for Edward Kmett's correspondingMonadTrans
instance originates. The question, then, becomes whether relying on that is, as you put it, "cheating". I am going to argue it is not.We can adapt Benjamin's game plan concerning
Distributive
and right adjoints and try to find whether left adjoints areTraversable
. A look atData.Functor.Adjunction
shows we have a quite good toolbox to work with:Edward helpfully tells us that
unabsurdL
andcozipL
witness that "[a] left adjoint must be inhabited, [and that] a left adjoint must be inhabited by exactly one element", respectively. That, however, meanssplitL
corresponds precisely to the shape-and-contents decomposition that characterisesTraversable
functors. If we add to that the fact thatsplitL
andunsplitL
are inverses, an implementation ofsequence
follows immediately:(Note that no more than
Functor
is demanded ofm
, as expected for traversable containers that hold exactly one value.)All that is missing at this point is verifying that both implementations of
lift
are equivalent. That is not difficult, only a bit laborious. In a nutshell, thedistributeR
andsequenceR
definitions here can be simplified to:We want to show that
distributeR . fmap unit = fmap sequenceL . unit
. After a few more rounds of simplification, we get:We can show those are really the same thing by picking
\fu -> fmap (\x -> fmap (const x) fu) mx
-- the argument toleftAdjunct
in the second right-hand side -- and slippingrightAdjunct unit = counit . fmap unit = id
into it:The takeaway is that the
Traversable
route towards yourMonadTrans
is just as secure as theDistributive
one, and concerns about it -- including the ones mentioned by theControl.Monad.Trans.Adjoint
documentation -- should no longer trouble anyone.P.S.: It is worth noting that the definition of
lift
put forward here can be spelled as:That is,
lift
issequenceL
sent through the adjunction isomorphism. Additionally, from...... if we apply
rightAdjunct
on both sides, we get...... and if we compose
fmap (fmap counit)
on the left of both sides, we eventually end up with:So
distributeR
andsequenceL
are interdefinable.Good question. Time for a game of type tennis!
The hole is typed
g (m (f a))
. We havemx :: m a
in scope, and of courseunit :: a -> g (f a)
andfmap :: (a -> b) -> m a -> m b
.Now it's
_ :: m (g (f a)) -> g (m (f a))
. This isdistribute
ifg
isDistributive
.So now we just need to prove that the right hand side of an adjunction is always
Distributive
:Since we need to return a
g
, the clear choice of methods fromAdjunction
isleftAdjunct :: Adjunction f g => (f a -> b) -> a -> g b
, which usesunit
to create ag (f a)
and then tears down the innerf a
byfmap
ping a function.I'm going to attack the first hole first, with the expectation that filling it in might tell me something about the second one. The first hole has a type of
m a
. The only way we can get hold of anm
of any type is byfmap
ping something overmgx
.Now the first hole has a type of
a
, and we havegx :: g a
in scope. If we had anf (g a)
we could usecounit
. But we do have anf x
(wherex
is currently an ambiguous type variable) and ag a
in scope.It turns out that the remaining hole has an ambiguous type, so we can use anything we want. (It'll be ignored by
$>
.)That derivation may have looked like a magic trick but really you just get better at type tennis with practice. The skill of the game is being able to look at the types and apply intuitions and facts about the objects you're working with. From looking at the types I could tell that I was going to need to exchange
m
andg
, and traversingm
was not an option (becausem
is not necessarilyTraversable
), so something likedistribute
was going to be necessary.Besides guessing I was going to need to implement
distribute
, I was guided by some general knowledge about how adjunctions work.Specifically, when you're talking about
* -> *
, the only interesting adjunctions are (uniquely isomorphic to) theReader
/Writer
adjunction. In particular, that means any right adjoint onHask
is alwaysRepresentable
, as witnessed bytabulateAdjunction
andindexAdjunction
. I also know that allRepresentable
functors areDistributive
(in fact logically the converse is also true, as described inDistributive
's docs, even though the classes aren't equivalent in power), perdistributeRep
.I'll leave that as an exercise. I suspect you'll need to lean on the
g ~ ((->) s)
isomorphism again. I actually don't expect this one to be true of all adjunctions, just the ones onHask
, of which there is only one.