функции catch.
Например программа, в которой мы дописывали данные в файл, упадёт, если мы передадим не существу-
ющий файл. Но мы можем исправить это поведение с помощью функции catch. Мы можем перезапускать
программу, если произошла ошибка:
module FileSafe where
import Control.Applicative
import Control.Monad
main = try ‘catch‘ const main
try = msg1 >> getLine >>= read >>= append
where read
file = readFile file >>= putStrLn >> return file
append file = msg2 >> getLine >>= appendFile file
msg1
= putStr ”input file: ”
msg2
= putStr ”input text: ”
Часто функции двух аргументов называют так, чтобы при инфиксной форме записи получалась фраза
из английского языка. Так если мы запишем catch в инфиксной форме получится очень наглядное выраже-
ние. Функция обработки ошибок реагирует на любую ошибку перезапуском программы. Попробуем взломать
программу:
*FileSafe> main
input file: fsldfksld
input file: sd;fls;dfl;vll; d;fld;f
input file: dflks;ldkf ldkfldkfld
input file: lsdkfksdlf ksdkflsdfkls;dfk
input file: bfk
input file: test
Hello!Hi)
input text: HowHow
Функция будет запрашивать файл до тех пор, пока мы не введём корректное значение. Мы можем доба-
вить сообщение об ошибке, немного изменив функцию обработки:
main = try ‘catch‘ const (msg >> main)
where msg = putStrLn ”Wrong filename, try again.”
А что делать если нам хочется различать ошибки по типу и предпринимать различные действия в зави-
симости от типа ошибки? Ошибки распознаются с помощью специальных предикатов, которые определены
в модуле System.IO.Error. Рассмотрим некоторые из них.
136 | Глава 8: IO
Например с помощью с помощью предиката isDoesNotExistErrorType мы можем опознать ошибки,
которые случились из-за того, что один из аргументов функции не существует. С помощью предиката
isPermissionErrorType мы можем узнать, что ошибка произошла из-за того, что мы пытались получить до-
ступ к данным, на которые у нас нет прав. Мы можем, немного изменив функцию-обработчик исключений,
выводить более информативные сообщения об ошибках перед перезапуском:
main = try ‘catch‘ handler
handler :: IOError -> IO ()
handler = ( >> main) . putStrLn . msg2 . msg1
msg1 e
| isDoesNotExistErrorType e = ”File does not exist. ”
| isPermissionErrorType e
= ”Access denied. ”
| otherwise
= ””
msg2 = (++ ”Try again.”)
В модуле System.IO.Error вы можете найти ещё много разных предикатов.
Потоки текстовых данных
Обмен данными, чтение и запись происходят с помощью потоков. Каждый поток имеет дескриптор
(handle), через него мы можем общаться с потоком, например считывать данные или записывать. Функции
для работы с потоками данных определены в модуле System.IO.
В любой момент в системе открыты три стандартных потока:
• stdin – стандартный ввод
• stdout – стандартный вывод
• stderr – поток ошибок и отладочных сообщений
Например когда мы выводим строку на экран, на самом деле мы записываем строку в поток stdout. А
когда мы читаем символ с клавиатуры, мы считываем его из потока stdin.
Файлы также являются потоками. При открытии файлу присваивается дескриптор через который, мы
можем обмениваться данными. Файл может быть открыт для чтения, записи, дополнения (записи в конец
файла) или чтения и записи. Файл открывается функцией:
openFile :: FilePath -> IOMode -> IO Handle
Функция принимает путь к файлу и режим работы с файлом и возвращает дескриптор. Режим может
принимать одно из значений:
• ReadMode – чтение
• WriteMode – запись
• AppendMode – добавление (запись в конец файла)
• ReadWriteMode – чтение и запись
Открыв дескриптор, мы можем начать обмениваться данными. Для этого определены функции аналогич-
ные тем, что мы уже рассмотрели. Функции для записи данных:
— запись символа
hPutChar :: Handle -> Char -> IO ()
— запись строки
hPutStr :: Handle -> String -> IO ()
— запись строки с переносом каретки
hPutStrLn :: Handle -> String -> IO ()
— запись значения
hPrint :: Show a => Handle -> a -> IO ()
Типичные задачи IO | 137
Все функции принимают первым аргументом дескриптор потока. Дескриптор должен позволять записы-
вать данные. Например для дескриптора, открытого в режиме ReadMode, выполнение этих функций приведёт
к ошибке.
Из потоков также можно читать данные. Эти функции похожи на те, что мы уже рассмотрели:
— чтение одного символа
hGetChar :: Handle -> IO Char
— чтение строки
hGetLine :: Handle -> IO String
— ленивое чтение строки
hGetContents :: Handle -> IO String
Как только, мы закончим работу с файлом, его необходимо закрыть. Нам нужно освободить дескриптор.
Сделать это можно функцией hClose:
hClose :: Handle -> IO ()
Стандартные функции ввода/вывода, которые мы рассмотрели ранее определены через функции работы
с дескрипторами. Например так мы выводим строку на экран:
putStr
:: String -> IO ()
putStr s
=
hPutStr stdout s
А так читаем строку с клавиатуры:
getLine
:: IO String
getLine
=
hGetLine stdin
В этих функциях используются дескрипторы стандартных потоков данных stdin и stdout. Отметим функ-
цию withFile:
withFile :: FilePath -> IOMode -> (Handle -> IO r) -> IO r
Она открывает файл в заданном режиме выполняет функцию на его дескрипторе и и закрывает файл.
Например через эту функцию определены функции readFile и appendFile:
appendFile
:: FilePath -> String -> IO ()
appendFile f txt = withFile f AppendMode (hdl -> hPutStr hdl txt)
writeFile :: FilePath -> String -> IO ()
writeFile f txt = withFile f WriteMode (hdl -> hPutStr hdl txt)
8.5 Форточка в мир побочных эффектов
В самом начале главы я сказал о том, что из мира IO
нет выхода. Нет функции с типом IO a -> a. На самом деле выход есть. Функция с таким типом живёт в
модуле System.IO.Unsafe:
unsafePerformIO :: IO a -> a
Длинное имя функции намекает на то, что её необходимо использовать с крайней осторожностью. По-
скольку последствия могут быть непредсказуемыми.
Эта функция используется при чтении конфигурационных файлов. Если есть уверенность в том, что файл
будет только читаться и во время выполнения программы файл не может быть изменён другой программой,
то мы можем считать, что его значение окажется неизменным на протяжении работы программы. Это говорит
о том, что нам не важно когда читать данные. Поэтому здесь мы вроде бы ничем не рискуем. “Вроде бы”
потому что ответственность за постоянство файла лежит на наших плечах.
Эта функция часто используется при вызове функций С через Haskell. В Haskell есть возможность вызывать
функции, написанные на C. Но по умолчанию такие функции заворачиваются в тип IO. Если функция является
чистой в С, то она будет чистой и при вызове через Haskell. Мы можем поручиться за её чистоту и вычислитель
нам поверит. Но если мы его обманули, мы пожнём плоды своего обмана.
138 | Глава 8: IO
Отладка программ
Раз уж речь зашла о “грязных” возможностях языка стоит упомянуть функцию trace из модуля
Debug.Trace. Посмотрим на её тип:
trace :: String -> a -> a
Это служебная функция эхо-печати. Когда дело доходит до вычисления функции trace на экран выводит-
ся строка, которая была передана в неё первым аргументом, после чего функция возвращает второй аргумент.





