Pourquoi concevoir un langage moderne sans mécanisme de traitement des exceptions?

47

De nombreux langages modernes fournissent de riches fonctionnalités de gestion des exceptions , mais le langage de programmation Swift d’Apple ne fournit pas de mécanisme de gestion des exceptions .

Imprégné d'exceptions que je suis, j'ai du mal à comprendre ce que cela signifie. Swift a des assertions, et bien sûr renvoie des valeurs; mais j'ai du mal à imaginer comment ma façon de penser basée sur les exceptions mappe sur un monde sans exceptions (et, d'ailleurs, pourquoi un tel monde est souhaitable ). Y a-t-il des choses que je ne peux pas faire dans une langue comme Swift que je pourrais faire avec des exceptions? Est-ce que je gagne quelque chose en perdant des exceptions?

Comment par exemple pourrais-je mieux exprimer quelque chose comme

try:
    operation_that_can_throw_ioerror()
except IOError:
    handle_the_exception_somehow()
else:
     # we don't want to catch the IOError if it's raised
    another_operation_that_can_throw_ioerror()
finally:
    something_we_always_need_to_do()

dans une langue (Swift, par exemple) qui ne gère pas les exceptions?

une partie
la source
11
Vous voudrez peut-être ajouter Go à la liste , si nous ignorons simplement panicce qui n’est pas tout à fait pareil. En plus de ce qui est dit là-bas, une exception n’est guère plus qu’un moyen sophistiqué (mais confortable) d’exécuter une action GOTO, bien que personne ne l’appelle ainsi, pour des raisons évidentes.
JensG
3
La réponse à votre question est la suivante: vous avez besoin d’un support linguistique pour les exceptions afin de les écrire. La prise en charge linguistique inclut généralement la gestion de la mémoire; puisqu’une exception peut être levée n’importe où et capturée n'importe où, il doit y avoir un moyen de supprimer des objets qui ne dépendent pas du flux de contrôle.
Robert Harvey
1
Je ne vous suis pas très bien, @Robert. C ++ parvient à prendre en charge les exceptions sans garbage collection.
Karl Bielefeldt
3
@ KarlBielefeldt: À ce que je comprends, à grands frais. Encore une fois, y a-t-il quelque chose qui est entrepris en C ++ sans grande dépense, au moins en effort et en connaissance de domaine requise?
Robert Harvey
2
@RobertHarvey: Bon point. Je suis un de ceux qui ne pensent pas assez à ces choses. On m'a laissé croire que l'ARC était GC, mais bien sûr que ce n'est pas le cas. Donc, en gros (si je comprends, à peu près), les exceptions seraient une chose en désordre (nonobstant C ++) dans un langage où l'élimination d'objets se baserait sur un flux de contrôle?
Vers

Réponses:

33

Dans la programmation intégrée, les exceptions étaient traditionnellement interdites, car les frais généraux liés à la décompression de la pile étaient considérés comme une variabilité inacceptable lorsqu’on essayait de maintenir les performances en temps réel. Bien que les smartphones puissent techniquement être considérés comme des plates-formes temps réel, ils sont suffisamment puissants pour permettre aux anciennes limitations des systèmes embarqués de ne plus être appliquées. Je soulève juste la question dans un souci de minutie.

Les exceptions sont souvent prises en charge dans les langages de programmation fonctionnels, mais si rarement utilisées qu'elles pourraient ne pas l'être. Une des raisons est l'évaluation paresseuse, qui est effectuée occasionnellement même dans des langues qui ne sont pas paresseuses par défaut. Avoir une fonction qui s'exécute avec une pile différente de celle où il était en file d'attente, il est difficile de déterminer où placer votre gestionnaire d'exceptions.

L'autre raison est que les fonctions de première classe permettent des constructions telles que des options et des futures qui vous offrent les avantages syntaxiques des exceptions avec plus de flexibilité. En d'autres termes, le reste du langage est suffisamment expressif pour que les exceptions ne vous rapportent rien.

Je ne connais pas bien Swift, mais le peu que j'ai lu au sujet de sa gestion des erreurs donne à penser qu'ils étaient destinés à la gestion des erreurs afin de suivre des modèles de style plus fonctionnels. J'ai vu des exemples de code avec successet des failureblocs qui ressemblent beaucoup à des futurs.

Voici un exemple utilisant un Futurede ce tutoriel Scala :

val f: Future[List[String]] = future {
  session.getRecentPosts
}
f onFailure {
  case t => println("An error has occured: " + t.getMessage)
}
f onSuccess {
  case posts => for (post <- posts) println(post)
}

