Какова цель исключения исключений в чистых функциях?

Поскольку основным источником недетерминированных исключений является IO, и вы можете перехватывать исключение только внутри IO monad, он сулит разумно не бросать исключения из чистых функций.

В самом деле, что может быть так "исключительным" в чистой функции? Пустой список или деление на ноль не являются действительно исключительными и можно ожидать. Поэтому почему бы не использовать только Maybe, Either или [] для представления таких случаев в чистом коде.

Существует ряд чистых функций, таких как (!!), tail, div, которые генерируют исключения. В чем причина их небезопасности?

Ответ 1

Небезопасные функции - все примеры частичных функций; они не определены для каждого значения в своей области. Рассмотрим head :: [a] -> a. Его домен [a], но head не определен для []: нет значения типа a, которое было бы правильным для возврата. Что-то вроде safeHead :: [a] -> Maybe a является полной функцией, потому что вы можете вернуть действительный Maybe a для любого списка; safeHead [] = Nothing и safeHead (x:xs) = Just x.

В идеале ваша программа будет состоять только из полных функций, но на практике это не всегда возможно. (Возможно, существует слишком много значений undefined для прогнозирования, или вы не можете заранее знать, какие значения вызывают проблемы.) Исключение является очевидным показателем того, что ваша программа не определена корректно. Когда вы получаете исключение, это означает, что вам нужно изменить свой код на

  • Избегайте вызывать функцию над значением, для которого оно не определено
  • Замените функцию функцией, которая определена в значении проблемы.

Ни при каких обстоятельствах не следует "3. Продолжать выполнение с undefined значением вместо возвращаемого значения вашей функции" считаться приемлемым.

(Некоторая гипотеза следовать, но я считаю, что это в основном правильное.) Исторически сложилось так, что у Haskell не было хорошего способа обработки исключений. Вероятно, было легче проверить, был ли список пустым до вызова head :: [a] -> a, чем для обработки возвращаемого значения, такого как Maybe a. Это стало менее проблематичным после введения монадов, что обеспечило общую структуру для подачи вывода safeHead :: [a] -> Maybe a на функции типа a -> b. Учитывая, что легко понять, что head [] не определен, по крайней мере, просто предоставить полезное конкретное сообщение об ошибке, чем полагаться на общее сообщение об ошибке. Теперь, когда функции, подобные safeHead, легче работать, функции, такие как head, могут считаться историческими реликвиями, а не моделью для эмулирования.

Ответ 2

Иногда, что-то, что верно в отношении поведения программы, не может быть доказано в его исходном языке. В других случаях это может быть доказуемо, но неэффективно. В других случаях это может быть доказуемо, но доказать это потребует огромного количества времени и усилий со стороны программиста.

Пример

Data.Sequence представляет последовательности в виде деревьев с размерами аннотированных пальцев. Он поддерживает инвариант, что количество элементов в любом поддереве равно аннотации, хранящейся в ее корне. Реализация zipWith для последовательностей разбивает более длинную последовательность, чтобы она соответствовала длине более короткой, а затем использует эффективный, оперативно ленивый метод, чтобы закрепить их вместе.

Этот метод включает разделение второй последовательности несколько раз вдоль естественной структуры первой последовательности. Когда он достигает листа первой последовательности, он полагается на связанный фрагмент второй последовательности, имеющий ровно один элемент. Это гарантируется, если сохраняется аннотация-инвариант. Если этот инвариант терпит неудачу, zipWith не имеет опции, но выдает ошибку.

Чтобы закодировать аннотационный инвариант в Haskell, вам нужно будет индексировать основные части дерева пальцев с их длиной. Затем вам понадобится каждая операция, чтобы доказать, что она поддерживает инвариант. Это возможно, и такие языки, как Coq, Agda и Idris, пытаются уменьшить боль и неэффективность. Но у них все еще есть боль, а иногда и огромная неэффективность. Haskell на самом деле не настроен должным образом для такой работы и, возможно, никогда не будет большой для него (это просто не его главная цель как язык). Это было бы крайне болезненно, а также крайне неэффективно. Поскольку эффективность была причиной выбора этой реализации в первую очередь, это просто не вариант.

Ответ 3

У некоторых функций есть связанные с ними предусловия (!! требует действительного индекса, tail требует непустого списка, div требует ненулевого делителя). Нарушение предварительного условия должно привести к исключению, поскольку вы не выполнили контракт.

Альтернативой является не использование предварительных условий, а использование возвращаемого значения, указывающего, был ли вызов успешным или нет.

Это все основные функции, поэтому они должны быть просты в использовании, что является большой точкой в ​​пользу предусловий с исключениями. Они также чисты, поэтому никогда не бывает сюрпризов, когда они терпят неудачу: вы точно знаете, когда это произойдет, а именно, когда вы передаете аргументы, которые нарушают предварительные условия. Но, в конце концов, это сводится к выбору дизайна, с пунктами в пользу и против обоих решений.