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
那样跳过列表的尾部一旦找到 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
时,在 运行 时不涉及 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)
下面的代码让我有些迷惑。在问题的非玩具版本中,我试图在 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
那样跳过列表的尾部一旦找到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
时,在 运行 时不涉及 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)