【问题标题】:IO monad prevents short circuiting of embedded mapM?IO monad 防止嵌入式 mapM 短路?
【发布时间】:2016-09-29 01:02:01
【问题描述】:

下面的代码有点迷惑。在问题的非玩具版本中,我试图在 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

【问题讨论】:

    标签: haskell lazy-evaluation traversal io-monad strictness


    【解决方案1】:

    很好的测试用例。以下是正在发生的事情:

    • mapM compute 中,我们像往常一样看到了工作中的懒惰。这并不奇怪。

    • mapM compute2 中,我们在 IO monad 中工作,其 mapM 定义将要求整个列表:不像 Result 会在找到 Failure 后立即跳过列表尾部,IO 将始终扫描整个列表。注意代码:

      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 时,在运行时不涉及装箱/拆箱(AKA 提升),因此强制使用 Id1 x 值会导致强制使用 xdata 类型不会发生这种情况:值被包装在一个盒子中(例如,Id2 undefined 不等于 undefined)。

      在您的示例中,您添加了一个 Identity 构造函数,但它来自 newtype Identity!!所以,打电话的时候

      return $ traceShow x $ Result x
      

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

    【讨论】:

    • 非常感谢您的回答,智。请问你怎么知道mapM的IO定义是严格的,return是惰性的?
    • @NioBium Failure &gt;&gt;= f = Failure 丢弃了f:没有必要在单子链上继续前进。 IO 有一个不容易编写的低级别定义,但是——除非有例外——action &gt;&gt;= f 将始终调用f,因为人们期望例如无论action 做什么,action &gt;&gt; print 4 最终都会打印 4(除非 IO 异常和非终止)。
    • 对。再次感谢!
    【解决方案2】:

    您的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) }
    

    如您所见,这只是newtypem (Maybe a) 的包装。但它的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 &gt;&gt;= f 在底层 monad 中运行 m 计算,得到 Maybe 或其他东西。如果它得到Nothing,它就会停止,返回Nothing。如果它得到一些东西,它会将它传递给f 并运行结果。您还可以使用来自Control.Monad.Trans.Classlift 将任何m 操作转换为“成功的”MaybeT m 操作:

    class MonadTrans t where
      lift :: Monad m => m a -> t m a
    
    instance MonadTrans MaybeT where
      lift m = MaybeT $ Just <$> m
    

    你也可以使用这个类,定义在 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)
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-08-15
      • 2014-06-07
      • 2013-08-15
      • 1970-01-01
      • 2012-08-30
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多