如何在Haskell中处理无限的IO对象列表?

时间:2011-10-11 22:53:18

标签: haskell io monads infinite-loop lazy-evaluation

我正在编写一个从文件列表中读取的程序。每个文件都包含指向下一个文件的链接或标记它是链的末尾。

作为Haskell的新手,似乎处理这个的惯用方法是为此目的的可能文件的懒惰列表,我有

getFirstFile :: String -> DataFile
getNextFile :: Maybe DataFile -> Maybe DataFile

loadFiles :: String -> [Maybe DataFile]
loadFiles = iterate getNextFile . Just . getFirstFile

getFiles :: String -> [DataFile]
getFiles = map fromJust . takeWhile isJust . loadFiles

到目前为止,这么好。唯一的问题是,由于getFirstFile和getNextFile都需要打开文件,我需要将它们的结果放在IO monad中。这给出了

的修改形式
getFirstFile :: String -> IO DataFile
getNextFile :: Maybe DataFile -> IO (Maybe DataFile)

loadFiles :: String -> [IO Maybe DataFile]
loadFiles = iterate (getNextFile =<<) . Just . getFirstFile

getFiles :: String -> IO [DataFile]
getFiles = liftM (map fromJust . takeWhile isJust) . sequence . loadFiles

问题在于,由于iterate返回无限列表,序列变为无限循环。我不知道怎么从这里开始。是否有一个更加懒惰的序列形式,不会命中所有列表元素?我是否应该重新调整地图并在每个列表元素的IO monad中进行操作?或者我是否需要删除整个无限列表进程并编写递归函数来手动终止列表?

4 个答案:

答案 0 :(得分:13)

朝着正确方向迈出的一步

让我感到困惑的是getNextFile。和我一起进入一个简化的世界,我们还没有处理IO。类型为Maybe DataFile -> Maybe DataFile。在我看来,这应该只是DataFile -> Maybe DataFile,我将在假设这种调整是可能的情况下运作。并且 看起来像是unfoldr的合适人选。我要做的第一件事是制作我自己的展开的简化版本,它不太通用但使用起来更简单。

import Data.List

-- unfoldr :: (b -> Maybe (a,b)) -> b -> [a]
myUnfoldr :: (a -> Maybe a) -> a -> [a]
myUnfoldr f v = v : unfoldr (fmap tuplefy . f) v
  where tuplefy x = (x,x)

现在,f :: a -> Maybe a类型匹配getNextFile :: DataFile -> Maybe DataFile

getFiles :: String -> [DataFile]
getFiles = myUnfoldr getNextFile . getFirstFile

漂亮吧? unfoldriterate非常相似,只要它点击Nothing,它就会终止列表。

现在,我们遇到了问题。 IO。我们怎样才能在那里抛出IO做同样的事情?甚至不认为关于不应命名的功能。我们需要加强解决方案来解决这个问题。幸运的是,我们可以使用source for unfoldr

unfoldr      :: (b -> Maybe (a, b)) -> b -> [a]
unfoldr f b  =
  case f b of
   Just (a,new_b) -> a : unfoldr f new_b
   Nothing        -> []

现在我们需要什么?健康剂量IOliftM2 unfoldr 几乎为我们提供了正确的类型,但这次不会完全削减它。

实际解决方案

