"dummies"、IO+Maybe 的最简单的非平凡 monad 转换器示例

Simplest non-trivial monad transformer example for "dummies", IO+Maybe

有人可以给出一个非常简单(几行)的 monad 转换器示例,这是非常重要的(即不使用 Identity monad - 我理解)。

例如,某人将如何创建一个执行 IO 并且可以处理故障(可能)的 monad?

可以证明这一点的最简单示例是什么?

我浏览了一些 monad 转换器教程,它们似乎都使用了 State Monad 或 Parsers 或一些复杂的东西(对于新手)。我希望看到比这更简单的东西。我认为 IO+Maybe 会很简单,但我自己真的不知道该怎么做。

如何使用 IO+Maybe monad 堆栈? 什么会在上面?底部会是什么?为什么?

在什么样的用例中会想要使用 IO+Maybe monad 或 Maybe+IO monad?创建这样一个复合 monad 是否有意义?如果是,什么时候,为什么?

当然,MaybeT monad 转换器是:

newtype MaybeT m a = MaybeT {unMaybeT :: m (Maybe a)}

我们可以这样实现它的 monad 实例:

instance (Monad m) => Monad (MaybeT m) where
    return a = MaybeT (return (Just a))

    (MaybeT mmv) >>= f = MaybeT $ do
        mv <- mmv
        case mv of
            Nothing -> return Nothing
            Just a  -> unMaybeT (f a)

这将允许我们执行 IO,并在某些情况下选择优雅地失败。

例如,假设我们有这样一个函数:

getDatabaseResult :: String -> IO (Maybe String)

我们可以用那个函数的结果独立地操作 monads,但是如果我们这样组合它:

MaybeT . getDatabaseResult :: String -> MaybeT IO String

我们可以忘记那个额外的 monadic 层,把它当作一个普通的 monad。

假设您必须使用在某种意义上 "may fail" 的 IO 值,例如 foo :: IO (Maybe a)func1 :: a -> IO (Maybe b)func2 :: b -> IO (Maybe c)

手动检查绑定链中是否存在错误会迅速产生可怕的 "staircase of doom":

do
    ma <- foo
    case ma of
        Nothing -> return Nothing
        Just a -> do
            mb <- func1 a
            case mb of
                Nothing -> return Nothing
                Just b -> func2 b

如何以某种方式 "automate" 这个?或许我们可以围绕 IO (Maybe a) 设计一个带有绑定函数的新类型,该函数会自动检查第一个参数是否是 IO 中的 Nothing,从而省去我们自己检查它的麻烦。像

newtype MaybeOverIO a = MaybeOverIO { runMaybeOverIO :: IO (Maybe a) }

绑定函数:

betterBind :: MaybeOverIO a -> (a -> MaybeOverIO b) -> MaybeOverIO b
betterBind mia mf = MaybeOverIO $ do
       ma <- runMaybeOverIO mia
       case ma of
           Nothing -> return Nothing
           Just a  -> runMaybeOverIO (mf a)

这有效!而且,更仔细地观察它,我们意识到我们没有使用 IO monad 独有的任何特定函数。稍微概括一下新类型,我们可以使它适用于任何底层 monad!

newtype MaybeOverM m a = MaybeOverM { runMaybeOverM :: m (Maybe a) }

本质上,这就是 MaybeT 转换器 works 的方式。我遗漏了一些细节,比如如何为转换器实现 return,以及如何将 "lift" IO 值转换为 MaybeOverM IO 值。

注意 MaybeOverIO 有种类 * -> *MaybeOverM 有种类 (* -> *) -> * -> * (因为它的第一个 "type argument" 是一个 monad 类型构造函数,它本身需要"type argument").

这是可用的 here 作为 .lhs 文件。

MaybeT 转换器将允许我们像抛出异常一样打破 monad 计算。

我先快速回顾一下一些预备知识。跳到 为 IO 添加 Maybe 的权力 以获得一个可行的例子。

首先导入一些:

 import Control.Monad
 import Control.Monad.Trans
 import Control.Monad.Trans.Maybe

经验法则:

In a monad stack IO is always on the bottom.

其他类似 IO 的 monad 通常也会出现在底部,例如状态转换器 monad ST.

MaybeT m is a new monad type which adds the power of the Maybe monad to the monad m - e.g. MaybeT IO.

我们稍后会讨论这种力量是什么。现在,习惯于将 MaybeT IO 视为 maybe+IO monad 堆栈。

Just like IO Int is a monad expression returning an Int, MaybeT IO Int is a MaybeT IO expression returning an Int.

习惯阅读复合类型签名是理解 monad 转换器的一半。

Every expression in a do block must be from the same monad.

即这是有效的,因为每个语句都在 IO-monad:

 greet :: IO ()                               -- type:
 greet = do putStr "What is your name? "      -- IO ()
            n <- getLine                      -- IO String
            putStrLn $ "Hello, " ++ n         -- IO ()

这将不起作用,因为 putStr 不在 MaybeT IO monad 中:

mgreet :: MaybeT IO ()
mgreet = do putStr "What is your name? "    -- IO monad - need MaybeT IO here
            ...

幸好有办法解决这个问题。

To transform an IO expression into a MaybeT IO expression use liftIO.

liftIO 是多态的,但在我们的例子中它的类型是:

