11

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

2 回答 2

10

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

  • 像往常一样,mapM compute我们看到工作中的懒惰。这里没有惊喜。

  • 我们在mapM compute2IO monad 内部工作,它的mapM定义将要求整个列表:不像Result一旦Failure找到就跳过列表的尾部,IO它将始终扫描整个列表。注意代码:

    compute2 x = traceShow x $ return $ Result x
    

    因此,只要访问了 IO 操作列表的每个元素,上述内容就会打印调试消息。都是,所以我们打印所有内容。

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

    compute3 x = return $ traceShow x $ Result x
    

    现在,由于returnin 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
    

    使用 a 时newtype,在运行时不涉及装箱/拆箱(AKA 提升),因此强制一个Id1 x值会导致x强制。对于data类型,这不会发生:值被包装在一个盒子中(例如Id2 undefined,不等同于undefined)。

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

    return $ traceShow x $ Result x
    

    这里return不包装任何东西,并且traceShow一旦mapM运行就会立即触发。

于 2016-05-31T07:07:39.113 回答
1

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

也就是说,在底层 monad 中m >>= f运行计算,得到一些东西。如果它得到,它就会停止,返回。如果它得到一些东西,它将传递给并运行结果。您还可以使用from将任何操作转换为“成功”操作:mMaybeNothingNothingfmMaybeT mliftControl.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)
于 2016-05-31T19:51:09.283 回答