如何使用 Monad Transformers 组合不同的(纯的和不纯的)monad?
How to use Monad Transformers to combine different (both pure and impure) monads?
我正在编写我的第一个 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
问题:
- 在
IO
没有输出的情况下,如何避免打印到控制台等操作(如 insertUser
辅助函数中所做的那样)。更具体地说,如何为 IO monad 创建一个 "zero" 值?
- 如何组合两种不同类型的 monad(在本例中为 Maybe 和 IO)以便我可以组合它们的内容并生成一个可能包含结果或可能的错误的统一结果?
- 如何用更容易理解的函数式方式表达这样的问题?
编辑: 更新答案以匹配您更新的问题。
需要说明的是,您实际上并没有在您的代码示例中使用 任何 monad 转换器。您只是将一个 monad 嵌套在另一个 monad 中。有关使用真正的 monad 转换器的示例 MonadT
,请参阅我对第二个问题的回答。
对于你的第一个问题, 正如@David Young 评论的那样,你可以使用 return ()
:
showSuccess :: Bool -> IO ()
showSuccess success =
if success then putStrLn "I am great!"
else return () -- fail silently
更一般地说,如果一个函数 returns IO a
对于某种类型 a
,那么你总是可以 return 一个 "pure" 值,它没有通过使用 return
函数关联的 IO 操作。 (这就是 return
的用途!)在函数 returning IO ()
的情况下,类型 ()
的唯一值是值 ()
,所以你唯一的选择是 return ()
。对于 IO a
某些其他类型 a
,您将需要 return
某些类型 a
的值。如果您想要 选项 到 return 一个值或不想要,您需要制作类型 IO (Maybe a)
或使用 MaybeT
转换器,因为下面。
对于你的第二个问题,你基本上是在问如何在 Maybe
monad 中巧妙地表达嵌套计算:
let newUser = encrypted >>= (\x -> Just user { password = x })
在外部 IO
monad 中。
一般来说,嵌套 monad 中的大量计算编写起来很痛苦,并且会导致难看、不清楚的代码。这就是发明 monad 转换器的原因。它们允许您从多个 monad 中借用设施并将它们捆绑在一个 single monad 中。然后,所有 bind (>>=
) 和 return
操作,以及所有 do-syntax 都可以引用相同的单个 monad 中的操作,因此您不会在 "IO mode" 和 "Maybe mode"当你阅读和编写代码时。
重写您的代码以使用转换器涉及从 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
我在评论中添加了一些类型注释。请注意,新的 M
monad 负责确保每个由 <-
运算符绑定的变量都已经过 Nothing
检查。如果任何步骤 returns Nothing
,处理将被中止。如果步骤 returns Just x
,x
将自动展开。您通常不必处理(甚至看不到)Nothing
或 Just
。
您的其他函数也必须存在于 M
monad 中,它们可以 return 一个值(成功)或指示失败,如下所示:
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 纯值(通过 return
)或使用 nothing
(我对 mzero
的别名)在 MaybeT
层中发出故障信号。
现在唯一剩下的就是为 "run" 您的自定义 monad 提供一个工具(包括将其从 M a
转换为 IO a
,因此您实际上可以 运行 它来自 main
)。对于这个 monad,定义很简单,但最好定义一个函数,以防 M
monad 更复杂:
runM :: M a -> IO (Maybe a)
runM = runMaybeT
下面包含一个带有存根代码的完整工作示例。
对于你的第三个问题, 使它成为 "more functional" 不一定会使它更容易理解,但我的想法是利用像 [= 这样的 monad 运算符61=] 或像 <*>
这样的应用运算符来模拟 monadic 上下文中的函数形式。以下将是我的 userSignUp
的 monad 转换器版本的等效 "more functional" 形式。不清楚这个比上面的命令式"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!"
我正在编写我的第一个 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
问题:
- 在
IO
没有输出的情况下,如何避免打印到控制台等操作(如insertUser
辅助函数中所做的那样)。更具体地说,如何为 IO monad 创建一个 "zero" 值? - 如何组合两种不同类型的 monad(在本例中为 Maybe 和 IO)以便我可以组合它们的内容并生成一个可能包含结果或可能的错误的统一结果?
- 如何用更容易理解的函数式方式表达这样的问题?
编辑: 更新答案以匹配您更新的问题。
需要说明的是,您实际上并没有在您的代码示例中使用 任何 monad 转换器。您只是将一个 monad 嵌套在另一个 monad 中。有关使用真正的 monad 转换器的示例 MonadT
,请参阅我对第二个问题的回答。
对于你的第一个问题, 正如@David Young 评论的那样,你可以使用 return ()
:
showSuccess :: Bool -> IO ()
showSuccess success =
if success then putStrLn "I am great!"
else return () -- fail silently
更一般地说,如果一个函数 returns IO a
对于某种类型 a
,那么你总是可以 return 一个 "pure" 值,它没有通过使用 return
函数关联的 IO 操作。 (这就是 return
的用途!)在函数 returning IO ()
的情况下,类型 ()
的唯一值是值 ()
,所以你唯一的选择是 return ()
。对于 IO a
某些其他类型 a
,您将需要 return
某些类型 a
的值。如果您想要 选项 到 return 一个值或不想要,您需要制作类型 IO (Maybe a)
或使用 MaybeT
转换器,因为下面。
对于你的第二个问题,你基本上是在问如何在 Maybe
monad 中巧妙地表达嵌套计算:
let newUser = encrypted >>= (\x -> Just user { password = x })
在外部 IO
monad 中。
一般来说,嵌套 monad 中的大量计算编写起来很痛苦,并且会导致难看、不清楚的代码。这就是发明 monad 转换器的原因。它们允许您从多个 monad 中借用设施并将它们捆绑在一个 single monad 中。然后,所有 bind (>>=
) 和 return
操作,以及所有 do-syntax 都可以引用相同的单个 monad 中的操作,因此您不会在 "IO mode" 和 "Maybe mode"当你阅读和编写代码时。
重写您的代码以使用转换器涉及从 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
我在评论中添加了一些类型注释。请注意,新的 M
monad 负责确保每个由 <-
运算符绑定的变量都已经过 Nothing
检查。如果任何步骤 returns Nothing
,处理将被中止。如果步骤 returns Just x
,x
将自动展开。您通常不必处理(甚至看不到)Nothing
或 Just
。
您的其他函数也必须存在于 M
monad 中,它们可以 return 一个值(成功)或指示失败,如下所示:
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 纯值(通过 return
)或使用 nothing
(我对 mzero
的别名)在 MaybeT
层中发出故障信号。
现在唯一剩下的就是为 "run" 您的自定义 monad 提供一个工具(包括将其从 M a
转换为 IO a
,因此您实际上可以 运行 它来自 main
)。对于这个 monad,定义很简单,但最好定义一个函数,以防 M
monad 更复杂:
runM :: M a -> IO (Maybe a)
runM = runMaybeT
下面包含一个带有存根代码的完整工作示例。
对于你的第三个问题, 使它成为 "more functional" 不一定会使它更容易理解,但我的想法是利用像 [= 这样的 monad 运算符61=] 或像 <*>
这样的应用运算符来模拟 monadic 上下文中的函数形式。以下将是我的 userSignUp
的 monad 转换器版本的等效 "more functional" 形式。不清楚这个比上面的命令式"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!"