Quel est l'avantage de n'avoir «aucune exception d'exécution», comme le prétend Elm?

16

Certaines langues prétendent n'avoir «aucune exception d'exécution» comme un net avantage sur les autres langues qui en disposent.

Je suis confus à ce sujet.

L'exception d'exécution n'est qu'un outil, pour autant que je sache, et lorsqu'il est bien utilisé:

  • vous pouvez communiquer des états "sales" (lancer des données inattendues)
  • en ajoutant une pile, vous pouvez pointer vers la chaîne d'erreur
  • vous pouvez faire la distinction entre l'encombrement (par exemple, renvoyer une valeur vide sur une entrée non valide) et une utilisation non sécurisée qui nécessite l'attention d'un développeur (par exemple, lever une exception sur une entrée non valide)
  • vous pouvez ajouter des détails à votre erreur avec le message d'exception fournissant d'autres détails utiles aidant aux efforts de débogage (théoriquement)

Par contre je trouve vraiment difficile de déboguer un logiciel qui "avale" les exceptions. Par exemple

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

La question est donc: quel est l'avantage théorique fort d'avoir "aucune exception d'exécution"?


Exemple

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

Aucune erreur d'exécution dans la pratique. Pas de null. Aucun indéfini n'est pas une fonction.

atoth
la source
Je pense qu'il serait utile de fournir un exemple ou deux des langues auxquelles vous faites référence et / ou des liens vers de telles allégations. Cela pourrait être interprété de plusieurs façons.
JimmyJames
Le dernier exemple que j'ai trouvé était orme par rapport à C, C ++, C #, Java, ECMAScript, etc. J'ai mis à jour mes @JimmyJames question
atoth
2
C'est la première fois que j'en entends parler. Ma première réaction est d'appeler BS. Notez les mots de belette: dans la pratique
JimmyJames
@atoth, je vais modifier le titre de votre question pour la rendre plus claire, car il existe plusieurs questions sans rapport qui lui ressemblent (comme "RuntimeException" vs "Exception" en Java). Si vous n'aimez pas le nouveau titre, n'hésitez pas à le modifier à nouveau.
Andres F.
OK, je voulais que ce soit assez général, mais je peux être d'accord si cela aide, merci pour votre contribution @AndresF.!
atoth

Réponses:

28

Les exceptions ont une sémantique extrêmement limitative. Ils doivent être manipulés exactement là où ils sont lancés, ou dans la pile d'appels directs vers le haut, et il n'y a aucune indication au programmeur au moment de la compilation si vous oubliez de le faire.

Comparez cela à Elm où les erreurs sont codées en tant que résultats ou Maybes , qui sont les deux valeurs . Cela signifie que vous obtenez une erreur de compilation si vous ne gérez pas l'erreur. Vous pouvez les stocker dans une variable ou même une collection pour différer leur manipulation à un moment opportun. Vous pouvez créer une fonction pour gérer les erreurs d'une manière spécifique à l'application au lieu de répéter des blocs try-catch très similaires partout. Vous pouvez les enchaîner dans un calcul qui ne réussit que si toutes ses parties réussissent, et elles ne doivent pas être entassées dans un bloc d'essai. Vous n'êtes pas limité par la syntaxe intégrée.

Cela ne ressemble en rien à «avaler des exceptions». Cela rend les conditions d'erreur explicites dans le système de types et fournit une sémantique alternative beaucoup plus flexible pour les gérer.

Prenons l'exemple suivant. Vous pouvez coller ceci dans http://elm-lang.org/try si vous souhaitez le voir en action.

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")
    ]

Notez que String.toIntdans la calculatefonction a la possibilité d'échouer. En Java, cela a le potentiel de lever une exception d'exécution. Lorsqu'il lit les entrées utilisateur, il a assez de chances de le faire. Au lieu de cela, Elm m'oblige à y faire face en retournant un Result, mais remarquez que je n'ai pas à m'en occuper tout de suite. Je peux doubler l'entrée et la convertir en chaîne, puis rechercher une mauvaise entrée dans la getDefaultfonction. Cet endroit est beaucoup mieux adapté au contrôle que le point où l'erreur s'est produite ou vers le haut dans la pile des appels.

