Skip to content

Instantly share code, notes, and snippets.

@xieyuschen
Created June 6, 2023 10:53
Show Gist options
  • Save xieyuschen/21845e3f9b5a9950ef0469ea30dc2100 to your computer and use it in GitHub Desktop.
Save xieyuschen/21845e3f9b5a9950ef0469ea30dc2100 to your computer and use it in GitHub Desktop.
Haskell: why we need a monad transoformer?

Haskell: why we need a monad transoformer?

When learning the haskell monad transformer(would call it monadt in the left content), a problem in my mind is why monadT? Because from a simple example view, we could make a pale of monad together to do what we want we want to do.

Technially, it's correct, the monadT is just a wrapper. For example, those two cases are equal.

ioOp :: IO String
ioOp = getLine

maybeOp :: Maybe String -> Maybe String
maybeOp a = fmap (++"suffix") a

combinedOpInMonadT :: MaybeT IO String
combinedOpInMonadT = 
  lift ioOp >>= \input -> MaybeT (pure (maybeOp (Just input)))
  -- lift ioOp >>= \input -> MaybeT (pure (maybeOp (Just input)))

combineOpManually :: IO (Maybe String)
combineOpManually =
  ioOp >>= \input -> pure $ maybeOp (Just input)
  
triggerBoth :: IO ()
triggerBoth = do
    runMaybeT combinedOpInMonadT >>= print
    combineOpManually >>=print

We could also confirm this idea from the source code of MaybeT implementation. Here I show show a brief but important code snippet here. What't do you find from the code? It is just a wrapper. However, what's more, you should notice that it(MaybeT m) also performs as a new monad and have the similar behavior of the maybe.

-- | A monad transformer which adds Maybe semantics to an existing monad.
newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }

instance (Functor m) => Functor (MaybeT m) where
  fmap f = MaybeT . fmap (fmap f) . runMaybeT

instance (Monad m) => Monad (MaybeT m) where
  fail _ = MaybeT (return Nothing)
  return = lift . return
  x >>= f = MaybeT (runMaybeT x >>= maybe (return Nothing) (runMaybeT . f))

instance (Monad m) => MonadPlus (MaybeT m) where
  mzero = MaybeT (return Nothing)
  mplus x y = MaybeT $ do v <- runMaybeT x
                          case v of
                            Nothing -> runMaybeT y
                            Just _  -> return v

After knowing this, we should know it's common to compose multiple monads together to support new utilites. However, haskell doesn't allow us to do that arbitaryly. We should achieve this with the help of monadT. The important part of monadT i think is it gives the ability to avoid unbox and box efforts. Sometimes as a library, multiple composition of monad really a lousy thing. For example , how do you operate a String inside the value return by the libMethod? You have no way but unbox by a lot of helper functions.

libMethod :: A (B (C (D (E String))))

However, benifited from the monadT, we could have a way to treat the A (B (C (D (E as an another monad, let's call it NewMonad here and the function signature becomes this.

libMethod :: NewMonad String

Nice, it's clear and easy to use.

Further more, I'd provide another code snippet for you to better understand the monadT. If you could treat the composition as a new monad, you are more flexiable to write your code and lest disturbing by noisy and sleasy unbox-box.

addPrefix :: String -> String
addPrefix str = "prefix" ++ str

theProblemOfIOMaybeString :: IO ()
theProblemOfIOMaybeString = do
  -- not easy to bind addPrefix on the IO (Maybe String)
  -- bind directly cannot work!
  -- combineOpManually >>= addPrefix >>= print
  (combineOpManually >>= \input -> pure $ fmap addPrefix input) >>= print
  
noTrouble :: IO ()
noTrouble = do
  runMaybeT (fmap addPrefix combinedOpInMonadT) >>= print
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment