Основы функционального программирования/Haskell/Ввод-вывод: различия между версиями

Первоначальная викификация
(Пока лишь копия текста из MS Word - необходима стилизация)
 
(Первоначальная викификация)
= Лекция 7. «Операции ввода/вывода в Haskell’е» =
 
В Haskell’е, как и во всех остальных языках, существует всесторонняя система операций ввода/вывода. Хотя обычно операции ввода/вывода предполагают некоторую последовательность в своем выполнении, т.е. по сути дела императивность, в Haskell’е система операций ввода/вывода полностью поддерживает функциональную парадигму программирования.
 
Уже говорилось, что все операции ввода/вывода построены при помощи такого понятия языка Haskell, как монада (лекция 6). В то же время для понимания системы операций ввода/вывода в Haskell’е нет особой необходимости понимать теоретические основы понятия монада. Монады можно рассматривать как концептуальные рамки, в которых содержится система ввода/вывода. Можно сказать, что понимание теории категорий так же необходимо для использования системы операций ввода/вывода в Haskell’е, как и понимание теории групп для выполнения арифметических операций.
 
Операции ввода/вывода в любом языке основаны на понятии действия. При возбуждении действия, оно выполняется. Однако в Haskell’е действия не возбуждаются, а скорее просто декларируются. В свою очередь действия могут быть атомарными или составленными из последовательности других действий. Монада IO содержит операции, которые позволяют создавать сложные действия из атомарных. Т.е. монаду в данном случае можно рассматривать как клей, который связывает действия в программе.
 
Базовые операции ввода/вывода
== Базовые операции ввода/вывода ==
 
Каждое действие ввода/вывода возвращает какое-то значение. Для того, чтобы различать эти значения от базовых, типы этих значений как бы обернуты типом IO (необходимо помнить, что монада является контейнерным типом). Например, тип функции getChar следующий:
 
getChar :: IO Char
getChar :: IO Char
 
В этом примере показано, что функция getChar выполняет некоторое действие, которое возвращает значение типа Char. Действия, которые не возвращают ничего интересного, имеют тип IO (). Т.е. символ () обозначает пустой тип (void в других языках). Так функция putChar имеет тип:
 
putChar :: Char -> IO ()
putChar :: Char -> IO ()
 
Друг с другом действия связываются при помощи оператора связывания. Т.е. символы >>= выстраивают последовательность действий. Как известно, вместо этой функции можно использовать служебное слово do. Оно использует такой же двумерный синтексис, как и слова let и where, поэтому можно не использовать для разделения вызова функций символ « ; ». При помощи слова do можно связывать вызовы функций, определение данных (при помощи символа ) и множество определений локальных переменных (служебное слово let).
 
Например, так можно определить программу, которая читает символ с клавиатуры и выводит его на экран:
 
main :: IO ()
main = do c:: <-IO getChar()
main = do c <- getChar
putChar c
putChar c
 
В этом примере не случайно для имени функции выбрано слово main. В Haskell’е, также, как и в языке C/C++ название функции main используется для обозначения точки входа в программу. Кроме того, в Haskell’е тип функции main должен быть типом монады IO (обычно используется IO ()). Ко всему прочему, точка входа в виде функции main должна быть определена в модуле с именем Main.
Пусть имеется функций ready, которая должна возвращать True, если нажата клавиша «y», и False в остальных случаях. Нельзя просто написать:
 
ready :: IO Bool
ready = do c:: <-IO getCharBool
ready = do c <- getChar
c == ’y’
c == ’y’
 
Потому что в этом случае результатом выполнения операции сравнения будет значение типа Bool, а не IO Bool. Для того, чтобы возвращать монадические значения, существует специальная функция return, которая из простого типа данных делает монадический. Т.е. в предыдущем примере последняя строка определения функции ready должна была выглядеть как «return (c == ‘y’)».
В следующем примере показана более сложная функция, которая считывает строку символов с клавиатуры:
 
Пример 16. Функция getLine.
'''Пример 16. Функция getLine.'''
getLine :: IO String
 