La façon dont le compilateur force notre main est également beaucoup plus fine que les exceptions vérifiées de Java. Vous devez utiliser une fonction très spécifique comme Result.withDefaultextraire la valeur souhaitée. Bien que, techniquement, vous puissiez abuser de ce type de mécanisme, cela ne sert à rien. Puisque vous pouvez reporter la décision jusqu'à ce que vous connaissiez un bon message par défaut / d'erreur à mettre, il n'y a aucune raison de ne pas l'utiliser.

Karl Bielefeldt
la source
8
That means you get a compiler error if you don't handle the error.- Eh bien, c'était le raisonnement derrière Checked Exceptions en Java, mais nous savons tous à quel point cela a fonctionné.
Robert Harvey
4
@RobertHarvey D'une certaine manière, les exceptions vérifiées de Java étaient la version du pauvre. Malheureusement, ils pourraient être "avalés" (comme dans l'exemple du PO). Ils n'étaient pas non plus de vrais types, ce qui en faisait un chemin supplémentaire superflu vers le flux de code. Langues avec de meilleurs systèmes de type permettent aux erreurs de codage en tant que valeurs de première classe, ce qui est ce que vous faites ( par exemple) Haskell avec Maybe, Either, etc. regards Elm comme il prend une page de langues telles que ML, OCaml ou Haskell.
Andres F.
3
@JimmyJames Non, ils ne vous forcent pas. Vous ne devez "gérer" l'erreur que lorsque vous souhaitez utiliser la valeur. Si je fais x = some_func(), je ne dois rien faire à moins que je veux examiner la valeur x, dans ce cas , je peux vérifier si j'ai une erreur ou une valeur « valide »; de plus, c'est une erreur de type statique pour tenter d'utiliser l'un à la place de l'autre, donc je ne peux pas le faire. Si les types d'Elm fonctionnent quelque chose comme d'autres langages fonctionnels, je peux réellement faire des choses comme composer des valeurs à partir de différentes fonctions avant même de savoir si ce sont des erreurs ou non! C'est typique des langages FP.
Andres F.
6
@atoth Mais vous n'avez des gains importants et il est une très bonne raison (comme cela a été expliqué dans plusieurs réponses à votre question). Je vous encourage vraiment à apprendre un langage avec une syntaxe de type ML et vous verrez à quel point il est libérateur de se débarrasser de la syntaxe de type C (ML, soit dit en passant, a été développé au début des années 70, ce qui le rend à peu près un contemporain de C). Les personnes qui ont conçu ce type de systèmes de types considèrent ce type de syntaxe comme normal, contrairement à C :) Pendant que vous y êtes, cela ne ferait pas de mal non plus d'apprendre un Lisp :)
Andres F.
6
@atoth Si vous voulez tirer une chose de tout cela, prenez celle-ci: assurez-vous toujours de ne pas être la proie du Blub Paradox . Ne soyez pas irrité par la nouvelle syntaxe. Peut-être que c'est là à cause de fonctionnalités puissantes que vous ne connaissez pas :)
Andres F.
10

Pour comprendre cette affirmation, nous devons d'abord comprendre ce qu'un système de type statique nous achète. En substance, ce qu'un système de type statique nous donne est une garantie: si les vérifications de type de programme, une certaine classe de comportements d'exécution ne peut pas se produire.

Cela semble inquiétant. Eh bien, un vérificateur de type est similaire à un vérificateur de théorème. (En fait, selon l'isomorphisme de Curry-Howard, c'est la même chose.) Une chose qui est très particulière à propos des théorèmes est que lorsque vous prouvez un théorème, vous prouvez exactement ce que dit le théorème, pas plus. (C'est par exemple pourquoi, lorsque quelqu'un dit "J'ai prouvé que ce programme est correct", vous devez toujours demander "veuillez définir" correct "".) La même chose est vraie pour les systèmes de type. Lorsque nous disons "un programme est sûr pour le type", nous ne voulons pas dire qu'aucune erreur possible ne peut se produire. Nous pouvons seulement dire que les erreurs que le système de type nous promet d'éviter ne peuvent pas se produire.

Ainsi, les programmes peuvent avoir une infinité de comportements d'exécution différents. Parmi ceux-ci, un nombre infini de ceux-ci sont utiles, mais aussi un nombre infini de ceux-ci sont "incorrects" (pour diverses définitions de "l'exactitude"). Un système de type statique nous permet de prouver qu'un certain ensemble fini et fixe de ces infiniment de comportements d'exécution incorrects ne peut pas se produire.

