IO monad 防止嵌入式 mapM 短路?

IO monad prevents short circuiting of embedded mapM?

下面的代码让我有些迷惑。在问题的非玩具版本中,我试图在 monad Result 中进行 monadic 计算,其值只能从 IO 中构造。似乎 IO 背后的魔力使此类计算变得严格,但我无法弄清楚这是如何发生的。

代码:

data Result a = Result a | Failure deriving (Show)

instance Functor Result where
  fmap f (Result a) = Result (f a)
  fmap f Failure = Failure

instance Applicative Result where
  pure = return
  (<*>) = ap

instance Monad Result where
  return = Result
  Result a >>= f = f a
  Failure >>= _ = Failure

compute :: Int -> Result Int
compute 3 = Failure
compute x = traceShow x $ Result x

compute2 :: Monad m => Int -> m (Result Int)
compute2 3 = return Failure
compute2 x = traceShow x $ return $ Result x

compute3 :: Monad m => Int -> m (Result Int)
compute3 = return . compute

main :: IO ()
main = do
  let results = mapM compute [1..5]
  print $ results
  results2 <- mapM compute2 [1..5]
  print $ sequence results2
  results3 <- mapM compute3 [1..5]
  print $ sequence results3
  let results2' = runIdentity $ mapM compute2 [1..5]
  print $ sequence results2'

输出:

1
2
Failure
1
2
4
5
Failure
1
2
Failure
1
2
Failure

不错的测试用例。这是正在发生的事情:

  • mapM compute 中,我们像往常一样看到工作中的懒惰。不足为奇。

  • mapM compute2 中我们在 IO monad 内部工作,其 mapM 定义将需要整个列表:不像 Result 那样跳过列表的尾部一旦找到 FailureIO 将始终扫描整个列表。注意代码:

    compute2 x = traceShow x $ return $ Result x
    

    因此,一旦访问 IO 操作列表的每个元素,上面的代码就会打印调试消息。都是,所以我们打印一切。

  • mapM compute3我们现在用的,大致是:

    compute3 x = return $ traceShow x $ Result x
    

    现在,由于IO中的return是惰性的,它不会不会在返回IO动作时触发traceShow。因此,当 mapM compute3 为 运行 时, 看不到消息 。相反,我们仅在 sequence results3 为 运行 时才看到消息,这会强制 Result -- 不是所有消息,而是仅根据需要显示消息。

  • 最后的 Identity 示例也很棘手。请注意:

    > newtype Id1 a = Id1 a
    > data Id2 a = Id2 a
    > Id1 (trace "hey!" True) `seq` 42
    hey!
    42
    > Id2 (trace "hey!" True) `seq` 42
    42
    

    当使用 newtype 时,在 运行 时不涉及 boxing/unboxing(也称为提升),因此强制使用 Id1 x 值会导致 x被迫。对于 data 类型,这不会发生:值被包裹在一个盒子中(例如 Id2 undefined 不等同于 undefined)。

    在您的示例中,您添加了一个 Identity 构造函数,但它来自 newtype Identity!!所以,当调用

    return $ traceShow x $ Result x
    

    这里的return不包裹任何东西,traceShow一旦mapM为运行立即触发。

您的 Result 类型似乎与 Maybe 几乎相同,

Result <-> Just
Failure <-> Nothing

为了我可怜的大脑,我将在本回答的其余部分坚持使用 Maybe 术语。

chi 解释了为什么 IO (Maybe a) 没有按照您预期的方式短路。但是 一种可以用于此类事情的类型!事实上,它本质上是相同的类型,但具有不同的 Monad 实例。您可以在 Control.Monad.Trans.Maybe 中找到它。它看起来像这样:

newtype MaybeT m a = MaybeT
  { runMaybeT :: m (Maybe a) }

如您所见,这只是 m (Maybe a)newtype 包装器。但它的 Monad 实例非常不同:

instance Monad m => Monad (MaybeT m) where
  return a = MaybeT $ return (Just a)
  m >>= f = MaybeT $ do
    mres <- runMaybeT m
    case mres of
      Nothing -> return Nothing
      Just a -> runMaybeT (f a)

也就是说,m >>= f 在底层 monad 中运行 m 计算,得到 Maybe 某些东西。如果它得到 Nothing,它就停止,返回 Nothing。如果它得到一些东西,它会将它传递给 f 并运行结果。您还可以使用来自 Control.Monad.Trans.Class:

lift 将任何 m 操作转换为 "successful" MaybeT m 操作
class MonadTrans t where
  lift :: Monad m => m a -> t m a

instance MonadTrans MaybeT where
  lift m = MaybeT $ Just <$> m

您也可以使用这个 class,定义在某个地方,如 Control.Monad.IO.Class,通常更清晰,也更方便:

class MonadIO m where
  liftIO :: IO a -> m a

instance MonadIO IO where
  liftIO m = m

instance MonadIO m => MonadIO (MaybeT m) where
  liftIO m = lift (liftIO m)