考虑以下简单的IO功能:
req :: IO [Integer]
req = do
print "x"
return [1,2,3]
实际上,这可能是一个http请求,它在解析结果后返回一个列表。
我试图以懒惰的方式连接该函数的几个调用的结果。
简单来说,以下内容应仅打印'x'两次:
fmap (take 4) req'
--> [1, 2, 3, 4]
我认为这可以通过sequence
或mapM
解决,但我的方法在懒惰方面失败了:
import Control.Monad
req' :: IO [Integer]
req' = fmap concat $ mapM req [1..1000] -- should be infinite..
这会得到正确的结果,但IO函数req被调用1000次而不是必要的2次。当使用无限列表上的映射实现上述内容时,评估不会终止。
答案 0 :(得分:6)
您不应该这样做,请查看流式IO库,例如pipes
或conduit
。
你做不到。或者至少,你真的不应该。允许懒惰评估的代码有副作用通常是一个非常糟糕的主意。不仅很快就会很难推断出效果何时以及执行多少次,但更糟糕的是,效果可能无法按照您期望的顺序执行!使用纯代码,这不是什么大问题。使用副作用代码,这是一场灾难。
想象一下,您想要从引用中读取值,然后使用更新的值替换该值。在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可能会导致有趣的竞争条件和资源泄漏,并且通常不赞成。建议的替代方法是使用评论中提到的conduit
或pipes
等流式IO库。
答案 1 :(得分:3)
以下是使用streaming
和pipes
库执行此类操作的方法。管道程序与使用管道编写的程序有些类似,特别是在这种情况下。 conduit
使用不同的名称,而pipes
和conduit
的类型和运算符比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并避免在任何地方累积。 (如果没有其他可移动的部分,例如不可能写splitAt
,partition
和chunksOf
,实际上你会发现堆栈溢出充满了问题如何使用例如{{ {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.List
和take
是我们对列表所做的事情,即使它是一个有效的列表,就像在这种情况下,管道和管道方面是一个分心(在面包和黄油)由于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)
streaming
和pipes
中逐字逐句。 conduit
与yield a >> rest
相同不同之处在于a:rest
行(在do块中)之前可能有一些IO,例如yield a
一般情况下,列表a <- liftIO readLn; yield a
mapM
replicateM
和traverse
应该避免 - 除了简短列表 - 您提到的原因。 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
时,它们都必须被记住并保持未使用状态。由于Nothing
,sequence
,mapM
,replicateM
和公司都是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个元素列表,mapM
将req
函数应用于每个元素 - 因此,打印了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
请注意,通常您不会使用管道将结果折叠到列表中,但这似乎是您的用例。