getLine = do c <- getChar
getLine :: IO String
if c == ’\n’
getLine = do c <- getChar
then return ””
if c == ’\n’ then return ””
else do l <- getLine
else do l <- getLine
return (c : l)
return (c : l)
 
Необходимо помнить, что в тот момент, когда программист перешёл в мир действий (использовал систему операций ввода/вывода), назад пути нет. Т.е. если функция не использует монадический тип IO, то она не может заниматься вводом/выводом, и наоборот, если функция возвращает монадический тип IO, то она должна подчиняться парадигме действий в Haskell’е.
 
Программирование при помощи действий
== Программирование при помощи действий ==
 
Действия ввода/вывода являются обычными значениями в терминах Haskell’а. Т.е. действия можно передавать в функции в качестве параметров, заключать в структуры данных и вообще использовать там, где можно использовать данные языка Haskell. В этом смысле система операций ввода/вывода является полностью функциональной. Например, можно предположить список действий:
 
todoList :: [IO ()]
todoList = :: [putCharIO ’a’,()]
todoList = do [putChar ’b’’a’,
do putChar ’c’,’b’
putChar ’c’,
do c <- getChar
do c <- getChar
putChar c]
putChar c]
 
Этот список не возбуждает никаких действий, он просто содержит их описания. Для того, чтобы выполнить эту структуру, т.е. возбудить все ее действия, необходима некоторая функция (например, sequence_):
 
sequence_ :: [IO ()] -> IO ()
sequence_ :: [IO ()] = return-> IO ()
sequence_ (a:as) [] = do areturn ()
sequence_ (a:as) = do a
sequence as
sequence as
 
Эта функция может быть полезна для написания функции putStr, которая выводит строку на экран:
 
