Closed khwarizmii closed 5 years ago
@khwarizmii: Do you need to use writerT
/runWriterT
to implement the instance? Could you implement it instead as:
instance MFunctor (WriterT w) where
hoist nat (WriterT f) = WriterT (\w -> nat (f w))
This was my initial thought too. I think this breaks the semantics of WriterT
, however. This is because nat
can drop the continuation. Hopefully I can get to you quickly with a concrete example soon!
Composing a counter example has not been particularly straightforward :-/
So here's a (failed!) attempt at a proof of correctness.
Let unWriterT (WriterT f) = f
. Let's try to prove the StateT
-like instance for CPS WriterT
is the same as the smart constructor instance. This is the case if and only if the following equation is obeyed:
WriterT . nat . unWriterT $ (WriterT f) = writerT . nat . runWriterT $ WriterT f (1)
Rewriting the LHS yields:
WriterT . nat . unWriterT $ (WriterT f) = WriterT . nat . f (2)
Rewriting the RHS yields:
writerT . nat . runWriterT $ WriterT f
= writerT . nat $ f empty
= WriterT $ \ w -> (\ (a, w') -> (a, w <> w')) <$> nat (f empty)
= WriterT $ fmap (\ w -> (\ (a, w') -> (a, w <> w'))) . nat $ f empty (3)
From (1), (2) and (3) we have, for all natural transformations nat
,
nat . f = fmap (\ w -> (\ (a, w') -> (a, w <> w'))) . nat $ f empty (4)
by naturality we have fmap g . nat = nat . fmap g
, hence
nat . f = nat . fmap (\ w -> (\ (a, w') -> (a, w <> w'))) $ f empty (5)
Since id
is a natural transformation, we know that (5) is true if and only if
f = fmap (\ w -> (\ (a, w') -> (a, w <> w'))) $ f empty (6)
This doesn't appear to be obvious, although coming up with a counter example hasn't been fruitful for me.
[EDIT: I expanded this attempted proof a little further]
@khwarizmii: I can complete your proof. The missing piece is that nat
is a monad morphism and all monad morphisms are natural transformations, which must obey the following law:
nat . fmap k = fmap k . nat
That means that you can rearrange this:
nat . f = \ w -> (\ (a, w') -> (a, w <> w')) <$> nat (f empty)
... to this:
nat . f = \ w -> nat ((\ (a, w') -> (a, w <> w')) <$> f empty)
... which you can further rearrange to:
nat . f = nat . (\w -> (\ (a, w') -> (a, w <> w')) <$> f empty)
... which is true if:
f = \w -> (\ (a, w') -> (a, w <> w')) <$> f empty
If we wrap both sides of that in the WriterT
constructor
WriterT f = WriterT (\w -> (\ (a, w') -> (a, w <> w')) <$> f empty)
... then we can recognize that the right-hand-side of that is the same as:
WriterT f = (writerT . runWriterT) (WriterT f)
... and since writerT . runWriterT = id
then we complete the proof:
WriterT f = WriterT f
Thanks! I regret having to resort to skipping the smart contructor to write this instance, but presumably introducing the proposed monad constraint could be breaking to anyone abusing this library.
Thanks again for helping me work through the proof!
@khwarizmii: You're welcome! 🙂
Hi! My company is using
mmorph
in our codebase. We would like to give an instance ofMFunctor
for the continuation passing styleWriterT
fromwriter-cps-transformers
. Sadly, we cannot do this correctly unless we strengthen the constraints onhoist
like so:Here's why: if you look at the CPS
WriterT
, it is superficially the same asStateT
:However, it has different semantics. For one,
runWriterT
for this transformer is defined as follows:CPS
WriterT
also comes with a smart constructor so it can hide its internal state:These two are inverses and obey the following laws. This distinguishes CPS
WriterT
fromStateT
:We can't give the usual instance of
MFunctor
for CPSWriterT
because of theFunctor
constraint unless we strengthenhoist
:I suspect that introducing a constraint on the second type constructor to be a monad might break some unlawful code. Otherwise I can't see how this change could introduce any backward incompatibility.