Vous pouvez voir qu'il a à peu près la même structure que votre exemple utilisant des exceptions. Le futurebloc est comme un try. Le onFailurebloc est comme un gestionnaire d'exceptions. En Scala, comme dans la plupart des langages fonctionnels, Futureest complètement implémenté en utilisant le langage lui-même. Cela ne nécessite aucune syntaxe spéciale, contrairement aux exceptions. Cela signifie que vous pouvez définir vos propres constructions similaires. Peut-être ajouter un timeoutbloc, par exemple, ou une fonctionnalité de journalisation.

De plus, vous pouvez faire passer le futur, le renvoyer depuis la fonction, le stocker dans une structure de données ou autre. C'est une valeur de première classe. Vous n'êtes pas limité comme des exceptions qui doivent être propagées directement dans la pile.

Les options résolvent le problème de gestion des erreurs d'une manière légèrement différente, ce qui fonctionne mieux pour certains cas d'utilisation. Vous n'êtes pas coincé avec la seule méthode.

Voilà le genre de choses que vous "gagnez en perdant des exceptions".

Karl Bielefeldt
la source
1
Il Futures’agit donc essentiellement d’une manière d’examiner la valeur renvoyée par une fonction, sans attendre de l’attendre. Comme Swift, il est basé sur une valeur de retour, mais contrairement à Swift, la réponse à la valeur de retour peut avoir lieu ultérieurement (un peu comme des exceptions). Droite?
Vers
1
Vous comprenez Futurebien, mais je pense que vous pourriez mal interpréter Swift. Voir la première partie de cette réponse stackoverflow , par exemple.
Karl Bielefeldt
Hmm, je suis nouveau à Swift, alors cette réponse est un peu difficile à analyser. Mais si je ne me trompe pas: cela passe essentiellement par un gestionnaire pouvant être invoqué ultérieurement; droite?
Vers
Oui. Vous créez essentiellement un rappel lorsqu'une erreur se produit.
Karl Bielefeldt
Eitherserait un meilleur exemple IMHO
Paweł Prażak 25/02/2016
19

Les exceptions peuvent rendre le code plus difficile à raisonner. Bien qu'ils ne soient pas aussi puissants que les gotos, ils peuvent causer nombre des mêmes problèmes en raison de leur nature non locale. Par exemple, disons que vous avez un morceau de code impératif comme ceci:

cleanMug();
brewCoffee();
pourCoffee();
drinkCoffee();

Vous ne pouvez pas dire immédiatement si l'une de ces procédures peut générer une exception. Vous devez lire la documentation de chacune de ces procédures pour comprendre cela. (Certaines langues rendent cela légèrement plus facile en augmentant la signature de type avec ces informations.) Le code ci-dessus se compilera parfaitement, que l'une des procédures jette ou non, ce qui rend vraiment facile d'oublier de gérer une exception.

De plus, même si l’intention est de propager l’exception à l’appelant, il est souvent nécessaire d’ajouter du code supplémentaire pour éviter que les éléments restent incohérents (par exemple, si votre cafetière tombe en panne, vous devez tout de même nettoyer le gâchis et le renvoyer. la tasse!). Ainsi, dans de nombreux cas, le code qui utilise des exceptions semble aussi complexe que le code qui ne l’était pas en raison du nettoyage supplémentaire requis.

Les exceptions peuvent être émulées avec un système de types suffisamment puissant. La plupart des langages qui évitent les exceptions utilisent des valeurs de retour pour obtenir le même comportement. Cela ressemble à la façon de faire en C, mais les systèmes de type modernes le rendent généralement plus élégant et plus difficile à oublier pour gérer la condition d'erreur. Ils peuvent également fournir du sucre syntaxique pour rendre les choses moins lourdes, parfois presque aussi propres qu’elles le seraient avec des exceptions.

En particulier, en intégrant la gestion des erreurs dans le système de types plutôt que de la mettre en œuvre séparément, les "exceptions" peuvent être utilisées pour d’autres tâches qui ne sont même pas liées aux erreurs. (Il est bien connu que la gestion des exceptions est en réalité une propriété des monades.)

