Reputation: 3591
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:
insertUser
helper function), for cases where no output is produced by IO
. More specifically, how to create a "zero" value for IO monad?Upvotes: 2
Views: 808
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 Nothing
s or Just
s.
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.)
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