web-dev-qa-db-fra.com

Y a-t-il une raison d'avoir un type bas dans un langage de programmation?

Un type inférieur est une construction apparaissant principalement dans la théorie des types mathématiques. Il est également appelé type vide. C'est un type qui n'a pas de valeurs, mais c'est un sous-type de tous les types.

Si le type de retour d'une fonction est le type inférieur, cela signifie qu'elle ne retourne pas. Période. Peut-être qu'il boucle pour toujours, ou peut-être qu'il lève une exception.

Quel est l'intérêt d'avoir ce type bizarre dans un langage de programmation? Ce n'est pas si courant, mais il est présent dans certains, tels que Scala et LISP.

50
GregRos

Je prendrai un exemple simple: C++ vs Rust.

Voici une fonction utilisée pour lever une exception en C++ 11:

[[noreturn]] void ThrowException(char const* message,
                                 char const* file,
                                 int line,
                                 char const* function);

Et voici l'équivalent dans Rust:

fn formatted_panic(message: &str, file: &str, line: isize, function: &str) -> !;

Sur un plan purement syntaxique, la construction Rust est plus sensible. Notez que la construction C++ spécifie un type de retour même si elle spécifie également qu'elle ne va pas retourner. C'est un peu bizarre.

Sur une note standard, la syntaxe C++ n'apparaissait qu'avec C++ 11 (elle était collée en haut), mais divers compilateurs fournissaient diverses extensions depuis un certain temps, de sorte que des outils d'analyse tiers devaient être programmés pour reconnaître les différentes façons cet attribut pourrait être écrit. Le standardiser est évidemment nettement supérieur.


Maintenant, quant à l'avantage?

