Article de reference

Problème de semi-prédicat

En programmation informatique , un problème de semi-prédicat se produit lorsqu'une sous-routine destinée à renvoyer une valeur utile peut échouer, mais la signalisation de l'éch...

En programmation informatique , un problème de semi-prédicat se produit lorsqu'une sous-routine destinée à renvoyer une valeur utile peut échouer, mais la signalisation de l'échec utilise une valeur de retour par ailleurs valide . Le problème est que l'appelant de la sous-routine ne peut pas dire ce que signifie le résultat dans ce cas.

Exemple

L' opération de division produit un nombre réel , mais échoue lorsque le diviseur est égal à zéro . Si nous devions écrire une fonction qui effectue une division, nous pourrions choisir de renvoyer 0 sur cette entrée non valide. Cependant, si le dividende est égal à 0, le résultat est également égal à 0. Cela signifie qu'il n'existe aucun nombre que nous puissions renvoyer pour signaler de manière unique une tentative de division par zéro, puisque tous les nombres réels sont compris dans la plage de division.

Implications pratiques

Les premiers programmeurs traitaient des cas potentiellement exceptionnels comme la division en utilisant une convention exigeant que la routine appelante vérifie les entrées avant d'appeler la fonction de division. Cela posait deux problèmes : d'abord, cela encombrait considérablement tout le code qui effectuait la division (une opération très courante) ; ensuite, cela violait les principes Ne vous répétez pas et d'encapsulation , le premier suggérant d'éliminer le code dupliqué, et le second suggérant que le code associé aux données soit contenu à un seul endroit (dans cet exemple de division, la vérification des entrées a été effectuée séparément). Pour un calcul plus compliqué que la division, il pouvait être difficile pour l'appelant de reconnaître une entrée non valide ; dans certains cas, déterminer la validité de l'entrée peut être aussi coûteux que d'effectuer l'intégralité du calcul. La fonction cible pouvait également être modifiée et s'attendrait alors à des conditions préalables différentes de celles de l'appelant ; une telle modification nécessiterait des changements à chaque endroit où la fonction était appelée.

Solutions

Le problème du semi-prédicat n’est pas universel parmi les fonctions qui peuvent échouer.

Utilisation d'une convention personnalisée pour interpréter les valeurs de retour

Si la plage d'une fonction ne couvre pas l'intégralité de l' espace correspondant au type de données de la valeur de retour de la fonction, une valeur connue pour être impossible dans le cadre d'un calcul normal peut être utilisée. Par exemple, considérons la fonction index, qui prend une chaîne et une sous-chaîne, et renvoie l' index entier de la sous-chaîne dans la chaîne principale. Si la recherche échoue, la fonction peut être programmée pour renvoyer −1 (ou toute autre valeur négative), car cela ne peut jamais signifier un résultat positif.

Cette solution présente cependant des problèmes, car elle surcharge la signification naturelle d’une fonction avec une convention arbitraire :

  • Le programmeur doit mémoriser des valeurs d'échec spécifiques pour de nombreuses fonctions, qui bien sûr ne peuvent pas être identiques si les fonctions ont des plages différentes.
  • Une implémentation différente de la même fonction peut choisir d'utiliser une valeur d'échec différente, ce qui peut entraîner des bogues lorsque les programmeurs passent d'un environnement à un autre.
  • Si la fonction défaillante souhaite communiquer des informations utiles sur la raison de son échec, une seule valeur d'échec est insuffisante.
  • Un entier signé divise par deux la plage d'index possible pour pouvoir stocker le bit de signe .
  • Bien que la valeur choisie soit un résultat non valide pour cette opération, elle peut être une entrée valide pour les opérations de suivi. Par exemple, en Python, str.findelle renvoie −1 si la sous-chaîne n'est pas trouvée, mais −1 est un index valide (les index négatifs commencent généralement à partir de la fin ).

Rendement à valeurs multiples

