[英]How do I handle an infinite list of IO objects in Haskell?
我正在編寫一個從文件列表中讀取的程序。 每個文件都包含指向下一個文件的鏈接或標記它是鏈的末尾。
作為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中進行操作? 或者我是否需要刪除整個無限列表進程並編寫遞歸函數來手動終止列表?
在正確方向邁出的一步
令我困惑的是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
漂亮吧? unfoldr
很像iterate
,除非一旦命中Nothing
,它就會終止列表。
現在,我們遇到了問題。 IO
。 我們如何在那里拋出IO
做同樣的事情? 甚至不要考慮不應該命名的功能。 我們需要加強解決方案來解決這個問題。 幸運的是,我們可以使用展開源 。
unfoldr :: (b -> Maybe (a, b)) -> b -> [a]
unfoldr f b =
case f b of
Just (a,new_b) -> a : unfoldr f new_b
Nothing -> []
現在我們需要什么? 健康劑量的IO
。 liftM2 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 fb = 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 = NoClueWhatGoesHere
以及getFirstFile
和getNextFile
的類型簽名來data DataFile = NoClueWhatGoesHere
所有這些,並將它們的定義設置為undefined
。
[edit]將myUnfoldr
和myUnfoldrM
更改為更像iterate
,包括結果列表中的初始值。
[edit]關於展開的其他見解:
如果你很難將頭部展開,那么Collatz序列可能是最簡單的例子之一。
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
是針對“下一個種子”和“當前輸出值”相同的情況的簡化展開,就像collatz的情況一樣。 鑒於myUnfoldr
在unfoldr
和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 -> mb) -> a -> mc
。
因為那些函數中的Monad m
可以與sinkM
的Identity
monad約束統一。 但是, 我沒有看到任何 sinkM
實際上有用的東西。
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
正如您所注意到的,IO結果不能是懶惰的,因此您無法(輕松地)使用IO構建無限列表。 然而,在unsafeInterleaveIO
有一條出路; 有了這個,你可以這樣做:
ioList startFile = do
v <- processFile startFile
continuation <- unsafeInterleaveIO (nextFile startFile >>= ioList)
return (v:continuation)
不過在這里要小心很重要 - 你只是將ioList
的結果推遲到將來某個不可預測的時間。 事實上,它可能永遠不會被運行。 所以當你像這樣聰明時,要非常小心。
就個人而言,我只想構建一個手動遞歸函數。
懶惰和I / O是一個棘手的組合。 使用unsafeInterleaveIO
是在IO monad中生成延遲列表的一種方法(這是標准getContents
, readFile
和friends使用的技術)。 但是,盡管如此方便,它會將純代碼暴露給可能的I / O錯誤,並使釋放資源(例如文件句柄)成為非確定性的。 這就是為什么大多數“嚴肅的”Haskell應用程序(特別是那些關注效率的應用程序)現在使用稱為枚舉器和迭代器的東西來進行流I / O. Hackage中的一個實現此概念的庫是enumerator
。
你可能在你的應用程序中使用惰性I / O很好,但我認為我仍然將此作為另一種解決這類問題的方法的例子。 您可以在此處和此處找到有關迭代的更深入的教程。
例如,您的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
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.