unfoldrM :: Monad m => (b -> m (Maybe (a, b))) -> b -> m [a]
unfoldrM f b = do
  res <- f b
  case res of
    Just (a, b') -> do
      bs <- unfoldrM f b'
      return $ a : bs
    Nothing -> return []

这是一个相当直接的转变;我想知道是否有一些组合器能够实现同样的目标。

有趣的事实:我们现在可以定义unfoldr f b = runIdentity $ unfoldrM (return . f) b

让我们再次定义一个简化的myUnfoldrM,我们只需要在liftM中加注:

myUnfoldrM :: Monad m => (a -> m (Maybe a)) -> a -> m [a]
myUnfoldrM f v = (v:) `liftM` unfoldrM (liftM (fmap tuplefy) . f) v
  where tuplefy x = (x,x)

现在我们都像以前一样完成了设定。

getFirstFile :: String -> IO DataFile
getNextFile :: DataFile -> IO (Maybe DataFile)

getFiles :: String -> IO [DataFile]
getFiles str = do
  firstFile <- getFirstFile str
  myUnfoldrM getNextFile firstFile

-- alternatively, to make it look like before
getFiles' :: String -> IO [DataFile]
getFiles' = myUnfoldrM getNextFile <=< getFirstFile

顺便说一句,我使用data DataFile = NoClueWhatGoesHeregetFirstFile以及getNextFile的类型签名对所有这些进行了检查,其定义设置为undefined


[edit]更改了myUnfoldrmyUnfoldrM,使其更像iterate,包括结果列表中的初始值。

[edit]关于展开的其他见解:

如果你很难将头部展开,Collatz sequence可能是最简单的例子之一。

collatz :: Integral a => a -> Maybe a
collatz 1 = Nothing -- the sequence ends when you hit 1
collatz n | even n    = Just $ n `div` 2
          | otherwise = Just $ 3 * n + 1

collatzSequence :: Integral a => a -> [a]
collatzSequence = myUnfoldr collatz

请记住,myUnfoldr是针对“下一个种子”和“当前输出值”相同的情况的简化展开,就像collat​​z的情况一样。鉴于myUnfoldrunfoldr的{​​{1}}简单定义,应该很容易看到此行为。

tuplefy x = (x,x)

更多,主要是无关的想法

其余的与这个问题完全无关,但我无法抗拒沉思。我们可以根据ghci> collatzSequence 9 [9,28,14,7,22,11,34,17,52,26,13,40,20,10,5,16,8,4,2,1]

来定义myUnfoldr
myUnfoldrM

看起来很熟悉?我们甚至可以抽象出这种模式:

myUnfoldr f v = runIdentity $ myUnfoldrM (return . f) v

sinkM :: ((a -> Identity b) -> a -> Identity c) -> (a -> b) -> a -> c sinkM hof f = runIdentity . hof (return . f) unfoldr = sinkM unfoldrM myUnfoldr = sinkM myUnfoldrM 应该能够“沉没”(与“提升”相反)任何形式的功能

sinkM

因为这些函数中的Monad m => (a -> m b) -> a -> m c可以与Monad m的{​​{1}} monad约束统一。但是,Identity sinkM实际上对此有用。

答案 1 :(得分:11)

sequenceWhile :: Monad m => (a -> Bool) -> [m a] -> m [a]
sequenceWhile _ [] = return []
sequenceWhile p (m:ms) = do
  x <- m
  if p x
    then liftM (x:) $ sequenceWhile p ms
    else return []

收率:

getFiles = liftM (map fromJust) . sequenceWhile isJust . loadFiles

答案 2 :(得分:8)

正如您所注意到的,IO结果不能是懒惰的,因此您无法(轻松地)使用IO构建无限列表。然而,在unsafeInterleaveIO中有一条出路;有了这个,你可以做类似的事情:

ioList startFile = do
    v <- processFile startFile
    continuation <- unsafeInterleaveIO (nextFile startFile >>= ioList)
    return (v:continuation)

在这里要小心谨慎是很重要的 - 你只是将ioList的结果推迟到将来某个不可预测的时间。事实上,它可能永远不会被运行。所以当你像这样聪明时,要非常小心。

就个人而言,我只想构建一个手动递归函数。

答案 3 :(得分:4)

懒惰和I / O是一个棘手的组合。使用unsafeInterleaveIO是在IO monad中生成延迟列表的一种方法(这是标准getContentsreadFile和朋友使用的技术。但是,尽管如此方便,它会将纯代码暴露给可能的I / O错误,并使释放资源(例如文件句柄)成为非确定性的。这就是为什么大多数“严肃的”Haskell应用程序(特别是那些关注效率的应用程序)现在使用称为枚举器和迭代器的东西来进行流I / O. Hackage中的一个实现此概念的库是enumerator

在你的应用程序中使用懒惰的I / O你可能没什么问题,但我认为我仍然会以此为例来解决这类问题。您可以找到有关迭代herehere的更深入的教程。

例如,您的DataFiles流可以像这样实现为枚举器:

import Data.Enumerator
import Control.Monad.IO.Class (liftIO)

iterFiles :: String -> Enumerator DataFile IO b
iterFiles s = first where
    first (Continue k) = do
        file <- liftIO $ getFirstFile s
        k (Chunks [file]) >>== next file
    first step = returnI step

    next prev (Continue k) = do
        file <- liftIO $ getNextFile (Just prev)
        case file of
            Nothing -> k EOF
            Just df -> k (Chunks [df]) >>== next df
    next _ step = returnI step