Как мне обработать бесконечный список объектов ввода-вывода в Haskell? - PullRequest
8 голосов
/ 12 октября 2011

Я пишу программу, которая читает из списка файлов.Каждый файл содержит либо ссылку на следующий файл, либо отмечает, что это конец цепочки.

Будучи новичком в 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 должны открывать файлы, мне нужно, чтобы их результаты были в монаде ввода-вывода.Это дает измененную форму

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

. Проблема в том, что, поскольку итерация возвращает бесконечный список, последовательность становится бесконечным циклом.Я не уверен, что делать дальше.Существует ли более ленивая форма последовательности, которая не затронет все элементы списка?Должен ли я перенастроить карту и сделать так, чтобы она работала внутри монады ввода-вывода для каждого элемента списка?Или мне нужно отбросить весь процесс бесконечного списка и написать рекурсивную функцию для завершения списка вручную?

Ответы [ 4 ]

13 голосов
/ 12 октября 2011

шаг в правильном направлении

Что меня озадачивает, так это getNextFile. Шагните в упрощенный мир со мной, где мы еще не имеем дело с IO. Тип Maybe DataFile -> Maybe DataFile. На мой взгляд, это должно быть просто DataFile -> Maybe DataFile, и я буду работать в предположении, что такая корректировка возможна. И , что выглядит хорошим кандидатом на unfoldr. Первое, что я собираюсь сделать, - это сделать свою собственную упрощенную версию 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 доступен нам.

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 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 = NoClueWhatGoesHere и сигнатур типов для getFirstFile и getNextFile с их определениями, установленными на undefined.


[править] изменил myUnfoldr и myUnfoldrM, чтобы вести себя больше как iterate, включая начальное значение в списке результатов.

[править] Дополнительная информация о развертывании:

Если вам трудно сложить голову, развернувшаяся последовательность 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 - это упрощенное разворачивание для случаев, когда «следующее начальное число» и «значение выходного тока» совпадают, как в случае с коллатцем. Такое поведение должно быть легко увидеть, учитывая простое определение 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 -> m b) -> a -> m c.

, поскольку Monad m в этих функциях можно объединить с ограничением монады Identity, равным sinkM. Однако, Я не вижу ничего , для которого sinkM было бы на самом деле полезно.

11 голосов
/ 12 октября 2011
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
8 голосов
/ 12 октября 2011

Как вы заметили, результаты ввода-вывода не могут быть ленивыми, поэтому вы не можете (легко) построить бесконечный список с помощью ввода-вывода.Выход есть, однако, в unsafeInterleaveIO;с этим вы можете сделать что-то вроде:

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

Здесь важно быть осторожным - вы просто отложили результаты ioList на некоторое непредсказуемое время в будущем.Это может вообще никогда не быть запущено.Так что будьте очень осторожны, когда вы так умны.

Лично я бы просто создал рекурсивную функцию вручную.

4 голосов
/ 12 октября 2011

Laziness и I / O - сложная комбинация. Использование unsafeInterleaveIO - это один из способов создания отложенных списков в монаде ввода-вывода (и это метод, используемый стандартами getContents, readFile и друзьями). Однако, как бы это ни было удобно, он подвергает чистый код возможным ошибкам ввода-вывода и делает освобождение ресурсов (например, файловых дескрипторов) недетерминированным. Вот почему большинство «серьезных» приложений на Haskell (особенно те, которые связаны с эффективностью) в настоящее время используют для потокового ввода-вывода вещи, называемые перечислителями и итераторами. В Hackage есть одна библиотека, которая реализует эту концепцию: <a href="http://hackage.haskell.org/package/enumerator" rel="nofollow">enumerator</a>.

Вероятно, у вас все в порядке с использованием ленивого ввода-вывода в вашем приложении, но я подумал, что все равно приведу это в качестве примера другого способа решения подобных проблем. Вы можете найти более подробные учебники об итераторах здесь и здесь .

Например, ваш поток 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
...