Rufflewind
la source
Est-il exact que ce type de système de type utilisé par Swift, y compris les options, est le type de "système de type puissant" qui permet d'atteindre cet objectif?
Vers
2
Oui, les options et plus généralement les types de somme (appelés "enum" dans Swift / Rust) peuvent atteindre cet objectif. Cependant, leur utilisation est agréable: il faut y parvenir avec Swift avec la syntaxe de chaînage facultative; en Haskell, cela est réalisé avec la notation monadique do.
Rufflewind
Un "système de types suffisamment puissant" peut-il donner une trace de pile, sinon c'est plutôt inutile
Paweł Prażak
1
+1 pour signaler que les exceptions obscurcissent le flux de contrôle. Tout comme une note auxilary: Il va de soi que les exceptions ne sont pas en fait plus mal que goto: Le gotoest limité à une seule fonction, ce qui est une gamme assez petite fonction fournie sont vraiment petits, l'exception agit plus comme une croix gotoet come from(voir en.wikipedia.org/wiki/INTERCAL ;-)). Il peut très bien connecter deux morceaux de code, en sautant éventuellement du code pour certaines fonctions tierces. La seule chose qu’il ne peut pas faire, qui gotopeut, est de revenir en arrière.
cmaster
2
@ PawełPrażak Lorsque vous travaillez avec de nombreuses fonctions d'ordre supérieur, les traces de pile ne sont pas aussi précieuses. De fortes garanties sur les entrées et les sorties et la prévention des effets secondaires empêchent cette indirection de générer des bogues déroutants.
Jack
11

Il y a d'excellentes réponses ici, mais je pense qu'une raison importante n'a pas été suffisamment soulignée: lorsque des exceptions se produisent, les objets peuvent être laissés dans des états non valides. Si vous pouvez "attraper" une exception, votre code de gestionnaire d'exceptions pourra accéder à ces objets non valides et les utiliser. Cela ira terriblement mal à moins que le code de ces objets soit écrit parfaitement, ce qui est très, très difficile à faire.

Par exemple, imaginez la mise en œuvre de Vector. Si quelqu'un instancie votre vecteur avec un ensemble d'objets, mais qu'une exception se produise lors de l'initialisation (par exemple, lors de la copie de vos objets dans la mémoire nouvellement allouée), il est très difficile de coder correctement l'implémentation de vecteur de manière à la mémoire est une fuite. Ce court article de Stroustroup couvre l'exemple de Vector .

Et ce n'est que la pointe de l'iceberg. Et si, par exemple, vous aviez copié certains des éléments, mais pas tous les éléments? Pour implémenter correctement un conteneur tel que Vector, vous devez presque rendre chaque action réversible, de sorte que l'opération est entièrement atomique (comme une transaction de base de données). C'est compliqué et la plupart des applications se trompent. Et même lorsque cela est fait correctement, cela complique grandement le processus de mise en œuvre du conteneur.

Donc, certaines langues modernes ont décidé que cela n'en valait pas la peine. Rust, par exemple, a des exceptions, mais elles ne peuvent pas être "interceptées". Il est donc impossible pour le code d'interagir avec des objets dans un état non valide.

