编辑:更新答案以匹配您更新的问题。
为了清楚起见,您实际上并没有在代码示例中使用任何 monad 转换器。您只是将一个单子嵌套在另一个单子中。有关使用真正的 monad 转换器 MonadT 的示例,请参阅我对第二个问题的回答。
对于您的第一个问题,正如@David Young 评论的那样,您可以使用return ():
showSuccess :: Bool -> IO ()
showSuccess success =
if success then putStrLn "I am great!"
else return () -- fail silently
更一般地说,如果一个函数为某种类型的a 返回IO a,那么您始终可以使用return 函数返回一个没有关联IO 操作的“纯”值。 (这就是return 的用途!)对于返回IO () 的函数,() 类型的唯一值是(),所以你唯一的选择是return ()。对于IO a,对于其他类型a,您需要return 一些a 类型的值。如果您希望 选项 返回一个值或不返回一个值,则需要将类型设置为 IO (Maybe a) 或使用 MaybeT 转换器,如下所示。
对于第二个问题,您基本上是在问如何在 Maybe monad 中巧妙地表达嵌套计算:
let newUser = encrypted >>= (\x -> Just user { password = x })
在外部 IO monad 内。
通常,嵌套 monad 中的大量计算编写起来很痛苦,并导致代码丑陋、不清晰。这就是发明单子变压器的原因。它们允许您从多个 monad 中借用设施,并将它们捆绑在一个 single monad 中。然后,所有绑定 (>>=) 和 return 操作,以及所有 do-syntax 都可以引用相同的单个 monad 中的操作,所以当你'重新读写代码。
重写代码以使用转换器涉及从 transformers 包中导入 MaybeT 转换器并定义您自己的 monad。您可以随意称呼它,尽管您可能会经常输入它,所以我通常使用简短的名称,例如 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
然后,你可以重写你的userSignUp函数如下:
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
我在 cmets 中添加了一些类型注释。请注意,新的M monad 负责确保由<- 运算符绑定的每个变量都已经检查了Nothing。如果任何步骤返回Nothing,则处理将中止。如果一个步骤返回Just x,x 将被自动解包。您通常不必处理(甚至查看)Nothings 或 Justs。
您的其他函数也必须存在于 M monad 中,它们可以返回值(成功)或指示失败,如下所示:
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
请注意,他们可以使用liftIO 将操作提升到底层 IO monad,因此所有 IO 操作都可用。否则,它们可以返回纯值(通过return)或在MaybeT 层中使用nothing(我的别名mzero)发出信号故障。
现在唯一剩下的就是提供一种工具来“运行”您的自定义 monad(这涉及将其从 M a 转换为 IO a,因此您实际上可以从 main 运行它)。对于这个 monad,定义很简单,但最好定义一个函数,以防您的 M monad 更复杂:
runM :: M a -> IO (Maybe a)
runM = runMaybeT
下面包含一个完整的带有存根代码的完整工作示例。
对于您的第三个问题,使其“更具功能性”并不一定会使其更易于理解,但我们的想法是使用像 =<< 这样的单子运算符或像 @ 这样的应用运算符987654370@ 在单子上下文中模仿功能形式。以下将是我的 userSignUp 的 monad 转换器版本的等效“更多功能”形式。不清楚这是否比上面的命令式“do-notation”版本更容易理解,而且肯定更难编写。
moreFunctionalUserSignUp :: Connection -> User -> M ()
moreFunctionalUserSignUp conn user
= join $ createUser conn
<*> (setPassword user <$> encryptPassword (password user))
where
setPassword u p = u { password = p }
你可以想象这大致相当于纯函数计算:
createUser conn (setPassword user (encryptPassword (password user)))
但是加入了正确的运算符,使其类型检查成为一元计算。 (为什么需要join?别问了。)
完整的 MaybeT 示例
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!"