В чем преимущество отсутствия «исключений во время выполнения», как утверждает Элм?

16

Некоторые языки утверждают, что у них нет «исключений времени выполнения», как явное преимущество перед другими языками, которые их имеют.

Я запутался в этом вопросе.

Насколько я знаю, исключение времени выполнения - это всего лишь инструмент:

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

С другой стороны, мне очень трудно отлаживать программное обеспечение, которое «проглатывает» исключения. Например

try { 
  myFailingCode(); 
} catch {
  // no logs, no crashes, just a dirty state
}

Таким образом, вопрос заключается в следующем: в чем заключается сильное теоретическое преимущество отсутствия «исключений во время выполнения»?


пример

https://guide.elm-lang.org/

Нет ошибок времени выполнения на практике. Нет нуля Нет undefined не является функцией.

atoth
источник
Я думаю, что это поможет предоставить пример или два языка, на которые вы ссылаетесь, и / или ссылки на такие утверждения. Это можно интерпретировать несколькими способами.
JimmyJames
Последний пример, который я нашел, был elm по сравнению с C, C ++, C #, Java, ECMAScript и т. Д. Я обновил свой вопрос @JimmyJames
atoth
2
Это первый раз, когда я услышал об этом. Моя первая реакция - позвонить в BS. Обратите внимание на слова ласки: на практике
JimmyJames
@atoth Я собираюсь отредактировать заголовок вашего вопроса, чтобы сделать его более понятным, потому что есть несколько несвязанных вопросов, которые похожи на него (например, «RuntimeException» против «Exception» в Java). Если вам не нравится новый заголовок, не стесняйтесь редактировать его снова.
Андрес Ф.
Хорошо, я хотел, чтобы это было достаточно общим, но я могу согласиться с этим, если это поможет, спасибо за ваш вклад @AndresF.!
atoth

Ответы:

28

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

Сравните это с Elm, где ошибки кодируются как Results или Maybes , которые оба являются значениями . Это означает, что вы получите ошибку компилятора, если не обработаете ошибку. Вы можете хранить их в переменной или даже в коллекции, чтобы отложить их обработку до удобного времени. Вы можете создать функцию для обработки ошибок в зависимости от приложения, вместо того чтобы повторять очень похожие блоки try-catch повсеместно. Вы можете объединить их в вычисления, которые будут успешными, только если все его части будут успешными, и их не нужно будет объединять в один блок попытки. Вы не ограничены встроенным синтаксисом.

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

Рассмотрим следующий пример. Вы можете вставить это в http://elm-lang.org/try, если хотите увидеть это в действии.

import Html exposing (Html, Attribute, beginnerProgram, text, div, input)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
import String

main =
  beginnerProgram { model = "", view = view, update = update }

-- UPDATE

type Msg = NewContent String

update (NewContent content) oldContent =
  content

getDefault = Result.withDefault "Please enter an integer" 

double = Result.map (\x -> x*2)

calculate = String.toInt >> double >> Result.map toString >> getDefault

-- VIEW

view content =
  div []
    [ input [ placeholder "Number to double", onInput NewContent, myStyle ] []
    , div [ myStyle ] [ text (calculate content) ]
    ]

myStyle =
  style
    [ ("width", "100%")
    , ("height", "40px")
    , ("padding", "10px 0")
    , ("font-size", "2em")
    , ("text-align", "center")
    ]

Обратите внимание, что String.toIntв calculateфункции есть возможность сбоя. В Java это может вызвать исключение времени выполнения. Поскольку он читает пользовательский ввод, у него довольно хорошие шансы. Вместо этого, Элм заставляет меня разобраться с этим, возвращая Result, но заметьте, что мне не нужно сразу с этим разбираться. Я могу удвоить ввод и преобразовать его в строку, а затем проверить правильность ввода в getDefaultфункции. Это место гораздо лучше подходит для проверки, чем точка, где произошла ошибка, или вверх в стеке вызовов.

То, как компилятор форсирует нашу руку, также намного тоньше, чем проверенные исключения Java. Вам нужно использовать очень специфическую функцию, например, Result.withDefaultдля извлечения нужного значения. Хотя технически вы можете злоупотреблять этим механизмом, особого смысла нет. Поскольку вы можете отложить принятие решения до тех пор, пока не узнаете хорошее сообщение по умолчанию / сообщение об ошибке, нет причин его не использовать.