La différence entre les différents types de systèmes réside essentiellement dans le fait de savoir combien, et combien de comportements d'exécution complexes ils peuvent ne pas se produire. Les systèmes de type faible tels que Java ne peuvent prouver que des choses très basiques. Par exemple, Java peut prouver qu'une méthode qui est typée comme retournant un Stringne peut pas retourner un List. Mais, par exemple, il ne peut pas prouver que la méthode ne reviendra pas. Il ne peut pas non plus prouver que la méthode ne lèvera pas d'exception. Et il ne peut pas prouver qu'il ne renverra pas le mauvais String  - tout Stringsatisfera le vérificateur de type. (Et, bien sûr, même nullsatisfera aussi bien.) Il y a même des choses que Java ne peut pas prouver très simple, ce qui est la raison pour laquelle nous avons des exceptions comme ArrayStoreException, ClassCastExceptionou tout le monde le favori, le NullPointerException.

Des systèmes de types plus puissants comme Agda peuvent également prouver des choses comme «renverra la somme des deux arguments» ou «renvoie la version triée de la liste passée en argument».

Maintenant, ce que les concepteurs d'Elm entendent par la déclaration selon laquelle ils n'ont pas d'exceptions d'exécution, c'est que le système de type d'Elm peut prouver l'absence de (une partie importante) des comportements d'exécution qui, dans d'autres langues, ne peuvent pas être prouvés comme ne se produisant pas et pourraient donc conduire à un comportement erroné lors de l'exécution (ce qui dans le meilleur des cas signifie une exception, dans le pire des cas, un crash, et dans le pire des cas, aucun crash, aucune exception, et juste un résultat silencieusement erroné).

Donc, ils ne disent pas "nous n'implémentons pas d'exceptions". Ils disent que "les choses qui seraient des exceptions d'exécution dans des langages typiques que les programmeurs typiques venant avec Elm auraient expérimentés, sont prises par le système de type". Bien sûr, quelqu'un venant d'Idris, Agda, Guru, Epigram, Isabelle / HOL, Coq ou d'autres langues similaires verra Elm comme assez faible en comparaison. L'instruction s'adresse davantage aux programmeurs Java, C♯, C ++, Objective-C, PHP, ECMAScript, Python, Ruby, Perl,… typiques.

Jörg W Mittag
la source
5
Note aux rédacteurs potentiels: je suis vraiment désolé pour l'utilisation de doubles et même triples négatifs. Cependant, je les ai laissés exprès: les systèmes de types garantissent l'absence de certains types de comportements d'exécution, c'est-à-dire qu'ils garantissent que certaines choses ne se produiront pas. Et je voulais garder intacte cette formulation "ne pas se produire", ce qui conduit malheureusement à des constructions comme "ça ne peut pas prouver qu'une méthode ne reviendra pas". Si vous pouvez trouver un moyen de les améliorer, allez-y, mais n'oubliez pas ce qui précède. Je vous remercie!
Jörg W Mittag
2
Bonne réponse dans l'ensemble, mais un petit pincement: "un vérificateur de type est similaire à un prouveur de théorème." En fait, un vérificateur de type s'apparente davantage à un vérificateur de théorème: ils font tous deux la vérification , pas la déduction .
gardenhead
4

Elm ne peut garantir aucune exception d'exécution pour la même raison C ne peut garantir aucune exception d'exécution: le langage ne prend pas en charge le concept d'exceptions.

Elm a un moyen de signaler les conditions d'erreur lors de l'exécution, mais ce système ne fait pas exception, il s'agit de "Résultats". Une fonction qui peut échouer renvoie un "résultat" qui contient soit une valeur régulière soit une erreur. Elms est fortement typé, c'est donc explicite dans le système de type. Si une fonction retourne toujours un entier, elle a le type Int. Mais s'il retourne un entier ou échoue, le type de retour est Result Error Int. (La chaîne est le message d'erreur.) Cela vous oblige à gérer explicitement les deux cas sur le site d'appel.

Voici un exemple de l'introduction (un peu simplifié):

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

    Ok age ->
        text "OK!"

La fonction toIntpeut échouer si l'entrée n'est pas analysable, donc son type de retour l'est Result String int. Pour obtenir la valeur entière réelle, vous devez "décompresser" via la correspondance de modèles, ce qui vous oblige à gérer les deux cas.

