How to lift MonadWriter's listen method parametrically?

It’s well-known that you can implement first-order effect methods of mtl classes for any monad transformer:

liftTell :: (MonadTrans t, MonadWriter w m) => w -> t m ()
liftTell = lift . tell

But this doesn’t work straightforwardly for higher order effects like listen :: MonadWriter w m => m a -> m (w, a). Is there a way to still implement a general liftListen somehow by constraining the transformer somewhat?

More precisely, can you give a class MyClass such that this can be implemented:

liftListen :: (MonadTrans t, MonadWriter w m, MyClass t) => m a -> m (w, a)

MyClass might include MFunctor, but I believe this will not suffice. What else do we need? Algebra from fused-effects should suffice, but can we do simpler?

Solving this would essentially solve the m × n instances problem of mtl. I remember someone on reddit showing me how to do that. I’ll try to find that conversation again… Edit: found it

Well, the constraint on t might exclude some transformers. Do ListT and SelectT work?

I believe ListT works, but SelectT is a bit awkward because it requires inserting a mempty:

changelogSelect :: MonadChangeset s w m => SelectT r m a -> SelectT r m (w, a)
changelogSelect = SelectT . ((fmap swap . changelog) .) . (. (\f a -> f (mempty, a))) . runSelectT

So I’d be fine with finding a general class that allows this operation even if SelectT is not an instance of it.

This is the comment I was referring to: jumper149 comments on How to analyze the run-time/amortization of Monads?

So perhaps ComposeT can be used?

I haven’t checked if this would fit your needs.

It seems Elevator might do what you want.

At the heart of listen is the duplication function \w -> (w,w) which makes sense for which categories? Monoidal categories, I would say. The join of the Writer monad relies on the opposite mapping (w,w) -> w. Thus I venture that if you can factor t m into an adjunction f -| g between the category (->) and some monoidal category c such that

t m a = (g `Compose` ((,) w) `Compose`  f) a

where the middle part is the monoidal product with w in category c, you can obtain a listen. Let

dup :: (w,a) -> (w,(w,a))
fmapF :: (a -> b) -> c (f a) (f b) -- functoriality of the left adjoint
natiso :: c (f a) b -> a -> g b -- natural isomorphism of hom-sets
counit :: c (f (g x)) x -- counit of the adjunction f -| g

then

natiso (fmapF dup . counit) :: g (f (w,a)) -> g (f (w,(w,a)))

Note that we need monoidal structure on c to make (,) w a monad on it for any Monoid w, so that the composite becomes a monad, too.

I had a hunch MonadTransControl would be the key here, and I’m glad I clicked your link because it saved me a bit of fiddling!

import Control.Monad.Trans.Control
import Control.Monad.Writer.Class

liftListen :: (MonadTransControl t, MonadWriter w m) => t m a -> t m (a, w)
liftListen tma = do
  (x, w) <- liftWith $ \run -> listen $ run tma
  (, w) <$> restoreT (pure x)