使用mapM保留懒惰

时间:2017-01-23 18:03:29

标签: haskell monads

考虑以下简单的IO功能:

req :: IO [Integer]
req = do
  print "x"
  return [1,2,3]

实际上,这可能是一个http请求,它在解析结果后返回一个列表。

我试图以懒惰的方式连接该函数的几个调用的结果。

简单来说,以下内容应仅打印'x'两次:

fmap (take 4) req'
--> [1, 2, 3, 4]

我认为这可以通过sequencemapM解决,但我的方法在懒惰方面失败了:

import Control.Monad

req' :: IO [Integer]
req' = fmap concat $ mapM req [1..1000] -- should be infinite..

这会得到正确的结果,但IO函数req被调用1000次而不是必要的2次。当使用无限列表上的映射实现上述内容时,评估不会终止。

5 个答案:

答案 0 :(得分:6)

简短版本:

您不应该这样做,请查看流式IO库,例如pipesconduit

长版:

你做不到。或者至少,你真的不应该。允许懒惰评估的代码有副作用通常是一个非常糟糕的主意。不仅很快就会很难推断出效果何时以及执行多少次,但更糟糕的是,效果可能无法按照您期望的顺序执行!使用纯代码,这不是什么大问题。使用副作用代码,这是一场灾难。

想象一下,您想要从引用中读取值,然后使用更新的值替换该值。在IO monad中,计算顺序很明确,这很容易:

main = do
  yesterdaysDate <- readIORef ref
  writeIORef ref todaysDate

但是,如果上面的代码是懒惰地进行评估,则不能保证在写入之前读取引用 - 或者甚至根本不执行两个计算。程序的语义完全取决于我们是否以及何时需要计算结果。这是首先提出monad的原因之一:为程序员提供一种编写带副作用的代码的方法,这些副作用以明确定义且易于理解的顺序执行。

现在,如果使用unsafeInterleaveIO创建列表, 实际上可能会延迟连接列表:

import System.IO.Unsafe

req :: IO [Integer]
req = unsafeInterleaveIO $ do
    print "x"
    return [1,2,3]

req' :: IO [Integer]
req' = fmap concat $ mapM (const req) [1..1000]

这将导致req的每个应用程序被推迟,直到需要相应的子列表。然而,像这样懒洋洋地执行IO可能会导致有趣的竞争条件和资源泄漏,并且通常不赞成。建议的替代方法是使用评论中提到的conduitpipes等流式IO库。

答案 1 :(得分:3)

以下是使用streamingpipes库执行此类操作的方法。管道程序与使用管道编写的程序有些类似,特别是在这种情况下。 conduit使用不同的名称,而pipesconduit的类型和运算符比streaming更有趣;但这真的是你使用的漠不关心的问题。我认为这个的情况基本上更简单streaming;该表述在结构上与相应的IO [a]程序类似,实际上通常更简单。关键点在于Stream (Of Integer) IO ()Integer的列表完全相同,但它的构建方式是列表或流的元素可以来自连续的IO操作。

我在下面给了req一个论点,因为这似乎就是你想到的。

import Streaming
import qualified Streaming.Prelude as S
import Streaming.Prelude (for, each)

req :: Integer -> Stream (Of Integer) IO ()
req x = do                   -- this 'stream' is just a list of Integers arising in IO
  liftIO $ putStr "Sending request #" >> print x
  each [x..x+2]               

req' :: Stream (Of Integer) IO ()
req' = for (S.each [1..]) req -- An infinite succession of requests 
                              -- each yielding three numbers. Here we are not 
                              -- actually using IO to get each but we could.
main = S.print $ S.take 4 req'
-- >>> main
-- Sending request #1
-- 1
-- 2
-- 3
-- Sending request #2
-- 2

要获得我们的四个期望值,我们必须发送两个“请求”;我们当然不会最终将req应用于所有整数! S.take不允许进一步开发无限流req'作为参数;所以只计算第二个请求中的第一个元素。然后一切都关闭了。花哨的签名Stream (Of Int) IO ()可以用同义词替换

 type List a = Stream (Of a) IO ()