Карл Билефельдт
источник
8
That means you get a compiler error if you don't handle the error.- Ну, это и есть причина Проверенных Исключений в Java, но мы все знаем, насколько хорошо это сработало.
Роберт Харви
4
@RobertHarvey В некотором смысле, проверенные исключения Java были версией этого бедняка. К сожалению, их можно «проглотить» (как в примере с ОП). Они также не были настоящими типами, что делает их дополнительным путем к потоку кода. Языки с лучшими системами типа позволяют вам ошибку закодировать как значения первого класса, который является то , что вы делаете в (скажу) Haskell с Maybe, Eitherи т.д. Elm выглядит он берет страницу из таких языков, как ML, OCaml или Haskell.
Андрес Ф.
3
@JimmyJames Нет, они не заставляют тебя. Вам нужно «обработать» ошибку только тогда, когда вы хотите использовать значение. Если я это сделаю x = some_func(), мне не нужно ничего делать, если я не хочу проверить значение x, и в этом случае я могу проверить, есть ли у меня ошибка или «допустимое» значение; более того, попытка использовать одно вместо другого является статической ошибкой, поэтому я не могу этого сделать. Если типы Elm работают так же, как другие функциональные языки, я на самом деле могу делать такие вещи, как составление значений из разных функций, прежде чем я даже узнаю, являются ли они ошибками или нет! Это типично для языков FP.
Андрес Ф.
6
@atoth Но вы действительно имеют значительные выгоды и там есть очень хорошая причина (как это было объяснено в нескольких ответах на ваш вопрос). Я искренне рекомендую вам выучить язык с ML-подобным синтаксисом, и вы увидите, как это освобождает, чтобы избавиться от C-подобного синтаксического несоответствия (ML, кстати, был разработан в начале 70-х годов, что делает его примерно современник с). Люди, которые проектировали такие системы типов, считают этот тип синтаксиса нормальным, в отличие от C :). Хотя вы и занимаетесь этим, не мешало бы изучать и Lisp :)
Andres F.
6
@atoth Если ты хочешь извлечь что-то из всего этого, возьми это: всегда следи за тем, чтобы не стать жертвой Blub Paradox . Не раздражайтесь от нового синтаксиса. Может быть, это из-за мощных функций, с которыми вы не знакомы :)
Андрес Ф.
10

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

Это звучит зловеще. Что ж, проверка типов похожа на проверку теорем. (На самом деле, согласно Каром-Говарду-Изоморфизму, это одно и то же.) Одна вещь, которая очень специфична в теоремах, это то, что когда вы доказываете теорему, вы доказываете в точности то, что говорит теорема, не более. (Вот, например, почему, когда кто-то говорит «Я доказал, что эта программа верна», вы всегда должны спрашивать «пожалуйста, определите« правильно »».) То же самое верно для систем типов. Когда мы говорим «программа является типобезопасной», мы имеем в виду не то, что возможная ошибка не возникает. Мы можем только сказать, что ошибки, которые система типов обещает нам предотвратить, не могут возникнуть.

Таким образом, программы могут иметь бесконечно много разных поведений во время выполнения. Из них бесконечно много полезных, но также бесконечно много «неправильных» (для разных определений «правильности»). Система статических типов позволяет нам доказать, что определенный конечный фиксированный набор этих бесконечно многих некорректных поведений во время выполнения не может возникнуть.

Разница между системами разных типов заключается в том, что, сколько и как сложно они могут возникнуть во время выполнения. Слабые системы типов, такие как Java, могут доказать только самые простые вещи. Например, Java может доказать, что метод, типизированный как возвращающий a, Stringне может вернуть a List. Но, например, он не может доказать, что метод не вернется. Это также не может доказать, что метод не будет генерировать исключение. И он не может доказать, что он не вернет неправильное String  - любой Stringудовлетворит проверку типов. (И, конечно, даже nullудовлетворит его.) Есть даже очень простые вещи , которые Java не могут доказать, что именно поэтому у нас есть исключения , например ArrayStoreException, ClassCastExceptionили все любимец, то NullPointerException.

Более мощные системы типов, такие как Agda, могут также доказать, что «вернет сумму двух аргументов» или «вернет отсортированную версию списка, переданного в качестве аргумента».

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

Таким образом, они не говорят «мы не реализуем исключения». Они говорят, что «вещи, которые могли бы быть исключениями времени выполнения в типичных языках, с которыми обычные программисты, приходящие в Elm, имели бы опыт, попадают в систему типов». Конечно, кто-то из Идриса, Агды, Гуру, Эпиграма, Изабель / HOL, Coq или похожих языков увидит Элма довольно слабым по сравнению. Это утверждение больше предназначено для типичных программистов на Java, C♯, C ++, Objective-C, PHP, ECMAScript, Python, Ruby, Perl,….

