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