Fleurs de charlie
la source
1
le but de la capture est de rendre un objet cohérent (ou de mettre fin à quelque chose si ce n'est pas possible) après une erreur.
JoulinRouge
@JoulinRouge je sais. Mais certaines langues ont décidé de ne pas vous donner cette opportunité, mais de bloquer tout le processus. Ces concepteurs de langage connaissent le type de nettoyage que vous aimeriez faire mais ont conclu que c’était trop délicat à vous donner, et que les compromis que cela impliquerait en valaient la peine. Je me rends compte que vous n'êtes peut-être pas d'accord avec leur choix ... mais il est important de savoir qu'ils ont fait le choix consciemment pour ces raisons particulières.
Charlie Flowers
6

Une des choses qui m'a le plus surpris au début, c'est que le langage Rust ne prend pas en charge les exceptions de capture. Vous pouvez générer des exceptions, mais seul le moteur d'exécution peut les intercepter lorsqu'une tâche (pensez à un thread, mais pas toujours à un thread séparé du système d'exploitation) meurt; si vous démarrez une tâche vous-même, vous pouvez alors demander si elle se termine normalement ou si elle se fail!()termine.

En tant que tel, il n’est pas idiomatique de le faire failtrès souvent. Les rares cas où cela se produit sont, par exemple, dans le harnais de test (qui ne sait pas à quoi ressemble le code utilisateur), en tant que niveau supérieur d'un compilateur (la plupart des compilateurs divisent à la place), ou lors de l'appel d'un rappel sur l'entrée de l'utilisateur.

Au lieu de cela, le langage courant consiste à utiliser le Resultmodèle pour ignorer explicitement les erreurs à traiter. Ceci est considérablement facilité par la try!macro , qui peut être enroulée autour de toute expression générant un résultat et le bras réussi s'il en existe un, ou renvoyant autrement de la fonction.

use std::io::IoResult;
use std::io::File;

fn load_file(name: &Path) -> IoResult<String>
{
    let mut file = try!(File::open(name));
    let s = try!(file.read_to_string());
    return Ok(s);
}

fn main()
{
    print!("{}", load_file(&Path::new("/tmp/hello")).unwrap());
}
o11c
la source
1
Donc, est-il juste de dire que cela aussi (comme l'approche de Go) est similaire à Swift, qui l'a fait assert, mais non catch?
Vers
1
Dans Swift, essayez! signifie: Oui, je sais que cela pourrait échouer, mais je suis sûr que non, alors je ne le gère pas, et si cela échoue, mon code est erroné, veuillez donc planter dans ce cas.
gnasher729
6

À mon avis, les exceptions sont un outil essentiel pour détecter les erreurs de code au moment de l'exécution. Tant dans les tests et dans la production. Faites en sorte que leurs messages soient suffisamment détaillés pour que, combiné à une trace de pile, vous puissiez savoir ce qui s’est passé dans un journal.

Les exceptions constituent principalement un outil de développement et un moyen d'obtenir des rapports d'erreur raisonnables de la production dans des cas inattendus.

Mis à part la séparation des problèmes (chemin heureux avec les erreurs attendues seulement, jusqu'à atteindre un gestionnaire générique d'erreurs inattendues), il est intéressant de rendre votre code plus lisible et maintenable, il est en fait impossible de préparer votre code pour tout ce qui est possible. cas inattendus, même en le gonflant avec un code de traitement des erreurs pour compléter l’illisibilité.

C'est en fait le sens du mot "inattendu".

Au fait, ce qui est attendu et ce qui ne l'est pas est une décision qui ne peut être prise que sur le site de l'appel. C'est pourquoi les exceptions vérifiées dans Java n'ont pas fonctionné - la décision est prise au moment de développer une API, quand il est difficile de savoir ce qui est attendu ou inattendu.

Exemple simple: l'API d'une carte de hachage peut avoir deux méthodes:

Value get(Key)

et

Option<Value> getOption(key)

le premier levant une exception s'il n'est pas trouvé, le dernier vous donnant une valeur optionnelle. Dans certains cas, ce dernier a plus de sens, mais dans d’autres, votre code doit absolument s’attendre à ce qu’il y ait une valeur pour une clé donnée. Par conséquent, s’il n’en existe pas, il s’agit d’une erreur que ce code ne peut corriger l'hypothèse a échoué. Dans ce cas, le comportement souhaité est de sortir du chemin du code et de passer à un gestionnaire générique en cas d'échec de l'appel.

Code ne doit jamais essayer de traiter des hypothèses de base qui ont échoué.

Sauf en les vérifiant et en jetant des exceptions bien lisibles, bien sûr.

Lancer des exceptions n'est pas mauvais, mais les attraper peut l'être. N'essayez pas de réparer des erreurs inattendues. Attrapez les exceptions dans quelques endroits où vous souhaitez continuer une boucle ou une opération, enregistrez-les, signalez peut-être une erreur inconnue, et c'est tout.

Les blocs de prise partout sont une très mauvaise idée.

Concevez vos API de manière à ce qu'il soit facile d'exprimer votre intention, c'est-à-dire d'indiquer si vous attendez un cas particulier, comme une clé non trouvée ou non. Les utilisateurs de votre API peuvent alors choisir l'appel de lancement uniquement pour des cas vraiment inattendus.

J'imagine que les mauvaises expériences sont la raison pour laquelle les gens refusent les exceptions et vont trop loin en omettant cet outil essentiel d'automatisation de la gestion des erreurs et de la séparation des préoccupations des nouvelles langues.

Cela, et certains malentendus sur ce qu’ils sont réellement bons pour.

Les simuler en faisant TOUT avec la liaison monadique rend votre code moins lisible et moins maintenable, et vous vous retrouvez sans trace de pile, ce qui rend cette approche bien pire.

La gestion des erreurs de style fonctionnel est idéale pour les cas d'erreur prévus.

Laissez la gestion des exceptions s'occuper automatiquement de tout le reste, c'est pour ça :)

yeoman
la source
3

Swift utilise ici les mêmes principes que Objective-C, mais plus en conséquence. En Objective-C, les exceptions indiquent des erreurs de programmation. Ils ne sont pas gérés sauf par les outils de signalement des incidents. La "gestion des exceptions" se fait en corrigeant le code. (Il y a quelques très mauvaises exceptions. Par exemple, dans les communications entre processus. Mais c'est assez rare et beaucoup de gens ne le rencontrent jamais. Et Objective-C a effectivement try / catch / finally / throw, mais vous les utilisez rarement). Swift vient d'éliminer la possibilité d'attraper des exceptions.

Swift a une fonctionnalité qui ressemble à la gestion des exceptions mais est simplement appliquée à la gestion des erreurs. Historiquement, Objective-C avait un modèle de traitement des erreurs assez répandu: une méthode retournait un BOOL (YES en cas de succès) ou une référence d'objet (nil en cas d'échec, pas de succès) et avait un paramètre "pointeur vers NSError *" qui serait utilisé pour stocker une référence NSError. Swift convertit automatiquement les appels à une telle méthode en quelque chose qui ressemble à la gestion des exceptions.

En général, les fonctions Swift peuvent facilement renvoyer des alternatives, comme un résultat si une fonction fonctionnait correctement et une erreur en cas d'échec. cela facilite beaucoup le traitement des erreurs. Mais la réponse à la question de départ: les concepteurs de Swift pensaient évidemment que créer un langage sûr et écrire du code réussi dans un tel langage était plus facile si le langage ne contenait pas d'exceptions.

gnasher729
la source
Pour Swift, c'est la bonne réponse, IMO. Swift doit rester compatible avec les frameworks de système Objective-C existants, de sorte qu'ils ne disposent pas d'exceptions traditionnelles. J'ai écrit un article il y a quelque temps sur le fonctionnement d'ObjC pour la gestion des erreurs: orangejuiceliberationfront.com/…
uliwitness
2
 int result;
 if((result = operation_that_can_throw_ioerror()) == IOError)
 {
  handle_the_exception_somehow();
 }
 else
 {
   # we don't want to catch the IOError if it's raised
   result = another_operation_that_can_throw_ioerror();
 }
 result |= something_we_always_need_to_do();
 return result;

En C, vous vous retrouvez avec quelque chose comme ce qui précède.

Y a-t-il des choses que je ne peux pas faire à Swift que je pourrais faire avec des exceptions?

Non, il n'y a rien. Vous finissez juste par gérer les codes de résultats au lieu d'exceptions.
Les exceptions vous permettent de réorganiser votre code afin que la gestion des erreurs soit distincte de votre code de chemin d'accès, mais c'est à peu près tout.

pierre précieuse
la source
Et, de même, ces appels pour ...throw_ioerror()renvoyer des erreurs plutôt que de lancer des exceptions?
Vers
1
@raxacoricofallapatorius Si nous affirmons que les exceptions n'existent pas, je suppose que le programme suit le schéma habituel de renvoi des codes d'erreur en cas d'échec et de 0 en cas de succès.
Stonemetal
1
@stonemetal Certaines langues, comme Rust et Haskell, utilisent le système de types pour renvoyer quelque chose de plus significatif qu'un code d'erreur, sans ajouter de points de sortie masqués comme le font les exceptions. Une fonction Rust, par exemple, peut renvoyer une Result<T, E>énumération, qui peut être un type Ok<T>ou un type Err<E>, avec Tle type souhaité, le cas échéant, et Eun type représentant une erreur. La correspondance des modèles et certaines méthodes particulières simplifient le traitement des succès et des échecs. En bref, ne présumez pas que l'absence d'exceptions signifie automatiquement des codes d'erreur.
8bittree
1

En plus de la réponse de Charlie:

Ces exemples de traitement des exceptions déclarées que vous voyez dans de nombreux manuels et livres ne sont très intelligents que sur de très petits exemples.

Même si vous mettez de côté l'argument concernant l'état d'objet invalide, ils provoquent toujours une douleur vraiment énorme lorsqu'il s'agit d'une application volumineuse.

Par exemple, lorsque vous devez traiter des opérations d'E / S, en utilisant une cryptographie, vous pouvez avoir 20 types d'exceptions qui peuvent être rejetées sur 50 méthodes. Imaginez la quantité de code de gestion des exceptions dont vous aurez besoin. La gestion des exceptions prendra plusieurs fois plus de code que le code lui-même.

En réalité, vous savez que des exceptions ne peuvent pas apparaître et que vous n'avez jamais besoin d'écrire autant de tâches de gestion des exceptions. Vous devez donc utiliser certaines solutions pour ignorer les exceptions déclarées. Dans ma pratique, il ne faut gérer que 5% environ des exceptions déclarées pour avoir une application fiable.

Konmik
la source
En réalité, ces exceptions peuvent souvent être traitées à un seul endroit. Par exemple, dans une fonction de "mise à jour des données de téléchargement" si le SSL échoue ou si le DNS est insoluble ou si le serveur Web renvoie un 404, cela n'a pas d'importance, récupérez-le en haut et signalez l'erreur à l'utilisateur.
Zan Lynx