Newbie question: Easing cognitive loads involving MonadTransformers and lifts

Hi guys, newbie here.

I just started a pet project in PureScript. I’m working in Monad Transformers, and I’m running into something that drains my brain quite a bit, and I’m wondering how you would approach it.

Let’s say I’ve got a bunch of functions S that result in Effect (Maybe a), but I want to work in a MaybeT Effect a. Now, for various reasons, I don’t want to change all the original functions S to be MaybeT Effect a, because doing so will involve rewriting a bunch of code. So I create two functions hoistMaybe and translateMaybe. It wasn’t hard to google it and find a base on how to write these:

hoistMaybe :: forall a m. Maybe a -> MaybeT m a
translateMaybe :: forall a m. Effect (Maybe a) -> MaybeT Effect a

And that’s fine. I wish the nature of Monad Transformers allowed there to be a general solution, but sure, I can write a hoistX and translateX for every concrete Monad Transformer, nbd.

So using these functions, I’m able to implement a bunch of MaybeT Effect a functions. So then I go to test, and I get to using Spec, where the tests are all written as Aff Unit. Ah, crap. To keep the code easy for my brain to handle, I now need to be in MaybeT Aff a, not MaybeT Effect a. So I write the following functions so that I can use my MaybeT functions in the same place, and then I write some tests:

-- I discard a Nothing as a testing failure
runMaybeAff :: forall a. MaybeT Aff a -> Aff Unit

-- Seems like I have to convert the results of my existing 
-- functions using `runMaybeT` and translateMaybe`.
liftMaybeEffect :: forall a. MaybeT Effect a -> MaybeT Aff a

-- I also have some `Effect a` functions to run.
liftAffEffect :: forall a. Effect a -> MaybeT Aff a

-- Now I can write my test
it "does a thing" $ runMaybeAff do 
  a <- (doThing1 :: MaybeT Effect a) # liftMaybeEffect
  b <- (doThing2 :: MaybeT Effect b) # liftMaybeEffect

  -- Easy enough to write `lift`
  a `shouldEqual` b # lift

  -- Yet another lift function
  liftAffEffect $ foreachE things \thing -> do
    doThing thing `shouldEqual` 2

And I guess my question … is, is this the right way to do things? Cognitively, it’s not easy for me. Conceptually, all these things are for a similar purpose as lift. But because PureScript doesn’t have function overloads, I can’t name them all lift and let the compiler figure it out for me. I have to remember “what type” of function I’m using, and select the correct liftX function based on that, and that makes the type inference less useful, and it makes me tired. Or is it just me?

And while it’s not great that I have to use a liftX in most lines of the function, it seems necessary. It’s a relatively small price to pay, if the only function I ever had to type was lift, with no variants. But that’s not true.

So I guess I’m just looking for feedback on my approach, and any other ways to lower cognitive load while programming in PureScript. I’m on VsCode with the PureScript plugin, but that’s about it. But maybe this is the kind of thing I should be expecting an IDE to handle.

I think what you have is pretty standard. There’s a couple minor things that might help you out.

  1. hoistX probably does need to be defined per transformer, though some transformers already have this defined (like except for the ExceptT monad transformer), though MaybeT doesn’t :cry:. In some cases, you maybe could have functions return forall m. Plus m => m a instead of Maybe a (and use empty/pure instead of Nothing/Just), though that might disrupt readability in those functions.
  2. translateX probably does not need to be defined per transformer, since it’s just the wrap function. Though one might prefer to use the MaybeT constructor directly instead of wrap for readability.
  3. I bet some of the boilerplate would be cut by using
    forall m. MonadEffect m => ... -> MaybeT m ...
    in some of the spots that you currently use
    ... -> MaybeT Effect ...
    b/c then you can get rid of liftMaybeEffect. If you similarly replace some of your other Effects, you might be able to get rid of liftAffEffect as well.

Thanks for those suggestions. I looked up Plus and MonadEffect, and I can get an idea of why those would be helpful. Could you elaborate on wrap though? I looked it up on Pursuit and Hoogle, and I could only figure a connection to free monads and not monad transformers. And I didn’t see an implementation for that for MaybeT either, so I’m not making the connection on how to use it.

I’m referring to this wrap, which is just a generic function for the constructor of any Newtype (the class, not the keyword). I think all transformers will also have Newtype instances. MaybeT does. MaybeT is just a newtype for an m (Maybe a), so your translateMaybe would really just be another name for the MaybeT constructor. wrap would be how to do the same for any transformer as long as it had a Newtype instance.

Ah, got it. My brain just aborted with “That’s impossible” when I saw the NewType package. But that fits very neatly. Thanks again!