首页 文章

最简单的非平凡单子变换器例子为“假人”,IO也许

提问于
浏览
30

有人可以提供一个超级简单(几行)的monad变换器示例,这是非平凡的(即不使用Identity monad - 我理解) .

例如,有人会如何创建一个执行IO并可以处理失败的monad(可能)?

什么是最简单的例子来证明这一点?

我已经浏览了一些monad变换器教程,他们似乎都使用State Monad或Parsers或者复杂的东西(对于newbee) . 我想看到一些比这简单的东西 . 我认为IO也许很简单,但我自己并不知道怎么做 .

我怎么能使用IO Maybe monad堆栈?最重要的是什么?什么会在底部?为什么?

在什么样的用例中,人们想要使用IO Maybe monad还是Maybe IO monad?创造这样一个复合单子会有意义吗?如果是,何时以及为什么?

3 回答

  • 67

    这可用here作为.lhs文件 .

    MaybeT 变换器将允许我们打破monad计算,就像抛出异常一样 .

    我会先快点一些预赛 . 跳到 Adding Maybe powers to IO 以获得一个有效的例子 .

    首先进口一些:

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

    经验法则:

    在monad堆栈中IO始终位于底部 .

    其他类似IO的monad通常也会出现在底部,例如:国家变压器monad ST .

    MaybeT m是一种新的monad类型,它将Maybe monad的强大功能添加到monad m中 - 例如也许是IO .

    我们将进入后期的力量 . 现在,习惯于将 MaybeT IO 视为可能的IO monad堆栈 .

    就像IO Int是返回Int的monad表达式一样,MaybeT IO Int是一个返回Int的MaybeT IO表达式 .

    习惯阅读复合型签名是了解monad变形金刚的一半 .

    do块中的每个表达式都必须来自同一个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
                ...
    

    幸运的是,有一种方法可以解决这个问题 .

    要将IO表达式转换为MaybeT IO表达式,请使用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 .

    每个monad变压器都有“运行”功能 .

    run函数“运行”monad堆栈的最顶层,从内层返回一个值 .

    对于 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 ()
    

    也尝试运行:

    runMaybeT (forever mgreet)
    

    你需要使用Ctrl-C来摆脱循环 .

    到目前为止 mgreet 没有't do anything more than what we could do in IO. Now we' ll工作一个例子,它展示了将Maybe monad与IO混合的强大功能 .

    将IO添加到IO

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

    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 中的所有状态都具有相同的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)
    

    尝试运行 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 可以返回的唯一值是 Nothing ,这对应于断开循环 .

    根据情况,您可能会发现编写 loop2 而不是递归 loop1 更容易 .

  • 12

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

    手动检查一系列绑定中是否存在错误会产生可怕的“厄运的阶梯”:

    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独有的任何特定功能 . 稍微概括一下newtype,我们可以为任何潜在的monad做这个工作!

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

    从本质上讲,这就是 MaybeT 变压器works . 我遗漏了一些细节,比如如何为变压器实现 return ,以及如何将"lift" IO 值转换为 MaybeOverM IO 值 .

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

  • 7

    当然, 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)
    

    我们可以使用该函数的结果独立地操作monad,但是如果我们这样组成它:

    MaybeT . getDatabaseResult :: String -> MaybeT IO String
    

    我们可以忘记那个额外的monadic层,并将它视为一个普通的monad .

相关问题