Йорг Миттаг
источник
5
Примечание для потенциальных редакторов: я очень сожалею об использовании двойных и даже тройных негативов. Однако я оставил их намеренно: системы типов гарантируют отсутствие определенных видов поведения во время выполнения, то есть гарантируют, что определенные вещи не произойдут. И я хотел сохранить эту формулировку «докажи, что ее не происходит», что, к сожалению, приводит к таким конструкциям, как «она не может доказать, что метод не вернется». Если вы можете найти способ улучшить их, продолжайте, но имейте это в виду. Спасибо!
Йорг Миттаг
2
Хороший ответ в целом, но одна маленькая мелочь: «проверка типов похожа на проверку теорем». На самом деле, средство проверки типов больше похоже на средство проверки теорем: они оба проверяют , а не выводят .
садовник
4

Elm может гарантировать отсутствие исключений времени выполнения по той же причине, что C может гарантировать отсутствие исключений времени выполнения: язык не поддерживает концепцию исключений.

В Elm есть способ оповещения об ошибках во время выполнения, но эта система не исключение, это «Результаты». Функция, которая может потерпеть неудачу, возвращает «Результат», который содержит либо обычное значение, либо ошибку. Вязы строго типизированы, так что это явно в системе типов. Если функция всегда возвращает целое число, она имеет тип Int. Но если он либо возвращает целое число, либо терпит неудачу, тип возвращаемого значения Result Error Int. (Строка является сообщением об ошибке.) Это заставляет вас явно обрабатывать оба случая на сайте вызова.

Вот пример из введения (немного упрощенный):

view : String -> String 
view userInputAge =
  case String.toInt userInputAge of
    Err msg ->
        text "Not a valid number!"

    Ok age ->
        text "OK!"

Функция toInt может завершиться ошибкой, если входные данные не могут быть проанализированы, поэтому тип возвращаемого значения - Result String int. Чтобы получить действительное целочисленное значение, вы должны «распаковать» с помощью сопоставления с образцом, что, в свою очередь, заставляет вас обрабатывать оба случая.

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

JacquesB
источник
2
@atoth Вот пример. Представьте, что язык А допускает исключения. Тогда вам дана функция doSomeStuff(x: Int): Int. Обычно вы ожидаете, что он вернет an Int, но может ли он также выдать исключение? Не глядя на его исходный код, вы не можете знать. Напротив, язык B, который кодирует ошибки через типы, может иметь такую ​​же функцию, объявленную так: doSomeStuff(x: Int): ErrorOrResultOfType<Int>(в Elm этот тип фактически называется Result). В отличие от первого случая, теперь сразу очевидно, может ли функция выйти из строя, и вы должны обращаться с ней явно.
Андрес Ф.
1
Как подразумевает @RobertHarvey в комментарии к другому ответу, это похоже на проверенные исключения в Java. Что я узнал от работы с Java в первые дни, когда проверялось большинство исключений, так это то, что вы действительно не хотите, чтобы вас всегда заставляли писать код всегда с ошибками в момент их возникновения.
JimmyJames
2
@JimmyJames Это не похоже на проверенные исключения, потому что исключения не составляются, их можно игнорировать («проглотить») и они не являются первоклассными значениями :) Я действительно рекомендую изучать статически типизированный функциональный язык, чтобы действительно понять это. Это не какая-то новомодная вещь, которую придумал Элм, - это то, как вы программируете на таких языках, как ML или Haskell, и это отличается от Java.
Андрес Ф.
2
@AndresF. this is how you program in languages such as ML or HaskellВ Хаскеле, да; ML, нет. Роберт Харпер, основной участник Standard ML и исследователь языка программирования, считает исключения полезными . Типы ошибок могут мешать составлению функций в тех случаях, когда вы можете гарантировать, что ошибка не возникнет. Исключения также имеют различную производительность. Вы не платите за исключения, которые не генерируются, но вы платите за проверку значений ошибок каждый раз, а исключения являются естественным способом выражения возврата в некоторых алгоритмах
Doval
2
@JimmyJames Надеюсь, теперь вы видите, что проверенные исключения и фактические типы ошибок только внешне похожи. Проверенные исключения не сочетаются изящно, громоздки в использовании и не ориентированы на выражения (и поэтому вы можете просто «проглотить» их, как это происходит с Java). Непроверенные исключения являются менее громоздкими, поэтому они везде являются нормой, но в Java, но они с большей вероятностью сбивают вас с толку, и вы не можете определить, глядя на объявление функции, бросает она или нет, делая вашу программу труднее понять.
Андрес Ф.
2

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

