【问题标题】:How to use Monad Transformers to combine different (both pure and impure) monads?如何使用 Monad Transformers 组合不同的(纯的和不纯的)monad?
【发布时间】:2017-10-09 15:18:04
【问题描述】:

我正在编写我的第一个 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 创建一个“零”值?
  • 如何组合两种不同类型的 monad(在本例中为 Maybe 和 IO),以便我可以组合它们的内容并生成一个可能包含结果或可能错误的统一结果?
  • 如何以更易于理解的函数式方式表达这样的问题?

【问题讨论】:

  • 正如所写,您的问题还不清楚。一般来说,“如何改进”是一个模棱两可的问题,并且可能会因为基于意见而被关闭(这类问题通常用于代码审查 SE)。什么是“意外情况”?什么不符合您的预期?
  • result &lt;- ...; return result 相当于根本不绑定结果;关于您关于代码审查的问题 - 存在一个专门用于该目的的单独的 se 站点 - codereview.stackexchange.com - 我建议迁移这个问题
  • 您可以使用return ()为任何Monad m(包括IO)创建m ()
  • 感谢大家的建议。我确实觉得这是 StackOverflow 的问题,而不是代码审查请求。我将编辑我的问题文本以澄清这一点并使其更加明显。很抱歉在第一次尝试时不清楚这一点。

标签: haskell functional-programming monads monad-transformers


【解决方案1】:

编辑:更新答案以匹配您更新的问题。

为了清楚起见,您实际上并没有在代码示例中使用任何 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 中。然后,所有绑定 (&gt;&gt;=) 和 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 负责确保由&lt;- 运算符绑定的每个变量都已经检查了Nothing。如果任何步骤返回Nothing,则处理将中止。如果一个步骤返回Just xx 将被自动解包。您通常不必处理(甚至查看)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

下面包含一个完整的带有存根代码的完整工作示例。

对于您的第三个问题,使其“更具功能性”并不一定会使其更易于理解,但我们的想法是使用像 =&lt;&lt; 这样的单子运算符或像 @ 这样的应用运算符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!"

【讨论】:

  • 另外,我应该提到,大多数人更喜欢MaybeT 的替代品,它允许返回对出错原因的解释,即错误消息,无论是纯字符串还是结构化异常。有些人为此使用ExceptT(以前的ErrorT)。
  • 感谢您的出色回答。是的,我意识到我没有使用任何单子变压器,但这就是我想要使用的。我真的不知道这就是monad转换器。您在回答中向我说明了这一点。
  • 关于您的回答,我还有一个简短的问题。 userSignup 函数中的 M () 输出就是所谓的 Free Monad,对吧?既然它不是真正的计算,而是一组可以运行的计算?
猜你喜欢
  • 1970-01-01
  • 2012-10-12
  • 2019-10-03
  • 2015-12-10
  • 2011-07-01
  • 1970-01-01
  • 2015-12-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多