Исключения[11]

В любой программе может встретиться фрагмент, который может отработать неправильно. Разные языки предлагают различные способы обработки подобных ошибок. В языке С мы обычно используем некоторое заведомо неправильное возвращаемое значение (например, –1 или пустой указатель), чтобы указать, что результат функции не должен рассматриваться как правильное значение. Языки Java и С#, с другой стороны, предлагают использовать для обработки ошибок механизм исключений. Когда возникает исключительная ситуация, выполнение программы передаётся некоему определённому нами участку кода, который выполняет ряд действий по восстановлению и, возможно, снова вызывает исключение, чтобы другой код для обработки ошибок мог выполниться и позаботиться о каких-либо других вещах.

В языке Haskell очень хорошая система типов. Алгебраические типы данных позволяют объявить такие типы данных, как Maybe и Either; мы можем использовать значения этих типов для представления результатов, которые могут отсутствовать. В языке C выбор, скажем, –1 для сигнала об ошибке – это просто предварительная договорённость. Эта константа имеет значение только для человека. Если мы не очень аккуратны, то можем трактовать подобные специальные значения как допустимые, и затем они могут привести к упадку и разорению вашего кода. Система типов языка Haskell даёт нам столь желанную безопасность в этом аспекте. Функция a –> Maybe b явно указывает, что результатом может быть значение типа b, завёрнутое в конструктор Just, или значение Nothing. Тип функции отличается от простого a –> b, и если мы попытаемся использовать один тип вместо другого, компилятор будет «жаловаться» на нас.

Кроме алгебраических типов, хорошо представляющих вычисления, которые могут закончиться неудачей, язык Haskell имеет поддержку исключительных ситуаций, так как они приобретают особое значение в контексте ввода-вывода. Всё может пойти вкривь и вкось, если вы работаете с внешним миром, который столь ненадёжен! Например, при открытии файла может случиться всякое. Он может быть заблокирован, его может не оказаться по заданному пути, или не будет такого диска, или ещё что-нибудь…

При возникновении исключительной ситуации хорошо бы иметь возможность перейти на некоторый код обработки ошибки. Хорошо, код для ввода-вывода (то есть «грязный» код) может вызывать исключения. Имеет смысл. Ну а как насчёт чистого кода? Он тоже может вызывать исключения! Вспомним функции div и head. Их типы – (Integral a) => a –> a –> a и [a] –> a соответственно. Никаких значений типа Maybe или Either в возвращаемом типе, и тем не менее они могут вызвать ошибку! Функция div взорвётся у вас в руках, если вы попытаетесь разделить на нуль, а функция head выпадет в осадок, если передать ей пустой список.

ghci> 4 `div` 0

*** Exception: divide by zero

ghci> head []

*** Exception: Prelude.head: empty list

Чистый код может выбрасывать исключения, но они могут быть перехвачены только в части кода, работающей с системой ввода-вывода (когда мы внутри блока do в функции main). Причина в том, что вы не знаете, когда что-то будет (если вообще будет!) вычислено в чистом коде, так как он ленив и не имеет жёстко определённого порядка выполнения, в то время как код для ввода-вывода такой порядок имеет.

Раньше мы говорили, что нам желательно проводить как можно меньше времени в части нашей программы, посвящённой вводу-выводу. Логика программы должна располагаться главным образом в чистых функциях, поскольку их результат зависит только от параметров, с которыми функции были вызваны. При работе с чистыми функциями вы должны думать только о том, что функции возвращают, так как они не могут сделать чего-либо другого. Это облегчит вам жизнь!.. Даже несмотря на то, что некоторая логика в коде для ввода-вывода необходима (например, открытие файлов и т. п.), она должна быть сведена к минимуму. Чистые функции по умолчанию ленивы; следовательно, мы не знаем, когда они будут вычислены – это не должно иметь значения. Но как только чистые функции начинают вызывать исключения, становится важным момент их выполнения. Вот почему мы можем перехватывать исключения из чистых функций в части кода, посвящённой вводу-выводу. И это плохо: ведь мы стремимся оставить такую часть настолько маленькой, насколько возможно!… Однако если мы не перехватываем исключения, наша программа «падает». Решение? Не надо мешать исключения и чистый код! Пользуйтесь преимуществами системы типов языка Haskell и используйте типы вроде Either и Maybe для представления результатов, при вычислении которых может произойти ошибка.