我正在编写我的第一个Haskell应用程序,而且我很难理解使用Monad变换器。
示例代码:
-- 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
问题:
insertUser
没有产生输出,如何避免打印到控制台等操作(如IO
辅助函数中所做的那样)。更具体地说,如何创建一个&#34;零&#34; IO monad的价值?答案 0 :(得分:5)
修改:更新的答案以匹配您更新的问题。
为了清楚起见,您实际上并未在代码示例中使用任何monad转换器。你只是将一个monad嵌套在另一个monad中。有关使用真实monad变换器MonadT
的示例,请参阅我对第二个问题的回答。
关于你的第一个问题,正如@David Young评论的那样,你可以使用return ()
:
showSuccess :: Bool -> IO ()
showSuccess success =
if success then putStrLn "I am great!"
else return () -- fail silently
更一般地说,如果函数为某些类型IO a
返回a
,那么您始终可以使用return
函数返回没有关联IO操作的“纯”值。 (这就是return
的用途!)如果函数返回IO ()
,则()
类型的唯一值是值()
,因此您唯一的选择是{ {1}}。对于某些其他类型return ()
的{{1}},您需要IO a
某些类型a
的值。如果您希望选项返回值,则需要输入类型return
或使用a
转换器,如下所示。
对于你的第二个问题,你基本上是在如何巧妙地表达IO (Maybe a)
monad中的嵌套计算:
MaybeT
在外Maybe
monad。
通常,嵌套monad中的大量计算很难编写并导致丑陋,不清楚的代码。这就是monad变形金刚被发明的原因。它们允许您从多个monad中借用设施并将它们捆绑在单个 monad中。然后,所有绑定(let newUser = encrypted >>= (\x -> Just user { password = x })
)和IO
操作,以及所有do语法都可以引用相同的单个monad中的操作,因此您不会在“IO模式”和“可能模式”之间切换“当你在阅读和编写代码时。
重写代码以使用转换器涉及从>>=
包导入return
转换器并定义您自己的monad。你可以把它叫做任何你喜欢的东西,虽然你可能会打字很多,所以我经常使用简短的东西,比如MaybeT
。
transformers
然后,您可以按如下方式重写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
monad负责确保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
运算符绑定的每个变量都已经检查过M
。如果任何步骤返回<-
,则处理将中止。如果某个步骤返回Nothing
,则Nothing
将自动解包。您通常不必处理(甚至查看)Just x
或x
s。
您的其他功能也必须存在于Nothing
monad中,并且它们可以返回值(成功)或指示失败,如下所示:
Just
请注意,他们可以使用M
将操作提升到底层IO monad,因此所有IO操作都可用。否则,他们可以使用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
的别名)返回纯值(通过return
)或MaybeT
图层中的信号失败。
现在唯一剩下的就是提供一个“运行”自定义monad的工具(包括将其从nothing
转换为mzero
,这样你就可以从{{1}运行它}})。对于这个monad,定义是微不足道的,但是如果你的M a
monad更复杂,定义一个函数是个好习惯:
IO a
下面包含一个包含存根代码的完整整个工作示例。
对于您的第三个问题,使其“更具功能性”并不一定会让它更容易理解,但想法是利用main
等monad运算符或运算符运算符像M
一样模仿monadic上下文中的函数形式。以下是我的monad变换器版本runM :: M a -> IO (Maybe a)
runM = runMaybeT
的等效“更多功能”形式。目前尚不清楚这比上面的命令式“do-notation”版本更容易理解,而且写作肯定更难。
=<<
你可以想象这与纯函数计算大致相同:
<*>
但是使用正确的操作符进行类型检查作为monadic计算。 (为什么你需要userSignUp
?甚至不要问。)
moreFunctionalUserSignUp :: Connection -> User -> M ()
moreFunctionalUserSignUp conn user
= join $ createUser conn
<*> (setPassword user <$> encryptPassword (password user))
where
setPassword u p = u { password = p }