Переопределение getContents с помощью getChar
На пути к пониманию ленивого ввода-вывода в Хаскеле я попробовал следующее:
main = do
chars <- getContents
consume chars
consume :: [Char] -> IO ()
consume [] = return ()
consume ('x':_) = consume []
consume (c : rest) = do
putChar c
consume rest
который просто повторяет все символы, введенные в stdin, пока я не нажму 'x'.
Итак, я наивно думал, что возможно будет переопределение getContents
с помощью getChar
делать что-то по следующим направлениям:
myGetContents :: IO [Char]
myGetContents = do
c <- getChar
-- And now?
return (c: ???)
Оказывается, это не так просто, так как ???
потребуется функция типа IO [Char] -> [Char]
что, я думаю, разрушило бы всю идею монады IO.
Проверка выполнения getContents
(или скорее hGetContents
) раскрывает целую колбасную фабрику грязного ввода-вывода. Верно ли мое предположение, что myGetContents
не может быть реализован без использования грязного, т.е. разрушающего монаду, кода?
3 ответа
Вам нужен новый примитив unsafeInterleaveIO :: IO a -> IO a
это задерживает выполнение его действия аргумента, пока результат этого действия не будет оценен. затем
myGetContents :: IO [Char]
myGetContents = do
c <- getChar
rest <- unsafeInterleaveIO myGetContents
return (c : rest)
Вы должны действительно избегать использования чего-либо в System.IO.Unsafe
если вообще возможно. Они, как правило, убивают ссылочную прозрачность и не являются общими функциями, используемыми в Haskell, если в этом нет крайней необходимости
Я подозреваю, что если вы немного измените сигнатуру вашего типа, вы сможете получить более идиоматический подход к вашей проблеме.
consume :: Char -> Bool
consume 'x' = False
consume _ = True
main :: IO ()
main = loop
where
loop = do
c <- getChar
if consume c
then do
putChar c
loop
else return ()
Вы можете сделать это без каких-либо взломов.
Если ваша цель просто прочитать все stdin
в String
Вам не нужно ничего из unsafe*
функции.
IO
является монадой, а монада является аппликативным функтором. Функтор определяется функцией fmap
чья подпись:
fmap :: Functor f => (a -> b) -> f a -> f b
это удовлетворяет этим двум законам:
fmap id = id
fmap (f . g) = fmap f . fmap g
Эффективно, fmap
применяет функцию к упакованным значениям.
Учитывая конкретный характер 'c'
какой тип fmap ('c':)
? Мы можем записать два типа, а затем объединить их:
fmap :: Functor f => (a -> b ) -> f a -> f b
('c':) :: [Char] -> [Char]
fmap ('c':) :: Functor f => ([Char] -> [Char]) -> f [Char] -> f [Char]
Напоминая что IO
является функтором, если мы хотим определить myGetContents :: IO [Char]
кажется разумным использовать это:
myGetContents :: IO [Char]
myGetContents = do
x <- getChar
fmap (x:) myGetContents
Это близко, но не совсем эквивалентно getContents
, так как эта версия будет пытаться прочитать за конец файла и выдать ошибку вместо возврата строки. Просто взглянув на это, вы должны понять: нет никакого способа вернуть конкретный список, есть только бесконечная цепочка минусов. Зная, что конкретный случай ""
в EOF (и используя синтаксис инфикса <$>
за fmap
) приводит нас к:
import System.IO
myGetContents :: IO [Char]
myGetContents = do
reachedEOF <- isEOF
if reachedEOF
then return []
else do
x <- getChar
(x:) <$> myGetContents
Аппликативный класс дает (небольшое) упрощение.
Напомним, что IO
является Аппликативным Функтором, а не просто старым Функтором. Существуют "Применимые законы", связанные с этим типом классов, очень похожие на "Законы Функтора", но мы рассмотрим конкретно <*>
:
<*> :: Applicative f => f (a -> b) -> f a -> f b
Это почти идентично fmap
(ака <$>
), за исключением того, что применяемая функция также переносится. Затем мы можем избежать привязки в нашем else
пункт с использованием стиля Applicative:
import System.IO
myGetContents :: IO String
myGetContents = do
reachedEOF <- isEOF
if reachedEOF
then return []
else (:) <$> getChar <*> myGetContents
Одна модификация необходима, если ввод может быть бесконечным.
Помните, когда я сказал, что вам не нужно unsafe*
функции, если вы просто хотите прочитать все stdin
в String
? Ну, если вы просто хотите получить какую -то информацию, вы делаете. Если ваш вклад может быть бесконечно длинным, вы определенно делаете. Конечная программа отличается одним импортом и одним словом:
import System.IO
import System.IO.Unsafe
myGetContents :: IO [Char]
myGetContents = do
reachedEOF <- isEOF
if reachedEOF
then return []
else (:) <$> getChar <*> unsafeInterleaveIO myGetContents
Определяющая функция ленивого ввода-вывода unsafeInterleaveIO
(от System.IO.Unsafe
). Это задерживает вычисление IO
действие, пока оно не востребовано.