Chetan Bhasin
Chetan Bhasin

Reputation: 3591

How to use Monad Transformers to combine different (both pure and impure) monads?

I'm writing my first Haskell application, and I'm having a hard time understanding the use of Monad transformers.

Example code:

-- Creates a new user in the system and encrypts their password
userSignup :: Connection -> User -> IO ()
userSignup conn user = do
    -- Get the encrypted password for the user
    encrypted <- encryptPassword $ password user.     -- encryptPassword :: Text -> IO (Maybe Text)
    -- Updates the password to the encrypted password
    -- if encryption was successful
    let newUser = encrypted >>= (\x -> Just user { password = x })
    -- Inserts the user using helper function and gets the result
    result <- insertUser (createUser conn) newUser
    return result
    where
        insertUser :: (User -> IO ()) -> (Maybe User) -> IO ()
        insertUser insertable inuser = case inuser of
            Just u -> insertable u -- Insert if encryption was successful
            Nothing -> putStrLn "Failed to create user" -- Printing to get IO () in failure case

Problem:

Upvotes: 2

Views: 808

Answers (1)

K. A. Buhr
K. A. Buhr

Reputation: 51159

Edit: updated answer to match your updated question.

Just to be clear, you aren't actually using any monad transformers in your code example. You're just nesting one monad inside another. For an example using a real monad transformer MonadT, see my answer to your second question.

For your first question, as @David Young commented, you can use return ():

showSuccess :: Bool -> IO ()
showSuccess success =
   if success then putStrLn "I am great!"
              else return ()    -- fail silently

More generally, if a function returns IO a for some type a, then you can always return a "pure" value that has no associated IO action by using the return function. (That's what return is for!) In the case of a function returning IO (), the only value of type () is the value (), so your only choice is return (). For IO a for some other type a, you'll need to return some value of type a. If you want the option to return a value or not, you'll need to make the type IO (Maybe a) or use a MaybeT transformer, as below.

For your second question, you're basically asking how to neatly express nested computations in the Maybe monad:

let newUser = encrypted >>= (\x -> Just user { password = x })

within an outer IO monad.

In general, extensive computations in nested monads are painful to write and lead to ugly, unclear code. That's why monad transformers were invented. They allow you to take facilities borrowed from multiple monads and bundle them up in a single monad. Then, all bind (>>=) and return operations, and all do-syntax can refer to operations in the same, single monad, so you aren't shifting between "IO mode" and "Maybe mode" when you're reading and writing code.

Rewriting your code to use a transformer involves importing the MaybeT transformer from the transformers package and defining your own monad. You can call it anything you like, though you'll likely be typing it a lot, so I usually use something short, like M.

import Control.Monad
import Control.Monad.Trans
import Control.Monad.Trans.Maybe

-- M is the IO monad supplemented with Maybe functionality
type M = MaybeT IO
nothing :: M a
nothing = mzero  -- nicer name for failing in M monad

Then, you can rewrite your userSignUp function as follows:

userSignUp :: Connection -> User -> M ()
userSignUp conn user = do
  encrypted <- encryptPassword (password user)     -- encrypted  :: String
  let newUser = user { password = encrypted }      -- newUser    :: User
  insertUser <- createUser conn                    -- insertUser :: User -> M ()
  insertUser newUser

I've added some type annotations in the comments. Note that the new M monad takes care of ensuring that each variable bound by the <- operator will have already been checked for Nothing. If any step returns Nothing, processing will be aborted. If a step returns Just x, the x will be automatically unwrapped. You don't typically have to deal with (or even see) the Nothings or Justs.

Your other functions must live in the M monad, too, and they can either return a value (success) or indicate failure like so:

encryptPassword :: String -> M String
encryptPassword pwd = do
  epwd <- liftIO $ do putStrLn "Dear System Operator,"
                      putStrLn $ "Plaintext password was " ++ pwd
                      putStr $ "Please manually calculate encrypted version: "
                      getLine
  if epwd == "I don't know" then nothing   -- return failure
    else return epwd                       -- return success

Note that they can use liftIO to lift operations to the underlying IO monad, so all IO operations are available. Otherwise, they can either return pure values (via return) or signal failure in the MaybeT layer with nothing (my alias for mzero).

The only thing left now is to provide a facility to "run" your custom monad (which involves converting it from an M a to an IO a, so you could actually run it from main). For this monad, the definition is trivial, but it's good practice to define a function in case your M monad is more complicated:

runM :: M a -> IO (Maybe a)
runM = runMaybeT

A complete whole working example with stub code is included below.

For your third question, making it "more functional" won't necessarily make it more understandable, but the idea would be to make use of monad operators like =<< or applicative operators like <*> to mimic a functional form in a monadic context. The following would be an equivalent "more functional" form of my monad transformer version of userSignUp. It's not clear that this is more understandable than the imperative, "do-notation" version above, and it's certainly harder to write.

moreFunctionalUserSignUp :: Connection -> User -> M ()
moreFunctionalUserSignUp conn user
  = join $ createUser conn
           <*> (setPassword user <$> encryptPassword (password user))
  where
    setPassword u p = u { password = p }

You can imagine this as roughly equivalent to the pure functional computation:

  createUser conn (setPassword user (encryptPassword (password user)))

but with the right operators sprinkled in to make it type-check as a monadic computation. (Why do you need join? Don't even ask.)

Full MaybeT Example

import Control.Monad
import Control.Monad.Trans
import Control.Monad.Trans.Maybe

-- M is the IO monad supplemented with Maybe functionality
type M = MaybeT IO
nothing :: M a
nothing = mzero  -- nicer name for failing in M monad

runM :: M a -> IO (Maybe a)
runM = runMaybeT

data User = User { username :: String, password :: String } deriving (Show)
data Connection = Connection

userSignUp :: Connection -> User -> M ()
userSignUp conn user = do
  encrypted <- encryptPassword (password user)     -- encrypted  :: String
  let newUser = user { password = encrypted }      -- newUser    :: User
  insertUser <- createUser conn                    -- insertUser :: User -> M ()
  insertUser newUser

encryptPassword :: String -> M String
encryptPassword pwd = do
  epwd <- liftIO $ do putStrLn "Dear System Operator,"
                      putStrLn $ "Plaintext password was " ++ pwd
                      putStr $ "Please manually calculate encrypted version: "
                      getLine
  if epwd == "I don't know" then nothing   -- return failure
    else return epwd                       -- return success

createUser :: Connection -> M (User -> M ())
createUser conn = do
  -- some fake storage
  return (\user -> liftIO $ putStrLn $ "stored user record " ++ show user)

main :: IO ()
main = do username  <- putStr "Username: " >> getLine
          password  <- putStr "Password: " >> getLine

          let user = User username password
          result <- runM (userSignUp Connection user)

          case result of
            Nothing -> putStrLn "Something failed -- with MaybeT, we can't tell what."
            Just () -> putStrLn "Success!"

Upvotes: 6

Related Questions