How to use bind with nested monads?

2019-02-08 21:47发布

问题:

I have two functions, one that tries to get a token from a webservice and may fail, and one that tries to use this token to get the username and may fail.

getToken :: IO (Maybe Token)
getUsername :: Token -> IO (Maybe String)

I would like to take the result of getToken and feed it to getUsername. If there was only IO or Maybe, I could simply use bind, but since there are down nested monads, I can't. How can I write something equivalent to getToken >>= getUsername :: IO (Maybe String) ?

More generally, what function has type m1 m2 a -> (a -> m1 m2 b) -> m1 m2 b?

Bonus question: how would I do that using the do notation in an IO context?

回答1:

I have defined a function useToken showing your use case:

type Token = String

getToken :: IO (Maybe Token)
getToken = undefined

getUsername :: Token -> IO (Maybe String)
getUsername = undefined

useToken :: IO (Maybe String)
useToken = do
  token <- getToken
  case token of
    Just x -> getUsername x
    Nothing -> return Nothing

If you don't want to use do notation, then you can use:

useToken2 :: IO (Maybe String)
useToken2 = getToken >>= \token -> maybe (return Nothing) getUsername token

Or using monad transformers, your code will become simpler:

import Control.Monad.Trans.Maybe
type Token = String

getToken :: MaybeT IO Token
getToken = undefined

getUsername :: Token -> MaybeT IO String
getUsername = undefined

useToken :: MaybeT IO String 
useToken = do
  token <- getToken
  getUsername token

Note that, you can also directly lift IO operations inside the monad transformer. As @Robedino points out, now the code will be more concise without do notation:

useToken :: MaybeT IO String 
useToken = getToken >>= getUsername


回答2:

As people in the comments suggest, you should just use monad transformers.

However you can avoid this in your case. Monads do not commute in general, so you can't write a function with this signature

bind' :: (Monad m, Monad n) => m (n a) -> (a -> m (n b)) -> m (n b)

But all is ok, if the inner monad is an instance of the Traversable class:

import Data.Traversable as T
import Control.Monad

joinT :: (Monad m, Traversable t, Monad t) => m (t (m (t a))) -> m (t a)
joinT = (>>= liftM join . T.sequence)

liftMM :: (Monad m, Monad n) => (a -> b) -> m (n a) -> m (n b)
liftMM = liftM . liftM

bindT :: (Monad m, Traversable t, Monad t) => m (t a) -> (a -> m (t b)) -> m (t b)
bindT x f = joinT (liftMM f x)

and the Maybe monad is; hence

type Token = String

getToken :: IO (Maybe Token)
getToken = undefined

getUsername :: Token -> IO (Maybe String)
getUsername = undefined

useToken :: IO (Maybe String)
useToken = getToken `bindT` getUsername

Also, with the {-# LANGUAGE RebindableSyntax #-} you can write

(>>=) = bindT

useToken :: IO (Maybe String)
useToken = do
    x <- getToken
    getUsername x

Update

With the type-level compose

newtype (f :. g) a = Nested { runNested :: f (g a) }

you can define a monad instance for nested monads:

instance (Monad m, Traversable t, Monad t) => Monad (m :. t) where
    return  = Nested . return . return
    x >>= f = Nested $ runNested x `bindT` (runNested . f)

Your example then is

type Token = String

getToken :: IO (Maybe Token)
getToken = undefined

getUsername :: Token -> IO (Maybe String)
getUsername = undefined

useToken :: IO (Maybe String)
useToken = runNested $ Nested getToken >>= Nested . getUsername

Or like you would do with the MaybeT transformer:

type Nested = (:.)

type Token = String

getToken :: Nested IO Maybe Token
getToken = undefined

getUsername :: Token -> Nested IO Maybe String
getUsername = undefined

useToken :: Nested IO Maybe String
useToken = getToken >>= getUsername

runUseToken :: IO (Maybe String)
runUseToken = runNested useToken