Le fait qu'une fonction ne retourne pas peut être utile pour:

  • optimisation: on peut tailler n'importe quel code après (il ne reviendra pas), il n'est pas nécessaire de sauvegarder les registres (car il ne sera pas nécessaire de les restaurer), ...
  • analyse statique: elle élimine un certain nombre de chemins d'exécution potentiels
  • maintenabilité: (voir analyse statique, mais par l'homme)
33
Matthieu M.

La réponse de Karl est bonne. Voici une utilisation supplémentaire que je ne pense pas que quiconque ait mentionnée. Le type de

if E then A else B

doit être un type qui inclut toutes les valeurs du type A et toutes les valeurs du type B. Si le type de B est Nothing, alors le type de l'expression if peut être le type de A. Je vais souvent déclarer une routine

def unreachable( s:String ) : Nothing = throw new AssertionError("Unreachable "+s) 

pour dire que le code ne devrait pas être atteint. Comme son type est Nothing, unreachable(s) peut désormais être utilisé dans n'importe quel if ou (plus souvent) switch sans affecter le type de résultat. Par exemple

 val colour : Colour := switch state of
         BLACK_TO_MOVE: BLACK
         WHITE_TO_MOVE: WHITE
         default: unreachable("Bad state")

Scala a un tel type Nothing.

Un autre cas d'utilisation pour Nothing (comme mentionné dans la réponse de Karl) est List [Nothing] est le type de listes dont chacun des membres a le type Nothing. Il peut donc s'agir du type de la liste vide.

La propriété clé de Nothing qui fait fonctionner ces cas d'utilisation est pas qu'elle n'a pas de valeurs - bien que dans Scala, par exemple, elle n'ait pas de valeurs - c'est qu'elle est un sous-type de tous les autres types.

Supposons que vous ayez une langue dans laquelle chaque type contient la même valeur - appelons-la (). Dans une telle langue, le type d'unité, qui a () Comme seule valeur, pourrait être un sous-type de chaque type. Cela n'en fait pas un type inférieur dans le sens où le PO signifiait; l'OP était clair qu'un type inférieur ne contient aucune valeur. Cependant, comme il s'agit d'un type qui est un sous-type de chaque type, il peut jouer sensiblement le même rôle qu'un type inférieur.

Haskell fait les choses un peu différemment. Dans Haskell, une expression qui ne produit jamais de valeur peut avoir le schéma de types forall a.a. Une instance de ce schéma de types s'unifiera avec n'importe quel autre type, donc il agit effectivement comme un type inférieur, même si (standard) Haskell n'a aucune notion de sous-typage. Par exemple, la fonction error du prélude standard a un schéma de type forall a. [Char] -> a. Vous pouvez donc écrire

if E then A else error ""

et le type de l'expression sera le même que le type de A, pour toute expression A.

La liste vide dans Haskell a le schéma de type forall a. [a]. Si A est une expression dont le type est un type liste, alors

if E then A else []

est une expression du même type que A.

27
Theodore Norvell

Les types forment un monoïde de deux manières, faisant ensemble un semiring . C'est ce qu'on appelle types de données algébriques . Pour les types finis, ce semi-cercle est directement lié au semi-nombre de nombres naturels (y compris zéro), ce qui signifie que vous comptez le nombre de valeurs possibles du type (à l'exclusion des "valeurs non terminales").

  • Le type du bas (je l'appellerai Vacuous) a zéro valeur.
  • Le type d'unité a une valeur. J'appellerai à la fois le type et sa valeur unique ().
  • La composition (que la plupart des langages de programmation supportent assez directement, via des enregistrements/structures/classes avec des champs publics) est une opération de produit . Par exemple, (Bool, Bool) A quatre valeurs possibles, à savoir (False,False), (False,True), (True,False) Et (True,True).
    Le type d'unité est l'élément d'identité de l'opération de composition. Par exemple. ((), False) Et ((), True) Sont les seules valeurs de type ((), Bool), Donc ce type est isomorphe à Bool lui-même.
  • Les types alternatifs sont quelque peu négligés dans la plupart des langues (les langues OO les supportent avec héritage), mais ils ne sont pas moins utiles. Une alternative entre deux types A et B a fondamentalement toutes les valeurs de A, plus toutes les valeurs de B, donc type de somme . Par exemple, Either () Bool a trois valeurs, je les appellerai Left (), Right False Et Right True.
    Le type inférieur est l'élément d'identité de la somme: Either Vacuous A A seulement les valeurs de la forme Right a, car Left ... n'a pas de sens (Vacuous n'a pas de valeur).

Ce qui est intéressant avec ces monoïdes, c'est que lorsque vous introduisez les fonctions dans votre langue, les catégorie de ces types avec les fonctions comme le morphisme est un catégorie monoïdale . Entre autres choses, cela vous permet de définir des foncteurs applicatifs et monades , qui se révèlent être une excellente abstraction pour les calculs généraux (impliquant éventuellement des effets secondaires, etc.) dans des termes autrement purement fonctionnels.

Maintenant, en fait, vous pouvez aller assez loin en vous souciant d'un seul côté du problème (le monoïde de la composition), alors vous n'avez pas vraiment besoin du type de fond explicitement. Par exemple, même Haskell n'a pas eu pendant longtemps de type de fond standard. Maintenant, il s'appelle Void .

Mais quand vous considérez l'image complète, comme un catégorie fermée bicartésienne , alors le système de type est en fait équivalent à tout le calcul lambda, donc en gros vous avez l'abstraction parfaite sur tout ce qui est possible dans un langage Turing-complet . Idéal pour les langages embarqués spécifiques au domaine, par exemple, il y a un projet de codage direct des circuits électroniques de cette façon .

Bien sûr, vous pouvez bien dire que ce sont tous les théoriciens non-sens général . Vous n'avez pas du tout besoin de connaître la théorie des catégories pour être un bon programmeur, mais lorsque vous le faites, cela vous donne des moyens puissants et ridiculement généraux pour raisonner sur le code et prouver les invariants.


mb21 me rappelle de noter que cela ne doit pas être confondu avec les valeurs inférieures . Dans les langages paresseux comme Haskell, chaque type contient une "valeur" inférieure, notée . Ce n'est pas une chose concrète que vous pourriez jamais faire passer explicitement, c'est plutôt ce qui est "retourné" par exemple quand une fonction boucle pour toujours. Même le type Void de Haskell "contient" la valeur inférieure, donc le nom. Dans cette optique, le type inférieur de Haskell a vraiment une valeur et son type unitaire a deux valeurs, mais dans la discussion sur la théorie des catégories, cela est généralement ignoré.

19
leftaroundabout

Peut-être qu'il boucle pour toujours, ou peut-être qu'il lève une exception.

Cela ressemble à un type utile à avoir dans ces situations, aussi rares soient-ils.

De plus, même si Nothing (nom Scala pour le type inférieur) ne peut avoir aucune valeur, List[Nothing] n'a pas cette restriction, ce qui la rend utile comme type de liste vide. La plupart des langues contournent cela en faisant une liste vide de chaînes un type différent d'une liste vide d'entiers, ce qui a du sens, mais rend une liste vide plus verbeuse à écrire, ce qui est un gros inconvénient dans un langage orienté liste.

18
Karl Bielefeldt

Il est utile pour l'analyse statique de documenter le fait qu'un chemin de code particulier n'est pas accessible. Par exemple, si vous écrivez ce qui suit en C #:

int F(int arg) {
 if (arg != 0)
  return arg + 1; //some computation
 else
  Assert(false); //this throws but the compiler does not know that
}
void Assert(bool cond) { if (!cond) throw ...; }

Le compilateur se plaindra que F ne renvoie rien dans au moins un chemin de code. Si Assert devait être marqué comme non retourné, le compilateur n'aurait pas besoin d'avertir.

3
usr

Dans certaines langues, null a le type le plus bas, car le sous-type de tous les types définit bien les langues utilisées pour null (malgré la légère contradiction d'avoir null à la fois lui-même et une fonction qui se renvoie , en évitant les arguments courants expliquant pourquoi bot doit être inhabité).

Il peut également être utilisé comme fourre-tout dans les types de fonctions (any -> bot) pour gérer la répartition qui a mal tourné.

Et certaines langues vous permettent de résoudre bot en tant qu'erreur, ce qui peut être utilisé pour fournir des erreurs de compilation personnalisées.

2
Telastyn

Dans certains langages, vous pouvez annoter une fonction pour indiquer au compilateur et aux développeurs qu'un appel à cette fonction ne va pas retourner (et si la fonction est écrite de manière à pouvoir être renvoyée, le compilateur ne l'autorisera pas ). C'est une chose utile à savoir, mais à la fin, vous pouvez appeler une fonction comme celle-ci comme n'importe quelle autre. Le compilateur peut utiliser les informations pour l'optimisation, pour donner des avertissements sur le code mort, etc. Il n'y a donc pas de raison très convaincante d'avoir ce type, mais pas de raison très impérieuse de l'éviter non plus.

Dans de nombreuses langues, une fonction peut retourner "void". Ce que cela signifie exactement dépend de la langue. En C, cela signifie que la fonction ne renvoie rien. Dans Swift, cela signifie que la fonction renvoie un objet avec une seule valeur possible, et puisqu'il n'y a qu'une seule valeur possible, cette valeur prend zéro bit et ne nécessite en fait aucun code. Dans les deux cas, ce n'est pas la même chose que "bas".

"bottom" serait un type sans valeur possible. Cela ne peut jamais exister. Si une fonction renvoie "bottom", elle ne peut pas réellement retourner, car il n'y a aucune valeur de type "bottom" qu'elle pourrait renvoyer.

Si un concepteur de langage en a envie, il n'y a aucune raison de ne pas avoir ce type. L'implémentation n'est pas difficile (vous pouvez l'implémenter exactement comme une fonction renvoyant void et marquée comme "ne revient pas"). Vous ne pouvez pas mélanger des pointeurs vers des fonctions renvoyant bottom avec des pointeurs vers des fonctions renvoyant void, car ils ne sont pas du même type).