putStr :: String -> IO ()
putStr s =:: sequence_String (map-> putCharIO s()
putStr s = sequence_ (map putChar s)
 
На этом примере видно явное отличие системы операций ввода/вывода языка Haskell от систем императивных языков. Если бы в каком-нибудь императивном языке была бы функция map, она бы выполнила кучу действий. Вместо этого в Haskell’е просто создается список действий (одно для каждого символа строки), который потом обрабатывается функцией sequence_ для выполнения.
 
Обработка исключений
== Обработка исключений ==
 
Что делать, если в процессе операций ввода/вывода возникла неординарная ситуация? Например, функция getChar обнаружила конец файла. В этом случае произойдет ошибка. Как и любой продвинутый язык программирования Haskell предлагает для этих целей механизм обработки исключений. Для этого не используется какой-то специальный синтаксис, но есть специальный тип IOError, который содержит описания всех возникаемых в процессе ввода/вывода ошибок.
 
Обработчик исключений имеет тип (IOError  IO a), при этом функция catch ассоциирует (связывает) обработчик исключений с набором действий:
 
catch :: IO a -> (IOError -> IO a) -> IO a
catch :: IO a -> (IOError -> IO a) -> IO a
 
Аргументами этой функции являются действие (первый аргумент) и обработчик исключений (второй аргумент). Если действие выполнено успешно, то просто возвращается результат без возбуждения обработчика исключений. Если же в процессе выполнения действия возникла ошибка, то она передается обработчику исключений в качестве операнда типа IOError, после чего выполняется сам обработчик.
 
Таким образом, можно написать более сложные функции, которые будут грамотно вести себя в случае выпадения ошибочных ситуаций:
getChar’ :: IO Char
getChar’ = getChar `catch` eofHandler
where eofHandler e = if isEofError e then return \’n\ else ioError e
 
getLine’ getChar’ :: IO StringChar
getChar’ = getChar `catch` eofHandler
getLine’ = catch getLine’’ (\err -> return (”Error: ” ++ show err))
where eofHandler e = if isEofError e then return \’n\ else ioError e
where getLine’’ = do c <- getChar’
id c == ’\n’ then return ””
getLine’ :: IO String
else do l <- getLine’
getLine’ = catch getLine’’ (\err -> return (”Error: ” ++ show err))
return (c : l)
where getLine’’ = do c <- getChar’
if c == ’\n’ then return ””
else do l <- getLine’
return (c : l)
 
В этой программе видно, что можно использовать вложенные друг в друга обработчики ошибок. В функции getChar’ отлавливается ошибка, которая возникает при обнаружении символа конца файла. Если ошибка другая, то при помощи функции ioError она отправляется дальше и ловится обработчиком, который «сидит» в функции getLine’. Для определённости в Haskell’е предусмотрен обработчик исключений по умолчанию, который находится на самом верхнем уровне вложенности. Если ошибка не поймана ни одним обработчиком, который написан в программе, то её ловит обработчик по умолчанию, который выводит на экран сообщение об ошибке и останавливает программу.
 
Файлы, каналы и обработчики
== Файлы, каналы и обработчики ==
 
Для работы с файлами Haskell предоставляет все возможности, что и другие языки программирования. Однако большинство этих возможностей определены в модуле IO, а не в Prelude, поэтому для работы с файлами необходимо явно импортировать модуль IO.
Открытие файла порождает обработчик (он имеет тип Handle). Закрытие обработчика инициирует закрытие соответствующего файла. Обработчики могут быть также ассоциированы с каналами, т.е. портами взаимодействия, которые не связаны напрямую с файлами. В Haskell’е предопределены три таких канала — stdin (стандартный канал ввода), stdout (стандартный канал вывода) и stderr (стандартный канал вывода сообщений об ошибках).
 
Таким образом, для использования файлов можно пользоваться следующими вещами:
 
type FilePath = String
openFile :: type FilePath ->= IOMode -> IO HandleString
hClose openFile :: HandleFilePath -> IOMode -> IO ()Handle
hClose :: Handle -> IO ()
data IOMode = ReadMode | WriteMode | AppendMode | ReadWriteMode
data IOMode = ReadMode | WriteMode | AppendMode | ReadWriteMode
 
Далее приводится пример программы, которая копирует один файл в другой:
main = do fromHandle <- getAndOpenFile "Copy from: " ReadMode
toHandle <- getAndOpenFile "Copy to: " WriteMode
contents <- hGetContents fromHandle
hPutStr toHandle contents
hClose toHandle
hClose fromHandle
putStr "Done."
 
main = do fromHandle <- getAndOpenFile "Copy from: " ReadMode
getAndOpenFile :: String -> IOMode -> IO Handle
toHandle <- getAndOpenFile prompt"Copy to: " modeWriteMode =
contents <- hGetContents fromHandle
do putStr prompt
name <- getLine hPutStr toHandle contents
catch (openFile name mode) hClose toHandle
hClose fromHandle
(\_ -> do putStrLn ("Cannot open "++ name ++ "\n")
putStr getAndOpenFile prompt mode)"Done."
getAndOpenFile :: String -> IOMode -> IO Handle
getAndOpenFile prompt mode = do putStr prompt
name <- getLine
catch (openFile name mode) (\_ -> do putStrLn ("Cannot open "++ name ++ "\n") getAndOpenFile prompt mode)
 
Здесь использована одна интересная и важная функция — hGetContents, которая берёт содержимое переданного ей в качестве аргумента файла и возвращает его в качестве одной длинной строки.
 
Окончательные замечания
== Окончательные замечания ==
 
Получается так, что в Haskell’е заново изобретено императивное программирование...
 
В некотором смысле — да. Монада IO встраивает в Haskell маленький императивный подъязык, при помощи которого можно осуществлять операции ввода/вывода. И написание программ на этом подъязыке выглядит обычно с точки зрения императивных языков. Но есть существенное различие: в Haskell’е нет специального синтаксиса для ввода в программный код императивных функций, все осуществляется на уровне функциональной парадигмы. В то же время опытные программисты могут минимизировать императивный код, используя монаду IO только на верхних уровнях своих программ, т.к. в Haskell’е императивный и функциональный миры чётко разделены между собой. В отличие от Haskell’а в императивных языках, в которых есть функциональные подъязыки, нет чёткого разделения между обозначенными мирами.
271

правка