De nombreux langages permettent, par un mécanisme ou un autre, à une fonction de renvoyer plusieurs valeurs. Si cela est possible, la fonction peut être repensée pour renvoyer une valeur booléenne signalant la réussite ou l'échec, ainsi que sa valeur de retour principale. Si plusieurs modes d'erreur sont possibles, la fonction peut à la place renvoyer un code de retour énuméré (code d'erreur) ainsi que sa valeur de retour principale.

Différentes techniques permettant de renvoyer plusieurs valeurs incluent :

  • Renvoyer un tuple de valeurs. Ceci est une pratique courante dans les langages (tels que Python ) qui ont un type de données de tuple intégré et une syntaxe spéciale pour les gérer : en Python, x, y = f()appelle la fonction frenvoyant une paire de valeurs et affecte les éléments de la paire à deux variables.
  • Valeurs de retour secondaires comme dans Common Lisp . Toutes les expressions ont une valeur principale, mais des valeurs secondaires peuvent être renvoyées aux appelants intéressés. Par exemple, la GETHASHfonction renvoie la valeur de la clé donnée dans une carte associative , ou une valeur par défaut dans le cas contraire. Cependant, elle renvoie également un booléen secondaire indiquant si la valeur a été trouvée, ce qui permet de faire la distinction entre les cas « aucune valeur n'a été trouvée » et « la valeur trouvée était égale à la valeur par défaut ». Cela diffère du renvoi d'un tuple, dans la mesure où les valeurs de retour secondaires sont facultatives : si un appelant ne s'en soucie pas, il peut les ignorer complètement, alors que les retours à valeur de tuple ne sont que du sucre syntaxique pour renvoyer et décompresser une liste, et chaque appelant doit toujours connaître et consommer tous les éléments renvoyés.
  • Les langages avec appel par référence – ou équivalents, comme l'appel par adresse à l'aide de pointeurs – peuvent permettre un retour multivalué en désignant certains paramètres comme paramètres de sortie . Dans ce cas, la fonction pourrait simplement renvoyer la valeur d'erreur, avec une variable destinée à stocker le résultat réel transmis à la fonction. Cela est analogue à l'utilisation d'un état de sortie pour stocker un code d'erreur et aux flux pour renvoyer du contenu.
  • Une variante des paramètres de sortie est utilisée dans les langages orientés objet qui utilisent l'appel par partage , où un objet mutable est passé à une fonction et l'objet est muté pour renvoyer des valeurs.
  • Les langages de programmation logique tels que Prolog n'ont pas de valeurs de retour. À la place, des variables logiques non liées sont utilisées comme paramètres de sortie, pour être unifiées avec des valeurs construites dans un appel de prédicat.

Variable globale pour le statut de retour

Similairement à un argument « out », une variable globale peut stocker quelle erreur s’est produite (ou simplement si une erreur s’est produite).

Par exemple, si une erreur se produit et est signalée (généralement comme ci-dessus, par une valeur illégale comme −1), la errnovariable Unix est définie pour indiquer quelle valeur s'est produite. L'utilisation d'une variable globale a ses inconvénients habituels : la sécurité des threads devient un problème (les systèmes d'exploitation modernes utilisent une version thread-safe de errno), et si une seule variable globale d'erreur est utilisée, son type doit être suffisamment large pour contenir toutes les informations intéressantes sur toutes les erreurs possibles dans le système.

Exceptions

Les exceptions sont un système largement utilisé pour résoudre ce problème. Une condition d'erreur n'est pas du tout considérée comme une valeur de retour de la fonction ; le flux de contrôle normal est perturbé et la gestion explicite de l'erreur a lieu automatiquement. Elles constituent un exemple de signalisation hors bande .

Extension du type de valeur de retour

Types hybrides créés manuellement

