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

时间:2017-10-09 15:18:05

标签: haskell functional-programming monads 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

问题:

  • 如果insertUser没有产生输出,如何避免打印到控制台等操作(如IO辅助函数中所做的那样)。更具体地说,如何创建一个&#34;零&#34; IO monad的价值?
  • 如何组合两种不同类型的monad(在本例中为Maybe和IO),以便我可以组合它们的内容并生成一个可能包含结果或可能包含错误的统一结果?
  • 如何以功能性的方式表达这样的问题?

1 个答案:

答案 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 xx 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?甚至不要问。)

完整的MaybeT示例

moreFunctionalUserSignUp :: Connection -> User -> M ()
moreFunctionalUserSignUp conn user
  = join $ createUser conn
           <*> (setPassword user <$> encryptPassword (password user))
  where
    setPassword u p = u { password = p }