Почему OCaml использует исключения вместо представления ошибок с помощью типов сумм? - PullRequest
7 голосов
/ 11 июля 2019

Я прочитал https://stackoverflow.com/a/12161946/, который несколько рассматривает исключения OCaml в контексте производительности и упоминает, что можно использовать исключения для преднамеренного управления потоком управления.

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

Мое понимание (и, пожалуйста, исправьте меня, если я ошибаюсь), заключается в том, что исключения в OCaml подрывают типсистема и тем самым усложнить рассуждения о конкретном состоянии программы.В отличие от совпадений для типов сумм, компилятор не будет проверять, обрабатываются ли все возможные случаи ошибок, что может стать проблемой, особенно если модификация библиотечной функции вводит новое состояние ошибки.Вот почему, например, язык программирования Zig обеспечивает обработку ошибок и предоставляет компиляторную конструкцию для проверки всех возможных случаев ошибок (https://ziglang.org/#A-fresh-take-on-error-handling).

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

Есть ли (много?) ситуации, когда исключения для обработки ошибок превосходят явную, проверенную компилятором обработку ошибок?

Я особенно не понимаю, как Hashtbl.find выбрасывает исключение. Учитывая, что Hashtbl.find_opt был введен несколько лет назадОзначает ли это некоторый сдвиг в направлении разработки стандартной библиотеки, не нарушая существующие программы?

Являются ли исключения в OCaml и стандартной библиотеке артефактом того времени, когда разрабатывался OCaml (например, были ли исключения, популярные вТимe / были ли их последствия полностью не поняты) и / или есть ли веские основания для исключения из языка?

Ответы [ 2 ]

4 голосов
/ 11 июля 2019

TL; DR; Основная причина - производительность. Вторая причина - это удобство использования.

Performace

Для переноса значения во время параметра (или тип result) требуется выделение и стоимость его выполнения. По сути, если у вас была функция, возвращающая int и поднимающая Not_found, если ничего не было найдено, то при изменении этой функции на int option будет выделено значение Some x, которое создаст в штучной упаковке значение, занимающее два слова в вашей куче. , Это по сравнению с нулевым распределением в версии, которая использовала исключения. Наличие этого в узком цикле может резко снизить общую производительность. Как от 10 до 100 раз, правда.

Даже если возвращаемое значение уже упаковано, оно по-прежнему будет содержать дополнительное поле (одно слово служебной информации) и один уровень косвенности.

Юзабилити

В не тотальном мире скоро становится очень очевидным, что не тотальность заразна и распространяется по всему вашему коду. То есть, если в вашей функции есть операция деления, и у вас нет исключений, чтобы скрыть этот факт, то вы должны распространять не совокупность вперед. Вскоре у вас будут все функции с ('a,'b) result, и вы будете использовать монаду Result, чтобы сделать ваш код управляемым. Но Монада Результатов - не что иное, как ограничение исключений, только медленнее и более неловко. Поэтому мы вернулись к статус-кво.

Есть ли идеальное решение?

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

Что делать прямо сейчас?

Пока мы, обычные люди, ожидаем доставки эффектов в OCaml, нам все еще приходится жить с исключениями и вариантами. так что нам делать? Ниже приведен мой личный кодекс поведения, который я использую при программировании в OCaml.

Для решения проблемы юзабилити я использую правило - использовать исключения для ошибок и ошибок программиста. Более конкретно, если функция имеет проверяемое и четко определенное предварительное условие, то ее неправильное использование является ошибкой программиста. Если программа не работает, она не должна запускаться. Таким образом, используйте исключения, если предварительное условие не выполнено. Хорошим примером является функция Array.init, которая завершается ошибкой, если аргумент size является отрицательным. Нет веских причин использовать тип суммы result, чтобы сообщить пользователю функции, что она использовала его неправильно. Критическим моментом с этим правилом является то, что предварительное условие должно быть проверяемым, а это означает, что проверка является быстрой и легкой. То есть, хост существует или сеть достижима не является предварительным условием.

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

Например, что-то вроде find_value_or_fail или (при использовании стиля Janesteet find_exn и просто find.

Кроме того, я всегда стараюсь сделать свои функции надежными, в основном следуя принципу надежности интернета. Или, с логической точки зрения, сделать их более сильными теориями. Другими словами, это означает, что я пытаюсь минимизировать набор предварительных условий и обеспечить разумное поведение для всех возможных входных данных. Например, вы можете обнаружить, что резкое деление на ноль имеет четко определенное значение в модульной арифметике, при котором GCD и LCM начнут иметь смысл, когда решетка делимости соответствует и присоединиться к операциям.

Наш мир, вероятно, является более полным и полным, чем наши теории, поскольку мы обычно не видим вокруг себя множества исключений :) Поэтому, прежде чем вызывать исключение или указывать на ошибку каким-либо иным способом, подумайте дважды, является ли она ошибкой илиэто просто неполнота вашей теории.

3 голосов
/ 11 июля 2019

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

Обработка деления на ноль была бы адом

Лучший пример, который я могу придумать, также простейший: / будет полной болью, если он вернет тип суммы. Простой кусок кода вроде:

let x = ( 4 / 2 ) + ( 3 / 2 ) (* OCaml code *)
let x' = match ( 4 / 2 ), ( 3 / 2 ) with
         | Some a, Some b -> Some ( a + b )
         | None, _ | _, None -> None (* Necessary code because of division by zero *)

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

Это создаст много мертвого кода

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

Добавление assert или printf потребует от вас изменения сигнатуры функции

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

Retro-совместимость

Последней причиной сохранения этих исключений является ретро-совместимость. Большая часть кода полагается на Hashtbl.find. Рефакторинг в OCaml прост, но мы говорим о полном пересмотре экосистемы с возможными ошибками и некоторой потерей эффективности.

...