你几乎没有注意到与Haskell列表的区别,除了你没有得到你注意到的启示。实际签名中的额外可移动部分在这里分散注意力,但是可以在基本上每个细节复制Data.List的整个API,同时允许IO并避免在任何地方累积。 (如果没有其他可移动的部分,例如不可能写splitAtpartitionchunksOf,实际上你会发现堆栈溢出充满了问题如何使用例如{{ {1}}。)

conduit等效于此

pipes

它的不同之处在于将import Pipes import qualified Pipes.Prelude as P req :: Integer -> Producer Integer IO () req x = do liftIO $ putStr "Sending request #" >> print x each [x..x+2] req' = for (each [1..]) req main = runEffect $ req' >-> P.take 4 >-> P.print -- >>> main -- Sending request #1 -- 1 -- 2 -- 3 -- Sending request #2 -- 2 take视为管道,而不是像print那样将流作为普通函数。这具有魅力,但在目前的情况下并不需要将流作为有效列表的概念占主导地位。直觉Data.Listtake是我们对列表所做的事情,即使它是一个有效的列表,就像在这种情况下,管道和管道方面是一个分心(在面包和黄油)由于print>->的费用类似于.|的费用,因此计算所需的时间也几乎翻了一倍。)

如果我们注意到上面的map可以写成

,这可能有助于理解
req

这将在req x = do liftIO $ putStr "Sending request #" >> print x yield x -- yield a >> yield b == each [a,b] yield (x+1) yield (x+2) streamingpipes中逐字逐句。 conduityield a >> rest相同不同之处在于a:rest行(在do块中)之前可能有一些IO,例如yield a

一般情况下,列表a <- liftIO readLn; yield a mapM replicateMtraverse应该避免 - 除了简短列表 - 您提到的原因。 sequence位于它们的底部,它基本上必须构成整个列表才能继续。 (注意sequence; sequence = mapM id)因此我们看到了

mapM f = sequence . map f

但是使用流媒体库我们会看到像

这样的内容
 >>> sequence [getChar,getChar,getChar] >>= mapM_ print
 abc'a'   -- here and below I just type abc, ghci prints 'a' 'b' 'c'
 'b'
 'c'

类似地

 >>> S.mapM_ print $ S.sequence $ S.each [getChar,getChar,getChar] 
 a'a'
 b'b'
 c'c'

是一团糟 - 在构建整个列表之前没有任何反应,然后每个收集的>>> replicateM 3 getChar >>= mapM_ print abc'a' 'b' 'c' 都会连续打印出来。但是使用流式库我们编写更简单的

Char

并且输出与输入同步。特别是,一次只能在内存中使用一个字符。相比之下,>>> S.mapM_ print $ S.replicateM 3 getChar a'a' b'b' c'c' replicateM_mapM_不会累积列表不是问题。其他应该促使人们想到流媒体库,任何流媒体库。单身将军sequence_无法做到这一点,正如你可以通过反思

看到的那样
sequence

如果列表的长度是一百万>>> sequence [Just 1, Just 2, Just 3] Just [1,2,3] >>> sequence [Just 1, Just 2, Nothing] Nothing ,那么在等待查看最后一项是Maybe Int时,它们都必须被记住并保持未使用状态。由于NothingsequencemapMreplicateM和公司都是monad general,traverse的内容适用于Maybe

继续上面,我们可以像你想要的那样收集列表:

IO

或,在main = S.toList_ (S.take 4 req') >>= print -- >>> main -- Sending request #1 -- Sending request #2 -- [1,2,3,2] 版本中:

pipes

或者为了堆积可能性,我们可以对每个元素进行IO,同时将它们收集在列表或向量中或其他任何

main = P.toListM (req' >->  P.take 4) >>= print  
-- >>> main
-- Sending request #1
-- Sending request #2
-- [1,2,3,2]

在这里,我打印副本并保存列表的“原件”。我们在这里玩的游戏开始达到main = do ls <- S.toList_ $ S.print $ S.copy $ S.take 4 req' print ls -- >>> main -- Sending request #1 -- 1 -- 2 -- 3 -- Sending request #2 -- 2 -- [1,2,3,2] pipes的限制,尽管这个特定的程序可以与它们一起复制。

答案 2 :(得分:1)

据我所知,您正在寻找的内容不应该/不能使用mapM完成,并且应该使用某种形式的流式传输。如果它有用,请使用io-streams

的示例
import qualified System.IO.Streams as Streams
import qualified System.IO.Streams.Combinators as Streams

req :: IO (Maybe [Integer])
req = do
  print "x"
  return (Just [1,2,3])

req' :: IO [Integer]
req' = Streams.toList =<< Streams.take 4 =<< Streams.concatLists =<< Streams.makeInputStream req

答案 3 :(得分:0)

代码的工作版本:

module Foo where

req :: Integer -> IO [Integer]
req _x = do
    print "x"
    return [1,2,3]

req' :: IO [Integer]
req' = concat <$> mapM req [1..1000]

(注意:我将fmap concat替换为concat <$>。)

当您评估fmap (take 4) req'时,需要mapM表达式的值,而这需要[1..1000]列表的值。因此,生成了1000个元素列表,mapMreq函数应用于每个元素 - 因此,打印了1000'x'-es。然后concat必须为(take 4)部分提供一个值,该部分会重复生成[1,2,3] 1000次。然后,只有这样,(take 4)才能获得前四个元素。

所有这些计算都是因为ghci需要一个值,如果您在解释器的REPL提示符下。否则,在执行程序中,take 4只是堆叠在等待的thunk中,直到实际需要它为止。

最好将此视为一个树,其中表达式被推送到树的根上,每次都替换根(root成为另一个需要其值的表达式中的叶子。)当树的根处的值时需要,从下往上评估。

现在,如果你真的只想要req只评估一次,因为它确实是一个常量值,这就是代码:

module Foo where

req2 :: IO [Integer]
req2 = do
    print "x"
    return [1,2,3]

req2' :: IO [Integer]
req2' = concat <$> mapM (const req2) ([1..1000] :: [Integer])

req2'只被计算一次,因为它的计算结果为常数(没有函数参数可以保证这一点。)不过,这可能不是你真正想要的。

答案 4 :(得分:0)

这就是管道和管道生态系统的设计目标。这是管道的一个例子。

#!/usr/bin/env stack
--stack runghc --resolver=lts-7.16 --package pipes

module Main where

import Control.Monad (forever)
import Pipes as P
import qualified Pipes.Prelude as P

req :: Producer Int IO ()
req = forever $ do
  liftIO $ putStrLn "Making a request."
  mapM_ yield [1,2,3]


main :: IO ()
main = P.toListM (req >-> P.take 4) >>= print

请注意,通常您不会使用管道将结果折叠到列表中,但这似乎是您的用例。