liftIO :: IO a -> MaybeT IO a

 mgreet :: MaybeT IO ()                             -- types:
 mgreet = do liftIO $ putStr "What is your name? "  -- MaybeT IO ()
             n <- liftIO getLine                    -- MaybeT IO String
             liftIO $ putStrLn $ "Hello, " ++ n     -- MaybeT IO ()

现在 mgreet 中的所有语句都来自 MaybeT IO monad。

Every monad transformer has a "run" function.

运行 函数 "runs" monad 堆栈的最顶层 returning 来自内层的值。

对于MaybeT IO,运行函数是:

runMaybeT :: MaybeT IO a -> IO (Maybe a)

示例:

ghci> :t runMaybeT mgreet 
mgreet :: IO (Maybe ())

ghci> runMaybeT mgreet
What is your name? user5402
Hello, user5402
Just ()

也试试 运行ning:

runMaybeT (forever mgreet)

您需要使用 Ctrl-C 来跳出循环。

到目前为止 mgreet 没有做任何我们在 IO 中可以做的事情。 现在我们将通过一个例子来展示混合的力量 带有 IO 的 Maybe monad。

为 IO 添加 Maybe 的权力

我们将从提出一些问题的程序开始:

 askfor :: String -> IO String
 askfor prompt = do
   putStr $ "What is your " ++ prompt ++ "? "
   getLine

 survey :: IO (String,String)
 survey = do n <- askfor "name"
             c <- askfor "favorite color"
             return (n,c)

现在假设我们想让用户能够结束调查 尽早输入 END 来回答问题。我们可能会处理它 这样:

 askfor1 :: String -> IO (Maybe String)
 askfor1 prompt = do
   putStr $ "What is your " ++ prompt ++ " (type END to quit)? "
   r <- getLine
   if r == "END"
     then return Nothing
     else return (Just r)

 survey1 :: IO (Maybe (String, String))
 survey1 = do
   ma <- askfor1 "name"
   case ma of
     Nothing -> return Nothing
     Just n  -> do mc <- askfor1 "favorite color"
                   case mc of
                     Nothing -> return Nothing
                     Just c  -> return (Just (n,c))

问题是 survey1 有熟悉的楼梯问题 如果我们添加更多问题,则不会扩展。

我们可以使用 MaybeT monad 转换器来帮助我们。

 askfor2 :: String -> MaybeT IO String
 askfor2 prompt = do
   liftIO $ putStr $ "What is your " ++ prompt ++ " (type END to quit)? "
   r <- liftIO getLine
   if r == "END"
     then MaybeT (return Nothing)    -- has type: MaybeT IO String
     else MaybeT (return (Just r))   -- has type: MaybeT IO String

请注意 askfor2 中的所有 statemens 如何具有相同的 monad 类型。

我们使用了一个新功能:

MaybeT :: IO (Maybe a) -> MaybeT IO a

以下是类型的计算方式:

                  Nothing     :: Maybe String
           return Nothing     :: IO (Maybe String)
   MaybeT (return Nothing)    :: MaybeT IO String

                 Just "foo"   :: Maybe String
         return (Just "foo")  :: IO (Maybe String)
 MaybeT (return (Just "foo")) :: MaybeT IO String

这里return来自IO-monad。

现在我们可以这样写我们的调查函数了:

 survey2 :: IO (Maybe (String,String))
 survey2 =
   runMaybeT $ do a <- askfor2 "name"
                  b <- askfor2 "favorite color"
                  return (a,b)

尝试 运行ning survey2 并通过键入 END 作为对任一问题的答复来提前结束问题。

捷径

我知道如果我不提及以下快捷方式,我会收到人们的评论。

表达式:

MaybeT (return (Just r))    -- return is from the IO monad

也可以简单写成:

return r                    -- return is from the MaybeT IO monad

另外,MaybeT (return Nothing)的另一种写法是:

mzero

此外,两个连续的 liftIO 语句总是可以组合成一个 liftIO,例如:

do liftIO $ statement1
   liftIO $ statement2 

等同于:

liftIO $ do statement1
            statement2

通过这些更改,我们的 askfor2 函数可以写成:

askfor2 prompt = do
  r <- liftIO $ do
         putStr $ "What is your " ++ prompt ++ " (type END to quit)?"
         getLine
  if r == "END"
    then mzero      -- break out of the monad
    else return r   -- continue, returning r

从某种意义上说,mzero 成为一种突破 monad 的方式——就像抛出异常一样。

另一个例子

考虑这个简单的密码询问循环:

loop1 = do putStr "Password:"
           p <- getLine
           if p == "SECRET"
             then return ()
             else loop1

这是一个(尾部)递归函数,工作正常。

在传统语言中,我们可以将其写成带有 break 语句的无限 while 循环:

def loop():
    while True:
        p = raw_prompt("Password: ")
        if p == "SECRET":
            break

使用 MaybeT 我们可以用与 Python 代码相同的方式编写循环:

loop2 :: IO (Maybe ())
loop2 = runMaybeT $
          forever $
            do liftIO $ putStr "Password: "
               p <- liftIO $ getLine
               if p == "SECRET"
                 then mzero           -- break out of the loop
                 else return ()

最后的 return () 继续执行,并且由于我们处于 forever 循环中,控制权返回到 do 块的顶部。请注意,loop2 可以 return 的唯一值是 Nothing,它对应于跳出循环。

根据具体情况,您可能会发现编写 loop2 比递归 loop1.

更容易