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

时间:2015-09-15 06:28:38

标签: haskell monads monad-transformers

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

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

最简单的例子是什么??

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

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

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

3 个答案:

答案 0 :(得分:72)

这是{。3}}可用的.lhs文件。

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

我先快点过一些预赛。对于一个有效的例子,请跳到将可能的权力添加到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 - 例如MaybeT IO

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

  

就像IO Int是一个返回Int的monad表达式一样,MaybeT IO IntMaybeT 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 ()

这不起作用,因为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变压器都有一个&#34; run&#34;功能

运行功能&#34;运行&#34;返回的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除了我们在IO中可以做的事情之外什么都做不了。 现在我们将展示一个展示混合能力的例子 带有IO的Maybe monad。

将可选功能添加到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

答案 1 :(得分:13)

假设您必须使用IO值,这些值可能会失败&#34;在某种意义上,例如foo :: IO (Maybe a)func1 :: a -> IO (Maybe b)func2 :: b -> IO (Maybe c)

手动检查一系列绑定中是否存在错误会产生可怕的厄运楼梯&#34;:

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

如何&#34;自动化&#34;这在某种程度上?也许我们可以使用绑定函数在IO (Maybe a)周围设计一个新类型,该函数会自动检查Nothing内的第一个参数是否为IO,从而避免了我们自己检查它的麻烦。像

这样的东西
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,以及如何提升&#34; IO值为MaybeOverM IO个值。

请注意,MaybeOverIO* -> *种,而MaybeOverM(* -> *) -> * -> *种类(因为它的第一个&#34;类型参数&#34;是monad类型的构造函数,本身需要一个&#34;类型的参数&#34;)。

答案 2 :(得分: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)

这将允许我们在某些情况下执行优先级失败的选项。

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

getDatabaseResult :: String -> IO (Maybe String)

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

MaybeT . getDatabaseResult :: String -> MaybeT IO String

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