问题 IO monad可以防止嵌入式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

8543
2018-05-31 06:40


起源



答案:


不错的测试用例。这是发生了什么:

  • 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
    

    现在,从那以后 return 在IO是懒惰的,它会  触发 traceShow 返回IO动作时。所以,何时 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 价值因素 x 被迫。同 data 类型不会发生:值被包装在一个盒子里(例如 Id2 undefined 不等于 undefined)。

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

    return $ traceShow x $ Result x
    

    return 这里没有包装任何东西,而且 traceShow 立刻就会立即触发 mapM 运行。


10
2018-05-31 07:07



非常感谢你的回答,chi。请问您是如何知道mapM的IO定义是严格的并且返回是懒惰的? - NioBium
@铌 Failure >>= f = Failure 抛弃了 f:没有必要继续进行monadic链。 IO具有较低的杠杆定义,不易编写,但 - 除非例外 - action >>= f 永远都会打电话 f 因为人们期望例如 action >> print 4 最终将打印4无论如何 action 确实(除非IO例外和不终止)。 - chi
对。再次感谢! - NioBium


答案:


不错的测试用例。这是发生了什么:

  • 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
    

    现在,从那以后 return 在IO是懒惰的,它会  触发 traceShow 返回IO动作时。所以,何时 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 价值因素 x 被迫。同 data 类型不会发生:值被包装在一个盒子里(例如 Id2 undefined 不等于 undefined)。

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

    return $ traceShow x $ Result x
    

    return 这里没有包装任何东西,而且 traceShow 立刻就会立即触发 mapM 运行。


10
2018-05-31 07:07



非常感谢你的回答,chi。请问您是如何知道mapM的IO定义是严格的并且返回是懒惰的? - NioBium
@铌 Failure >>= f = Failure 抛弃了 f:没有必要继续进行monadic链。 IO具有较低的杠杆定义,不易编写,但 - 除非例外 - action >>= f 永远都会打电话 f 因为人们期望例如 action >> print 4 最终将打印4无论如何 action 确实(除非IO例外和不终止)。 - chi
对。再次感谢! - NioBium


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

如你所见,这只是一个 newtype 包装好 m (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 >>= f 跑了 m 在底层monad中计算,得到 Maybe 某事或其他。如果它得到 Nothing,它只是停止,返回 Nothing。如果它得到了什么,它就会传递给它 f 并运行结果。你也可以转 m 行动成“成功” MaybeT m 行动使用 lift 从 Control.Monad.Trans.Class

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)

1
2018-05-31 19:51