如何使用Monad变形金刚来组合不同的(纯粹的和不纯的)单子?

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(在本例中为Maybe和IO),以便我可以组合它们的内容并生成一个可能包含结果或可能的错误的统一结果?
  • 如何以更容易理解的功能方式表达这样的问题?

K. *_*uhr 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
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模式"和"可能模式"之间切换.

重写代码以使用变换器涉及MaybeTtransformers包中导入变换器并定义自己的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?甚至不问.)

完整的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!"
Run Code Online (Sandbox Code Playgroud)