如何在Maybe和IO中同时使用Do符号?

3

我正在努力掌握Haskell中的do表示法

我可以将其与Maybe一起使用,然后打印结果。像这样:

maybeAdd :: Maybe Integer
maybeAdd = do one <- maybe1
              two <- maybe2
              three <- maybe3
              return (one + two + three)

main :: IO ()
main = putStr (show $ fromMaybe 0 maybeAdd)

但是我试图在主函数中使用 Maybe 的 do 表达式而不是单独的函数,但是我没有成功。我尝试过的各种方法包括:

main :: IO ()
main = do one <- maybe1
          two <- maybe2
          three <- maybe3
          putStr (show $ fromMaybe 0 $ return (one + two + three))

main :: IO ()
main = do one <- maybe1
          two <- maybe2
          three <- maybe3
          putStr (show $ fromMaybe 0 $ Just (one + two + three))

main :: IO ()
main = do one <- maybe1
          two <- maybe2
          three <- maybe3
          putStr (show $ (one + two + three))

所有这些都会导致各种类型的编译错误,遗憾的是我没有解密出正确的方法。

我该如何实现上述目标?或许需要解释一下我尝试过的方法为什么是错误的吗?


1
正因为如此,才有了Monad Transformers这个抽象概念。对于你的情况,即MaybeT IO ()。然后你将拥有一个单一的do块。 - Redu
感谢@Redu的参与。我猜MaybeT可能存在于其他模块中。那会在哪里呢?它的使用会是什么样子的?现在的主函数类型会变成MaybeT IO()吗?我认为那不正确。 - Finlay Weber
我尝试通过以下回答来解决你的问题。 - Redu
2个回答

7

每个 do 块必须在单个 monad 中工作。如果您想使用多个 monad,可以使用多个 do 块。尝试调整您的代码:

main :: IO ()
main = do -- IO block
   let x = do -- Maybe block
          one <- maybe1
          two <- maybe2
          three <- maybe3
          return (one + two + three)
   putStr (show $ fromMaybe 0 x)

你甚至可以使用

main = do -- IO block
   putStr $ show $ fromMaybe 0 $ do -- Maybe block
      one <- maybe1
      two <- maybe2
      three <- maybe3
      return (one + two + three)
   -- other IO actions here

但在某些情况下,它可能会变得不太易读。


3
每个 do 块必须与一个单一的 monad 一起工作 - 我想这就是我之前缺失的部分。感谢您指出这一点! - Finlay Weber

3
MaybeT 也许会在这种特定情况下发挥作用。MaybeT 单子变换器只是一种类型定义,类似于:
newtype MaybeT m a = MaybeT {runMaybeT :: m (Maybe a)}

实际上,像MaybeTStateT等转换器已经在Control.Monad.Trans.MaybeControl.Monad.Trans.State等库中提供了。为了说明,它们的Monad实例可以像下面展示的那样:
instance Monad m => Monad (MaybeT m) where
  return  = MaybeT . return . Just
  x >>= f = MaybeT $ runMaybeT x >>= g
            where
            g Nothing  = return Nothing
            g (Just x) = runMaybeT $ f x

正如您所注意到的,单子函数 f 接受一个存储在 Maybe 单子中的值,该值本身在另一个单子中(在我们的例子中是 IO)。f 函数执行其任务并将结果封装回 MaybeT m a

此外,有一个 MonadTrans 类,其中可以拥有一些被变换器类型使用的公共功能。其中之一是 lift,它用于根据特定实例的定义将值提升到转换器中。对于 MaybeT,它应该如下所示:

instance MonadTrans MaybeT where
    lift = MaybeT . (liftM Just)

让我们使用单子变换器来执行您的任务。

addInts :: MaybeT IO ()
addInts = do
          lift $ putStrLn "Enter two integers.."
          i <- lift getLine
          guard $ test i
          j <- lift getLine
          guard $ test j
          lift . print $ (read i :: Int) + (read j :: Int)
          where
          test = and . (map isDigit)

所以,当像这样调用时
λ> runMaybeT addInts
Enter two integers..
1453
1571
3024
Just ()

问题在于,由于单子转换器也是Monad类型类的成员,人们可以无限嵌套它们,并仍然在一个do符号下执行操作。 编辑:回答被投票降低,但我不清楚为什么。如果方法有问题,请详细说明,以便帮助包括我在内的人学习更好的内容。
趁此机会进行编辑时,我想添加更好的代码,因为我认为基于Chartest可能不是最佳选择,因为它不会考虑负数Int。所以让我们尝试在处理Maybe类型时使用Text.Read包中的readMaybe
import Control.Monad.Trans.Maybe
import Control.Monad.Trans.Class (lift)
import Text.Read (readMaybe)

addInts :: MaybeT IO ()
addInts = do
          lift $ putStrLn "Enter two integers.."
          i <- lift getLine
          MaybeT $ return (readMaybe i :: Maybe Int)
          j <- lift getLine
          MaybeT $ return (readMaybe j :: Maybe Int)
          lift . print $ (read i :: Int) + (read j :: Int)

我想现在它的工作表现更佳了...

λ> runMaybeT addInts
Enter two integers..
-400
500
100
Just ()

λ> runMaybeT addInts
Enter two integers..
Not an Integer
Nothing

3
我理解你的意图,但我认为这个答案会更清晰,如果至少在它的初始示例中,它尝试更精确地匹配问题中的代码,省略guardgetLine。作为一个次要的风格建议,使用liftIO而不是lift来提升IO操作可能更好,因为它更容易转换为mtl代码,或者用于在IO之上使用多个变换器层。 - duplode
1
@duplode 好的,我尽力解释了转换器抽象化的概念,以及我理解 OP 可能会问到的问题和最终需要学习的内容。无论如何,如果代码没问题,我对我的负评感到满意,因为这也是我的学习实践的一部分。 - Redu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接