IO monad可以防止嵌入式mapM的短路?

Nio*_*ium 11 haskell traversal lazy-evaluation strictness io-monad

以下代码有点神秘.在非玩具版本的问题中,我试图在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'
Run Code Online (Sandbox Code Playgroud)

输出:

1
2
Failure
1
2
4
5
Failure
1
2
Failure
1
2
Failure
Run Code Online (Sandbox Code Playgroud)

chi*_*chi 10

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

  • mapM compute我们像往常一样在工作中看到懒惰.这里不足为奇.

  • mapM compute2我们的IO单子,其内部工作mapM的定义将要求整个列表:不像Result其尽快跳过列表的尾部Failure被发现,IO总是会扫描整个表.注意代码:

    compute2 x = traceShow x $ return $ Result x
    
    Run Code Online (Sandbox Code Playgroud)

    因此,一旦访问IO动作列表的每个元素,上面将打印调试消息.一切都是,所以我们打印一切.

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

    compute3 x = return $ traceShow x $ Result x
    
    Run Code Online (Sandbox Code Playgroud)

    现在,因为return在IO中是懒惰的,所以在返回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
    
    Run Code Online (Sandbox Code Playgroud)

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

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

    return $ traceShow x $ Result x
    
    Run Code Online (Sandbox Code Playgroud)

    return这里不换任何东西,而traceShow一旦被立即触发mapM运行.

  • @NioBium`Wailed >> = f = Failure`丢弃`f`:没有必要继续进行monadic链.IO有一个较低的杠杆定义,不容易写,但是 - 除了例外 - `action >> = f`将始终调用`f`,因为人们期望例如`action >> print 4`最终将打印4什么`动作`做(禁止IO异常和非终止). (3认同)