__TOC__
{{ОФП Содержание}}
В Haskell’е[[w:Haskell|Haskell]], как и во всех остальных [[w:Язык программирования|языках]], существует всесторонняя система операций ввода/-вывода. Хотя обычно операции ввода/-вывода предполагают некоторую последовательность в своемсвоём выполнении, т.е.то есть по сути дела [[w:Императивное программирование|императивность]], в Haskell’еHaskell система операций ввода/-вывода полностью поддерживает [[w:Функциональное программирование|функциональную парадигму программирования]].
Уже говорилось, что все операции ввода/-вывода построены при помощи такого понятия языка Haskell, как [[w:Монада (математика)|монада]] ([[#Основы функционального программирования/Модули и монады в Haskell|лекция 6]]). В то же время для понимания системы операций ввода/-вывода в Haskell’еHaskell нет особой необходимости понимать теоретические основы понятия монада. Монады можно рассматривать как концептуальные рамки, в которых содержится система ввода/-вывода. Можно сказать, что понимание [[w:Теория категорий|теории категорий]] так же необходимо для использования системы операций ввода/-вывода в Haskell’еHaskell, как и понимание [[w:Теория групп|теории групп]] для выполнения арифметических операций.
Операции ввода/-вывода в любом языке основаны на понятии действия. При возбуждении действия, оно выполняется. Однако в Haskell’еHaskell действия не возбуждаются, а скорее просто декларируются. В свою очередь действия могут быть атомарными или составленными из последовательности других действий. Монада <code>IO</code> содержит операции, которые позволяют создавать сложные действия из атомарных. Т.е.То есть монаду в данном случае можно рассматривать как клей, который связывает действия в [[w:Компьютерная программа|программе]].
== Базовые операции ввода/-вывода ==
Каждое действие ввода/-вывода возвращает какое-то значение. Для того, чтобы различать эти значения от базовых, [[w:Тип данных|типы]] этих значений как бы обернутыобёрнуты типом <code>IO</code> (необходимо помнить, что монада является [[w:Контейнер (программирование)|контейнерным типом]]). Например, тип [[w:Функция (программирование)|функции]] <code>getChar</code> следующий:
<code>getChar :: IO Char</code>
В этом примере показано, что функция <code>getChar</code> выполняет некоторое действие, которое возвращает значение типа <code>Char</code>. Действия, которые не возвращают ничего интересного, имеют тип <code>IO ()</code>. Т.е.То есть символ <code>()</code> обозначает пустой тип (<code>void</code> в других языках). Так функция <code>putChar</code> имеет тип:
<code>putChar :: Char ->> IO ()</code>
Друг с другом действия связываются при помощи оператора связывания. Т.е.То есть символы <code>>>>=</code> выстраивают последовательность действий. Как известно, вместо этой функции можно использовать служебное слово <code>do</code>. Оно использует такой же двумерный синтексис[[w:Синтаксис (программирование)|синтаксис]], как и слова <code>let</code> и <code>where</code>, поэтому можно не использовать для разделения вызова функций символ « <code>; »</code>. При помощи слова <code>do</code> можно связывать вызовы функций, определение данных (при помощи символов « <code>-></code> ») и множество определений локальных [[w:Переменная (программирование)|переменных]] (служебное слово <code>let</code>).
Например, так можно определить программу, которая читает символ с [[w:Клавиатура|клавиатуры]] и выводит его на [[w:Монитор|экран]]:
<code>main :: IO ()
main = do c <<- getChar
putChar c</code>
В этом примере не случайно для имени функции выбрано слово <code>main</code>. В Haskell’еHaskell, также, как и в языкеязыках C[[w:Си (язык программирования)|Си]]/[[w:C++|Си++]] название функции <code>main</code> используется для обозначения точки входа в программу. Кроме того, в Haskell’еHaskell тип функции <code>main</code> должен быть типом монады <code>IO</code> (обычно используется <code>IO ()</code>). Ко всему прочему, точка входа в виде функции <code>main</code> должна быть определена в модуле с именем <code>Main</code>.
Пусть имеется функций ready, которая должна возвращать True, если нажата клавиша «y», и False в остальных случаях. Нельзя просто написать: ▼
▲Пусть имеется функцийфункция <code>ready </code>, которая должна возвращать <code>True </code>, если нажата клавиша «y», и <code>False </code> в остальных случаях. Нельзя просто написать:
c == ’y’
▲ ready = do c <<- getChar
c == 'y'</code>
Потому что в этом случае результатом выполнения операции сравнения будет значение типа <code>Bool </code>, а не <code>IO Bool </code>. Для того, чтобы возвращать монадические значения, существует специальная функция <code>return </code>, которая из простого типа данных делает монадический. Т.е.То есть в предыдущем примере последняя строка определения функции <code>ready </code> должна была выглядеть как «<code>return (c == ‘y’'y') »</code>. ▼
▲Потому что в этом случае результатом выполнения операции сравнения будет значение типа Bool, а не IO Bool. Для того, чтобы возвращать монадические значения, существует специальная функция return, которая из простого типа данных делает монадический. Т.е. в предыдущем примере последняя строка определения функции ready должна была выглядеть как «return (c == ‘y’)».
В следующем примере показана более сложная функция, которая считывает строку символов с клавиатуры:
'''Пример 16. Функция <code>getLine</code>.'''
<code>getLine :: IO String
getLine = do c <<- getChar
if c == ’'\n’n' then return ””""
else do l <<- getLine
return (c : l)</code>
Необходимо помнить, что в тот момент, когда [[w:Программист|программист]] перешёл в мир действий (использовал систему операций ввода/-вывода), назад пути нет. Т.е.То есть если функция не использует монадический тип <code>IO</code>, то она не может заниматься вводом/выводом, и наоборот, если функция возвращает монадический тип <code>IO</code>, то она должна подчиняться парадигме действий в Haskell’еHaskell.
== Программирование при помощи действий ==
Действия ввода/-вывода являются обычными значениями в терминах Haskell’аHaskell. Т.е.То есть действия можно передавать в функции в качестве [[w:Параметр (программирование)|параметров]], заключать в [[w:Структура данных|структуры данных]] и вообще использовать там, где можно использовать данные языка Haskell. В этом смысле система операций ввода/-вывода является полностью функциональной. Например, можно предположить список действий:
<code>todoList :: [IO ()]
todoList = [putChar ’a’'a',
do putChar ’b’'b'
putChar ’c’'c',
do c <<- getChar
putChar 'c']</code>
Этот список не возбуждает никаких действий, он просто содержит их описания. Для того, чтобы выполнить эту структуру, т.е.то есть возбудить все еееё действия, необходима некоторая функция (например, <code>sequence_</code>):
<code>sequence_ :: [IO ()] ->> IO ()
sequence_ [] = return ()
sequence_ (a:as) = do a
sequence as</code>
Эта функция может быть полезна для написания функции <code>putStr</code>, которая выводит строку на экран:
<code>putStr :: String ->> IO ()
putStr s = sequence_ (map putChar s)</code>
На этом примере видно явное отличие системы операций ввода/-вывода языка Haskell от систем императивных языков. Если бы в каком-нибудь императивном языке была бы функция <code>map</code>, она бы выполнила кучу действий. Вместо этого в Haskell’еHaskell просто создаетсясоздаётся список действий (одно для каждого символа строки), который потом обрабатывается функцией <code>sequence_</code> для выполнения.
== [[w:Обработка исключений|Обработка исключений]] ==
Что делать, если в процессе операций ввода/-вывода возникла неординарная ситуация? Например, функция <code>getChar</code> обнаружила конец [[w:Файл|файла]]. В этом случае произойдетпроизойдёт ошибка. Как и любой продвинутый язык программирования, Haskell предлагает для этих целей механизм обработки исключений. Для этого не используется какой-то специальный синтаксис, но есть специальный тип <code>IOError</code>, который содержит описания всех возникаемых в процессе ввода/-вывода ошибок.
Обработчик исключений имеет тип <code>(IOError ->> IO a)</code>, при этом функция <code>catch</code> ассоциирует (связывает) обработчик исключений с набором действий:
<code>catch :: IO a ->> (IOError ->> IO a) ->> IO a</code>
[[w:Аргумент (программирование)|Аргументами]] этой функции являются действие (первый аргумент) и обработчик исключений (второй аргумент). Если действие выполнено успешно, то просто возвращается результат без возбуждения обработчика исключений. Если же в процессе выполнения действия возникла ошибка, то она передаетсяпередаётся обработчику исключений в качестве операнда типа <code>IOError</code>, после чего выполняется сам обработчик.
Таким образом, можно написать более сложные функции, которые будут грамотно вести себя в случае выпадениявозникновения ошибочных ситуаций:
getChar’<code>getChar' :: IO Char
getChar’getChar' = getChar `catch` eofHandler
where eofHandler e = if isEofError e then return \’n'n\ else ioError e
getLine’getLine' :: IO String
getLine’getLine' = catch getLine’’getLine'<em/>' (\err ->> return (”Error"Error: ”" ++ show err))
where getLine’’getLine'<em/>' = do c <<- getChar’getChar'
if c == ’'\n’n' then return ””""
else do l <<- getLine’getLine'
return (c : l)</code>
В этой программе видно, что можно использовать вложенные друг в друга обработчики ошибок. В функции getChar’<code>getChar'</code> отлавливается ошибка, которая возникает при обнаружении символа конца файла. Если ошибка другая, то при помощи функции <code>ioError</code> она отправляется дальше и ловится обработчиком, который «сидит» в функции getLine’<code>getLine'</code>. Для определённости в Haskell’еHaskell предусмотрен обработчик исключений по умолчанию, который находится на самом верхнем уровне вложенности. Если ошибка не поймана ни одним обработчиком, который написан в программе, то её ловит обработчик по умолчанию, который выводит на экран сообщение об ошибке и останавливает программу.
== Файлы, каналы и обработчики ==
Для работы с файлами Haskell предоставляет все возможности, что и другие языки программирования. Однако большинство этих возможностей определены в модуле <code>IO</code>, а не в <code>Prelude</code>, поэтому для работы с файлами необходимо явно импортировать модуль <code>IO</code>.
Открытие файла порождает обработчик (он имеет тип <code>Handle</code>). Закрытие обработчика инициирует закрытие соответствующего файла. Обработчики могут быть также ассоциированы с каналами, т.е.то есть портами взаимодействия, которые не связаны напрямую с файлами. В Haskell’еHaskell предопределены [[w:Стандартные потоки|три таких канала]] — <code>stdin</code> (стандартный канал ввода), <code>stdout</code> (стандартный канал вывода) и <code>stderr</code> (стандартный канал вывода сообщений об ошибках).
Таким образом, для использования файлов можно пользоваться следующими вещами:
<code>type FilePath = String
openFile :: FilePath ->> IOMode ->> IO Handle
hClose :: Handle ->> IO ()
data IOMode = ReadMode | WriteMode | AppendMode | ReadWriteMode</code>
Далее приводится пример программы, которая копирует один файл в другой:
<code>main = do fromHandle <<- getAndOpenFile "Copy from: " ReadMode
toHandle <<- getAndOpenFile "Copy to: " WriteMode
contents <<- hGetContents fromHandle
hPutStr toHandle contents
hClose toHandle
hClose fromHandle
putStr "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)</code>
Здесь использована одна интересная и важная функция — <code>hGetContents</code>, которая берёт содержимое переданного ей в качестве аргумента файла и возвращает его в качестве одной длинной строки.
== Окончательные замечания ==
Получается так, что в Haskell’еHaskell заново изобретено императивное программирование...программирование…
В некотором смысле — да. Монада <code>IO</code> встраивает в Haskell маленький императивный подъязык, при помощи которого можно осуществлять операции ввода/-вывода. И написание программ на этом подъязыке выглядит обычно с точки зрения императивных языков. Но есть существенное различие: в Haskell’еHaskell нет специального синтаксиса для ввода в программный код императивных функций, всевсё осуществляется на уровне функциональной парадигмы. В то же время опытные программисты могут минимизировать императивный код, используя монаду <code>IO</code> только на верхних уровнях своих программ, т.к.так как в Haskell’еHaskell императивный и функциональный миры чётко разделены между собой. В отличие от Haskell’аHaskell, в императивных языках, в которых есть функциональные подъязыки, нет чёткого разделения между обозначенными мирами.
|