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

м
Викификация, ссылки, пунктуация, прочее
м
м (Викификация, ссылки, пунктуация, прочее)
__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 ->&gt; IO ()</code>
 
Друг с другом действия связываются при помощи оператора связывания. Т.е.То есть символы <code>>&gt;&gt;=</code> выстраивают последовательность действий. Как известно, вместо этой функции можно использовать служебное слово <code>do</code>. Оно использует такой же двумерный синтексис[[w:Синтаксис (программирование)|синтаксис]], как и слова <code>let</code> и <code>where</code>, поэтому можно не использовать для разделения вызова функций символ « <code>; »</code>. При помощи слова <code>do</code> можно связывать вызовы функций, определение данных (при помощи символов « <code>-&gt;</code> ») и множество определений локальных [[w:Переменная (программирование)|переменных]] (служебное слово <code>let</code>).
 
Например, так можно определить программу, которая читает символ с [[w:Клавиатура|клавиатуры]] и выводит его на [[w:Монитор|экран]]:
 
<code>main :: IO ()
main = do c <&lt;- 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> в остальных случаях. Нельзя просто написать:
ready :: IO Bool
 
ready = do c <- getChar
<code>ready :: IO Bool
c == ’y’
ready = do c <&lt;- 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 <&lt;- getChar
if c == '\n’n' then return ””""
else do l <&lt;- 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 <&lt;- getChar
putChar 'c']</code>
 
Этот список не возбуждает никаких действий, он просто содержит их описания. Для того, чтобы выполнить эту структуру, т.е.то есть возбудить все еееё действия, необходима некоторая функция (например, <code>sequence_</code>):
 
<code>sequence_ :: [IO ()] ->&gt; IO ()
sequence_ [] = return ()
sequence_ (a:as) = do a
sequence as</code>
 
Эта функция может быть полезна для написания функции <code>putStr</code>, которая выводит строку на экран:
 
<code>putStr :: String ->&gt; 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 ->&gt; IO a)</code>, при этом функция <code>catch</code> ассоциирует (связывает) обработчик исключений с набором действий:
 
<code>catch :: IO a ->&gt; (IOError ->&gt; IO a) ->&gt; 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 ->&gt; return (”Error"Error: " ++ show err))
where getLine’’getLine'<em/>' = do c <&lt;- getChar’getChar'
if c == '\n’n' then return ””""
else do l <&lt;- 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 ->&gt; IOMode ->&gt; IO Handle
hClose :: Handle ->&gt; IO ()
data IOMode = ReadMode | WriteMode | AppendMode | ReadWriteMode</code>
 
Далее приводится пример программы, которая копирует один файл в другой:
 
<code>main = do fromHandle <&lt;- getAndOpenFile "Copy from: " ReadMode
toHandle <&lt;- getAndOpenFile "Copy to: " WriteMode
contents <&lt;- hGetContents fromHandle
hPutStr toHandle contents
hClose toHandle
hClose fromHandle
putStr "Done."
getAndOpenFile :: String ->&gt; IOMode ->&gt; IO Handle
getAndOpenFile prompt mode = do putStr prompt
name <&lt;- getLine
catch (openFile name mode) (\_ ->&gt; 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, в императивных языках, в которых есть функциональные подъязыки, нет чёткого разделения между обозначенными мирами.