Les résultats et les exceptions font fondamentalement la même chose, la différence importante étant les «défauts». Les exceptions bouillonneront et mettront fin au programme par défaut, et vous devez les intercepter explicitement si vous voulez les gérer. Le résultat est dans l'autre sens - vous êtes obligé de les gérer par défaut, vous devez donc les passer explicitement jusqu'au sommet si vous voulez qu'ils terminent le programme. Il est facile de voir comment ce comportement peut conduire à un code plus robuste.

JacquesB
la source
2
@atoth Voici un exemple. Imaginez que le langage A autorise des exceptions. Ensuite, vous avez la fonction doSomeStuff(x: Int): Int. Normalement, vous vous attendez à ce qu'il renvoie un Int, mais peut-il également lever une exception? Sans regarder son code source, vous ne pouvez pas savoir. En revanche, un langage B qui code des erreurs via des types peut avoir la même fonction déclarée comme ceci: doSomeStuff(x: Int): ErrorOrResultOfType<Int>(dans Elm ce type est en fait nommé Result). Contrairement au premier cas, il est maintenant immédiatement évident que la fonction peut échouer et vous devez la gérer explicitement.
Andres F.
1
Comme @RobertHarvey l'indique dans un commentaire sur une autre réponse, cela semble être fondamentalement comme des exceptions vérifiées en Java. Ce que j'ai appris en travaillant avec Java au début, lorsque la plupart des exceptions étaient vérifiées, c'est que vous ne voulez vraiment pas être obligé d'écrire du code pour toujours des erreurs au moment où elles se produisent.
JimmyJames
2
@JimmyJames Ce n'est pas comme les exceptions vérifiées parce que les exceptions ne se composent pas, peuvent être ignorées ("avalées") et ne sont pas des valeurs de première classe :) Je recommande vraiment d'apprendre un langage fonctionnel typé statiquement pour vraiment comprendre cela. Ce n'est pas quelque chose de nouveau inventé par Elm - c'est ainsi que vous programmez dans des langages tels que ML ou Haskell, et c'est différent de Java.
Andres F.
2
@AndresF. this is how you program in languages such as ML or HaskellÀ Haskell, oui; ML, non. Robert Harper, un contributeur majeur au chercheur Standard ML et le langage de programmation, considère les exceptions soient utiles . Les types d'erreur peuvent entraver la composition de la fonction dans les cas où vous pouvez garantir qu'une erreur ne se produira pas. Les exceptions ont également des performances différentes. Vous ne payez pas pour les exceptions qui ne sont pas levées, mais vous payez pour vérifier les valeurs d'erreur à chaque fois, et les exceptions sont un moyen naturel d'exprimer le retour en arrière dans certains algorithmes
Doval
2
@JimmyJames J'espère que vous voyez maintenant que les exceptions vérifiées et les types d'erreur réels ne sont que superficiellement similaires. Les exceptions vérifiées ne se combinent pas gracieusement, sont lourdes à utiliser et ne sont pas orientées vers les expressions (et vous pouvez donc simplement les "avaler", comme cela arrive avec Java). Les exceptions non vérifiées sont moins lourdes, c'est pourquoi elles sont la norme partout sauf en Java, mais elles sont plus susceptibles de vous trébucher, et vous ne pouvez pas le dire en regardant une déclaration de fonction si elle lève ou non, ce qui rend votre programme plus difficile comprendre.
Andres F.
2

Tout d'abord, veuillez noter que votre exemple d'exceptions de «déglutition» est en général une pratique terrible et complètement sans rapport avec l'absence d'exceptions de temps d'exécution; lorsque vous y réfléchissez, vous avez eu une erreur d'exécution, mais vous avez choisi de la masquer et de ne rien y faire. Cela entraînera souvent des bogues difficiles à comprendre.

Cette question pourrait être interprétée de plusieurs façons, mais comme vous avez mentionné Elm dans les commentaires, le contexte est plus clair.

