Переопределение 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 действие, пока оно не востребовано.

Другие вопросы по тегам