Этот вопрос может быть истолкован по-разному, но поскольку вы упомянули Elm в комментариях, контекст стал более понятным.

Элм, помимо прочего, является статически типизированным языком программирования. Одним из преимуществ систем такого типа является то, что многие классы ошибок (хотя и не все) обнаруживаются компилятором до фактического использования программы. Некоторые виды ошибок могут быть закодированы в типах (таких как Elm's ResultиTask ), а не выбрасываться как исключения. Это то, что имеют в виду дизайнеры Elm: многие ошибки будут обнаружены во время компиляции, а не во время выполнения, и компилятор заставит вас разбираться с ними, а не игнорировать их и надеяться на лучшее. Понятно, почему это преимущество: лучше, чтобы программист узнал о проблеме раньше, чем пользователь.

Обратите внимание, что когда вы не используете исключения, ошибки кодируются другими, менее удивительными способами. Из документации Вяза :

Одна из гарантий Elm заключается в том, что вы не увидите ошибок времени выполнения на практике. NoRedInk использует Elm в производстве уже около года, а у них до сих пор не было! Как и все гарантии в Elm, это сводится к фундаментальному выбору языкового дизайна. В этом случае нам помогает тот факт, что Elm рассматривает ошибки как данные. (Вы заметили, что мы делаем данные здесь много?)

Дизайнеры Elm немного смелы в утверждении «никаких исключений во время выполнения» , хотя они квалифицируют его как «на практике». Вероятно, они означают «менее неожиданные ошибки, чем если бы вы кодировали в javascript».

Андрес Ф.
источник
Я неправильно понимаю, или они просто играют в семантическую игру? Они объявили вне закона имя «исключение во время выполнения», но затем просто заменили его другим механизмом, который передает информацию об ошибках в стек. Это звучит как просто изменение реализации исключения на аналогичную концепцию или объект, который по-разному реализует сообщение об ошибке. Это вряд ли потрясающее. Это как любой статически типизированный язык. Сравните переход от COM HRESULT к исключению .NET. Другой механизм, но все же это исключение времени выполнения, независимо от того, как вы его называете.
Майк поддерживает Монику
@ Майк Если честно, я не смотрел на Элма подробно. Судя по документам, у них есть типы Resultи Taskкоторые очень похожи на более знакомые Eitherи Futureс других языков. В отличие от исключений, значения этих типов могут быть объединены, и в какой-то момент вы должны явно обработать их: представляют ли они допустимое значение или ошибку? Я не читаю мысли, но это отсутствие удивления программиста, вероятно, то, что дизайнеры Elm имели в виду под «без исключений времени выполнения» :)
Andres F.
@ Майк, я согласен, это не потрясающе. Разница с исключениями времени выполнения состоит в том, что они не являются явными в типах (т.е. вы не можете узнать, не глядя на его исходный код, может ли кусок кода генерироваться); Ошибки кодирования в типах очень явные и не позволяют программисту игнорировать их, что приводит к более безопасному коду. Это сделано многими языками FP и действительно ничего нового.
Андрес Ф.
1
Что касается ваших комментариев, я думаю, что это больше, чем «статические проверки типов» . Вы можете добавить его в JS, используя Typescript, который намного менее стеснителен, чем новая экосистема "сделай или сломай".
atoth
1
@AndresF .: Технически говоря, новейшей особенностью системы типов Java является параметрический полиморфизм, который происходит с конца 60-х годов. Таким образом, говоря словами «современный», когда вы имеете в виду «не Java», это несколько правильно.
Йорг Миттаг
0

Вяз утверждает:

Нет ошибок времени выполнения на практике. Нет нуля Нет undefined не является функцией.

Но вы спрашиваете об исключениях во время выполнения . Есть разница

В Elm ничего не дает неожиданного результата. Вы НЕ МОЖЕТЕ написать правильную программу в Elm, которая выдает ошибки во время выполнения. Таким образом, вам не нужны исключения.

Итак, вопрос должен быть:

В чем преимущество отсутствия ошибок во время выполнения?

Если вы можете написать код, который никогда не имеет ошибок во время выполнения, ваши программы никогда не будут аварийно завершаться.

Эктор
источник