Elm est, entre autres, un langage de programmation typé statiquement . L'un des avantages de ce type de systèmes de types est que de nombreuses classes d'erreurs (mais pas toutes) sont détectées par le compilateur, avant que le programme ne soit réellement utilisé. Certains types d'erreurs peuvent être encodés en types (tels que Elm Resultet Task), au lieu d'être levés comme exceptions. C'est ce que les concepteurs d'Elm veulent dire: de nombreuses erreurs seront détectées au moment de la compilation plutôt qu'au "moment de l'exécution", et le compilateur vous forcera à les traiter au lieu de les ignorer et d'espérer le meilleur. Il est clair pourquoi c'est un avantage: mieux vaut que le programmeur prenne conscience d'un problème avant que l'utilisateur ne le fasse.

Notez que lorsque vous n'utilisez pas d'exceptions, les erreurs sont codées d'autres manières moins surprenantes. D'après la documentation d'Elm :

L'une des garanties d'Elm est que vous ne verrez pas d'erreurs d'exécution dans la pratique. NoRedInk utilise Elm en production depuis environ un an maintenant, et ils n'en ont toujours pas! Comme toutes les garanties dans Elm, cela se résume aux choix fondamentaux de conception de langage. Dans ce cas, nous sommes aidés par le fait qu'Elm traite les erreurs comme des données. (Avez-vous remarqué que nous faisons beaucoup de données ici?)

Les concepteurs d'orme sont un peu audacieux en déclarant "aucune exception de temps d'exécution" , bien qu'ils le qualifient de "en pratique". Ce qu'ils signifient probablement "moins d'erreurs inattendues que si vous codiez en javascript".

Andres F.
la source
Suis-je en train de mal le lire ou jouent-ils simplement à un jeu sémantique? Ils interdisent le nom «exception d'exécution», mais le remplacent ensuite par un mécanisme différent qui transmet les informations d'erreur dans la pile. Cela revient à changer l'implémentation d'une exception en un concept ou un objet similaire qui implémente le message d'erreur différemment. C'est à peine bouleversant. C'est comme n'importe quelle langue typée statiquement. Comparez le passage d'un COM HRESULT à une exception .NET. Mécanisme différent, mais toujours une exception à l'exécution, peu importe comment vous l'appelez.
Mike soutient Monica
@Mike Pour être honnête, je n'ai pas regardé Elm en détail. À en juger par les documents, ils ont des types Resultet Taskqui ressemblent beaucoup aux plus familiers Eitheret à Futured'autres langues. Contrairement aux exceptions, les valeurs de ces types peuvent être combinées et à un moment donné, vous devez les gérer explicitement: représentent-elles une valeur valide ou une erreur? Je ne lis pas les esprits, mais ce manque de surprendre le programmeur est probablement ce que les concepteurs d'Elm entendaient par "aucune exception de temps d'exécution" :)
Andres F.
@ Mike, je suis d'accord que ce n'est pas bouleversant. La différence avec les exceptions d'exécution est qu'elles ne sont pas explicites dans les types (c'est-à-dire que vous ne pouvez pas savoir, sans regarder son code source, si un morceau de code peut lancer); les erreurs de codage dans les types sont très explicites et empêchent le programmeur de les ignorer, conduisant à un code plus sûr. Ceci est fait par de nombreux langages FP et n'est en effet rien de nouveau.
Andres F.
1
Selon vos commentaires, je pense qu'il y a plus que des "vérifications de type statiques" . Vous pouvez l'ajouter à JS en utilisant Typescript, ce qui est beaucoup moins contraignant qu'un nouvel écosystème "make-or-break".
atoth
1
@AndresF .: Techniquement parlant, la dernière caractéristique du système de type Java est le polymorphisme paramétrique, qui vient de la fin des années 60. Donc, d'une manière parlant, dire "moderne" quand vous voulez dire "pas Java" est quelque peu correct.
Jörg W Mittag
0

L'orme affirme:

Aucune erreur d' exécution dans la pratique. Pas de null. Aucun indéfini n'est pas une fonction.

Mais vous posez des questions sur les exceptions d' exécution . Il y a une différence.

Dans Elm, rien ne renvoie un résultat inattendu. Vous ne pouvez PAS écrire un programme valide dans Elm qui génère des erreurs d'exécution. Ainsi, vous n'avez pas besoin d'exceptions.

Donc, la question devrait être:

Quel est l'avantage d'avoir "aucune erreur d'exécution"?

Si vous pouvez écrire du code qui n'a jamais d'erreurs d'exécution, vos programmes ne se bloqueront jamais.

Hector
la source