“傻瓜"的最简单的非平凡单子转换器示例,IO+Maybe [英] Simplest non-trivial monad transformer example for "dummies", IO+Maybe

查看:20
本文介绍了“傻瓜"的最简单的非平凡单子转换器示例,IO+Maybe的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

Could someone give a super simple (few lines) monad transformer example, which is non-trivial (i.e. not using the Identity monad - that I understand).

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

For example, how would someone create a monad that does IO and can handle failure (Maybe)?

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

What would be the simplest example that would demonstrate this?

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

I have skimmed through a few monad transformer tutorials and they all seem to use State Monad or Parsers or something complicated (for a newbee). I would like to see something simpler than that. I think IO+Maybe would be simple, but I don't really know how to do that myself.

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

How could I use an IO+Maybe monad stack? What would be on top? What would be on bottom? Why?

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

In what kind of use case would one want to use the IO+Maybe monad or the Maybe+IO monad? Would that make sense to create such a composite monad at all? If yes, when, and why?

推荐答案

可在此处以 .lhs 文件形式提供.

This is available here as a .lhs file.

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

The MaybeT transformer will allow us to break out of a monad computation much like throwing an exception.

我会先快速复习一些预备知识.跳到为 IO 添加可能的权力以获得一个有效的例子.

I'll first quickly go over some preliminaries. Skip down to Adding Maybe powers to IO for a worked example.

首先导入:

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

经验法则:

在 monad 堆栈中,IO 总是在底部.

In a monad stack IO is always on the bottom.

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

Other IO-like monads will also, as a rule, always appear on the bottom, e.g. the state transformer monad ST.

MaybeT m 是一种新的 monad 类型,它为 monad m 添加了 Maybe monad 的功能 - 例如可能是 IO.

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 视为可能+IO monad 堆栈.

We'll get into what that power is later. For now, get used to thinking of MaybeT IO as the maybe+IO monad stack.

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

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

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

Getting used to reading compound type signatures is half the battle to understanding monad transformers.

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

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

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

I.e. this works because each statement is in the 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 中:

This will not work because putStr is not in the MaybeT IO monad:

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

幸运的是有办法解决这个问题.

Fortunately there is a way to fix this.

要将IO 表达式转换为MaybeT IO 表达式,请使用liftIO.

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

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

liftIO is polymorphic, but in our case it has the type:

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.

Now all of the statement in mgreet are from the MaybeT IO monad.

每个 monad 转换器都有一个运行"功能.

Every monad transformer has a "run" function.

run 函数运行"monad 栈的最顶层,返回来自内部层的值.

The run function "runs" the top-most layer of a monad stack returning a value from the inside layer.

对于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 来跳出循环.

You'll need to use Ctrl-C to break out of the loop.

到目前为止,mgreet 并没有做比我们在 IO 中可以做的更多的事情.现在我们将研究一个演示混合功能的示例带有 IO 的 Maybe monad.

So far mgreet doesn't do anything more than what we could do in IO. Now we'll work on an example which demonstrates the power of mixing the Maybe monad with IO.

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

We'll start with a program which asks some questions:

 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 来回答问题.我们可能会处理它这样:

Now suppose we want to give the user the ability to end the survey early by typing END in response to a question. We might handle it this way:

 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 有熟悉的楼梯问题如果我们添加更多问题,则不会扩展.

The problem is that survey1 has the familiar staircasing issue which doesn't scale if we add more questions.

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

We can use the MaybeT monad transformer to help us here.

 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 类型.

Note how all of the statemens in askfor2 have the same monad type.

我们使用了一个新函数:

We've used a new function:

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

以下是类型的工作原理:

Here is how the types work out:

                  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.

Here return is from the IO-monad.

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

Now we can write our survey function like this:

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

尝试运行 survey2 并通过键入 END 作为对任一问题的回答来尽早结束问题.

Try running survey2 and ending the questions early by typing END as a response to either question.

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

I know I'll get comments from people if I don't mention the following short-cuts.

表达式:

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

也可以简单地写成:

return r                    -- return is from the MaybeT IO monad

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

Also, another way of writing MaybeT (return Nothing) is:

mzero

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

Furthermore, two consecutive liftIO statements may always combined into a single liftIO, e.g.:

do liftIO $ statement1
   liftIO $ statement2 

等同于:

liftIO $ do statement1
            statement2

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

With these changes our askfor2 function may be written:

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 的一种方式 - 就像抛出异常一样.

In a sense, mzero becomes a way of breaking out of the monad - like throwing an exception.

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

Consider this simple password asking loop:

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

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

This is a (tail) recursive function and works just fine.

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

In a conventional language we might write this as a infinite while loop with a break statement:

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

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

With MaybeT we can write the loop in the same manner as the Python code:

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,它对应于跳出循环.

The last return () continues execution, and since we are in a forever loop, control passes back to the top of the do block. Note that the only value that loop2 can return is Nothing which corresponds to breaking out of the loop.

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

Depending on the situation you might find it easier to write loop2 rather than the recursive loop1.

这篇关于“傻瓜"的最简单的非平凡单子转换器示例,IO+Maybe的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