1
gnasher729

Oui, c'est un type assez utile; alors que son rôle serait principalement intérieur au système de types, il y a des occasions où le type inférieur apparaît ouvertement.

Considérons un langage typé statiquement dans lequel les conditionnelles sont des expressions (donc la construction if-then-else se double de opérateur ternaire de C et amis, et il pourrait y avoir une déclaration de cas multi-voies similaire). Le langage de programmation fonctionnel a cela, mais cela arrive aussi dans certains langages impératifs (depuis ALGOL 60). Ensuite, toutes les expressions de branche doivent finalement produire le type de l'expression conditionnelle entière. On pourrait simplement exiger que leurs types soient égaux (et je pense que c'est le cas pour l'opérateur ternaire en C) mais cela est trop restrictif, surtout lorsque le conditionnel peut également être utilisé comme instruction conditionnelle (ne retournant aucune valeur utile). En général, on veut que chaque expression de branche soit (implicitement) convertible en un type commun qui sera le type de l'expression complète (éventuellement avec plus ou moins restrictions compliquées pour permettre à ce type commun d'être trouvé efficacement par le complice, cf. C++, mais je n'entrerai pas dans ces détails ici).

Il existe deux types de situations où un type général de conversion permettra la flexibilité nécessaire de ces expressions conditionnelles. L'un est déjà mentionné, où le type de résultat est le type d'unité void; c'est naturellement un super-type de tous les autres types, et permettre à n'importe quel type d'être converti (trivialement) en lui permet d'utiliser l'expression conditionnelle comme instruction conditionnelle. L'autre implique des cas où l'expression renvoie une valeur utile, mais une ou plusieurs branches sont incapables d'en produire une. Ils lèveront généralement une exception ou impliqueront un saut, et les obliger à produire (également) une valeur du type de l'expression entière (à partir d'un point inaccessible) serait inutile. C'est ce genre de situation qui peut être géré avec élégance en donnant des clauses de déclenchement d'exception, des sauts et des appels qui auront un tel effet, le type inférieur, le type qui peut être (trivialement) converti en n'importe quel autre type.

Je suggérerais d'écrire un type de fond comme * pour suggérer sa convertibilité en type arbitraire. Il peut servir à d'autres fins utiles en interne, par exemple lorsque vous essayez de déduire un type de résultat pour une fonction récursive qui n'en déclare aucun, l'inférenceur de type peut affecter le type * à tout appel récursif pour éviter une situation de poulet et d'oeufs; le type réel sera déterminé par des branches non récursives et celles récursives seront converties en type commun des branches non récursives. S'il n'y a pas de branches non récursives, le type restera *, et indique correctement que la fonction n'a aucun moyen possible de revenir de la récursivité. Autre que cela et comme type de résultat des fonctions de levée d'exceptions, on peut utiliser * comme type de composant de séquences de longueur 0, par exemple de la liste vide; à nouveau si jamais un élément est sélectionné dans une expression de type [*] (liste nécessairement vide), puis le type résultant * indiquera correctement que cela ne peut jamais revenir sans erreur.

1
Marc van Leeuwen