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

jhe*_*dus 39 monads haskell monad-transformers

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

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

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

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

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

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

Eri*_*ikR 81

这可以 .lhs文件中找到.

MaybeT变压器将使我们能够打出来单子计算就像抛出异常.

我会先快点一些预赛.跳过了添加也许权力IO的工作实例.

首先进口一些:

 import Control.Monad
 import Control.Monad.Trans
 import Control.Monad.Trans.Maybe
Run Code Online (Sandbox Code Playgroud)

经验法则:

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

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

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

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

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

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

do块中的每个表达式都必须来自同一个monad.

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

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

这不起作用,因为putStr不在MaybeT IOmonad中:

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

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

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 ()
Run Code Online (Sandbox Code Playgroud)

现在所有的声明mgreet都来自MaybeT IOmonad.

每个monad变换器都具有"运行"功能.

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

对于MaybeT IO,运行功能是:

runMaybeT :: MaybeT IO a -> IO (Maybe a)
Run Code Online (Sandbox Code Playgroud)

例:

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

ghci> runMaybeT mgreet
What is your name? user5402
Hello, user5402
Just ()
Run Code Online (Sandbox Code Playgroud)

也尝试运行:

runMaybeT (forever mgreet)
Run Code Online (Sandbox Code Playgroud)

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

到目前为止mgreet,除了我们在IO中可以做的事情之外,什么都没做.现在我们将研究一个示例,该示例演示了将Maybe monad与IO混合的强大功能.

向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)
Run Code Online (Sandbox Code Playgroud)

现在假设我们希望通过在回答问题时输入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))
Run Code Online (Sandbox Code Playgroud)

问题是,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
Run Code Online (Sandbox Code Playgroud)

请注意所有状态都askfor2具有相同的monad类型.

我们使用了一个新功能:

MaybeT :: IO (Maybe a) -> MaybeT IO a
Run Code Online (Sandbox Code Playgroud)

以下是这些类型的解决方法:

                  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
Run Code Online (Sandbox Code Playgroud)

return是来自IO-monad.

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

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

尝试运行survey2并提前结束问题,输入END作为对任一问题的回答.

捷径

我知道如果我不提及以下捷径,我会得到人们的评论.

表达方式:

MaybeT (return (Just r))    -- return is from the IO monad
Run Code Online (Sandbox Code Playgroud)

也可以简单地写成:

return r                    -- return is from the MaybeT IO monad
Run Code Online (Sandbox Code Playgroud)

另外,另一种写作方式MaybeT (return Nothing)是:

mzero
Run Code Online (Sandbox Code Playgroud)

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

do liftIO $ statement1
   liftIO $ statement2 
Run Code Online (Sandbox Code Playgroud)

是相同的:

liftIO $ do statement1
            statement2
Run Code Online (Sandbox Code Playgroud)

通过这些更改,我们的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
Run Code Online (Sandbox Code Playgroud)

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

另一个例子

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

loop1 = do putStr "Password:"
           p <- getLine
           if p == "SECRET"
             then return ()
             else loop1
Run Code Online (Sandbox Code Playgroud)

这是一个(尾部)递归函数,工作得很好.

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

def loop():
    while True:
        p = raw_prompt("Password: ")
        if p == "SECRET":
            break
Run Code Online (Sandbox Code Playgroud)

使用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 ()
Run Code Online (Sandbox Code Playgroud)

最后一次return ()继续执行,因为我们处于forever循环中,所以控制权传递回do块的顶部.请注意,唯一loop2可以返回的值是Nothing对应于断开循环.

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

  • 我创建了这个要点:https://gist.github.com/jhegedus42/26917d2abded3e82f427,这样这个小 perl 代码就不会丢失:) (2认同)
  • 感谢您不遗余力地编写一个详尽且解释清楚的示例.+1 (2认同)

dan*_*iaz 13

假设你有工作IO的值是"可能会失败,"在某种意义上,像 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
Run Code Online (Sandbox Code Playgroud)

如何以某种方式"自动化"这个?也许我们可以IO (Maybe a)使用绑定函数设计一个newtype ,它会自动检查第一个参数是否为Nothing内部IO,这样就省去了自己检查它的麻烦.就像是

newtype MaybeOverIO a = MaybeOverIO { runMaybeOverIO :: IO (Maybe a) }
Run Code Online (Sandbox Code Playgroud)

使用绑定功能:

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)
Run Code Online (Sandbox Code Playgroud)

这有效!而且,仔细观察它,我们意识到我们没有使用IOmonad 独有的任何特定功能.稍微概括一下newtype,我们可以为任何潜在的monad做这个工作!

newtype MaybeOverM m a = MaybeOverM { runMaybeOverM :: m (Maybe a) }
Run Code Online (Sandbox Code Playgroud)

从本质上讲,这就是MaybeT变压器的方式工作原理.我遗漏了一些细节,比如如何实现return变压器,以及如何将IO值"提升" 到MaybeOverM IO值中.

请注意 MaybeOverIO有一种* -> *同时MaybeOverM具有那种(* -> *) -> * -> *(因为它的第一个"类型参数"是一个单子类型构造,本身需要一个"类型参数").


AJF*_*mar 7

当然,MaybeTmonad变压器是:

newtype MaybeT m a = MaybeT {unMaybeT :: m (Maybe a)}
Run Code Online (Sandbox Code Playgroud)

我们可以这样实现它的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)
Run Code Online (Sandbox Code Playgroud)

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

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

getDatabaseResult :: String -> IO (Maybe String)
Run Code Online (Sandbox Code Playgroud)

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

MaybeT . getDatabaseResult :: String -> MaybeT IO String
Run Code Online (Sandbox Code Playgroud)

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