En C , une approche courante, lorsque cela est possible, consiste à utiliser un type de données délibérément plus large que ce qui est strictement nécessaire à la fonction. Par exemple, la fonction standard getchar()est définie avec un type de retour intet renvoie une valeur dans la plage [0, 255] (la plage de unsigned char) en cas de succès ou la valeur EOF( définie par l'implémentation , mais en dehors de la plage de unsigned char) à la fin de l'entrée ou d'une erreur de lecture.

Types de référence nullables

Dans les langages avec des pointeurs ou des références, une solution consiste à renvoyer un pointeur vers une valeur, plutôt que la valeur elle-même. Ce pointeur de retour peut alors être défini sur null pour indiquer une erreur. Il est généralement adapté aux fonctions qui renvoient de toute façon un pointeur. Cela présente un avantage en termes de performances par rapport au style de gestion des exceptions de la programmation orientée objet avec l'inconvénient que les programmeurs négligents peuvent ne pas vérifier la valeur de retour, ce qui entraîne un plantage lorsque le pointeur non valide est utilisé. Qu'un pointeur soit nul ou non est un autre exemple du problème de prédicat ; null peut être un indicateur indiquant un échec ou la valeur d'un pointeur renvoyée avec succès. Un modèle courant dans l' environnement UNIX consiste à définir une variable distincte pour indiquer la cause d'une erreur. La fonction de la bibliothèque standard C en est un exemple fopen().

Types implicitement hybrides

Dans les langages typés dynamiquement , tels que PHP et Lisp , l'approche habituelle consiste à renvoyer false, noneou nulllorsque l'appel de fonction échoue. Cela fonctionne en renvoyant un type différent du type de retour normal (élargissant ainsi le type). Il s'agit d'un équivalent typé dynamiquement du renvoi d'un pointeur null.

Par exemple, une fonction numérique renvoie normalement un nombre (int ou float), et bien que zéro puisse être une réponse valide, false ne l'est pas. De même, une fonction qui renvoie normalement une chaîne peut parfois renvoyer la chaîne vide comme réponse valide, mais renvoyer false en cas d'échec. Ce processus de jonglage de types nécessite de faire attention lors du test de la valeur de retour : par exemple, en PHP, utilisez ===(c'est-à-dire, égal et de même type) plutôt que simplement ==(c'est-à-dire égal, après conversion automatique de type). Cela ne fonctionne que lorsque la fonction d'origine n'est pas censée renvoyer une valeur booléenne, et nécessite toujours que les informations sur l'erreur soient transmises par d'autres moyens.

Types explicitement hybrides

En Haskell et dans d'autres langages de programmation fonctionnelle , il est courant d'utiliser un type de données aussi grand que nécessaire pour exprimer un résultat possible. Par exemple, on peut écrire une fonction de division qui renvoie le type Maybe Real, et une getcharfonction qui renvoie Either String Char. Le premier cas est un type d'option , qui n'a qu'une seule valeur d'échec, Nothing. Le deuxième cas est une union étiquetée : un résultat est soit une chaîne avec un message d'erreur descriptif, soit un caractère lu avec succès. Le système d'inférence de type de Haskell permet de garantir que les appelants traitent les erreurs possibles. Étant donné que les conditions d'erreur deviennent explicites dans le type de fonction, l'examen de sa signature indique immédiatement au programmeur comment traiter les erreurs. De plus, les unions étiquetées et les types d'option forment des monades lorsqu'elles sont dotées de fonctions appropriées : cela peut être utilisé pour garder le code propre en propageant automatiquement les conditions d'erreur non gérées.

Exemple

Rust possède des types de données algébriquesResult<T, E> et est livré avec les types et intégrés Option<T>.

fn 
find ( clé : String ) -> Option < String > { si clé == "bonjour" { Some ( clé ) } else { Aucun } }

Le langage de programmation C++std::optional<T> introduit dans la mise à jour C++17 .

std :: optionnel < int > find_int_in_str ( std :: string_view str ) { constexpr auto digits = "0123456789" ; auto n = str . find_first_of ( digits ); if ( n == std :: string :: npos ) { // La chaîne ne contient simplement aucun nombre, pas nécessairement une erreur return std :: nullopt ; }
int result ; // Plus de logique de recherche qui définit le résultat de retour « résultat » ; }

et std::expected<T, E>dans la mise à jour C++23

classe d'énumération parse_error { kEmptyString , kOutOfRange , kNotANumber };
std :: expected < int , parse_error > parse_number ( std :: string_view str ) { if ( str . empty ()) { // Signaler une situation inattendue parmi plusieurs return std :: unknown ( parse_error :: kEmptyString ); }
int result ; // Plus de logique de conversion qui définit le résultat de retour « result » ; }

Plus d articles de Worldlex Wiki

Revenez a l index pour explorer davantage de pages sur l histoire, la science, la culture, la geographie et la societe en francais.

Explorer l index