Eric Lippert a fait un point très intéressant dans sa discussion sur les raisons pour lesquelles C # utilise un type null
plutôt qu'un Maybe<T>
type :
La cohérence du système de types est importante. Pouvons-nous toujours savoir qu'une référence non Nullable n'est jamais considérée comme invalide? Qu'en est-il dans le constructeur d'un objet avec un champ de type référence non nullable? Qu'en est-il dans le finaliseur d'un tel objet, où l'objet est finalisé parce que le code qui était censé remplir la référence renvoyait une exception? Un système de type qui vous ment sur ses garanties est dangereux.
C'était un peu révélateur. Les concepts en jeu m'intéressent et j'ai un peu joué avec les compilateurs et les systèmes de typage, mais je n'ai jamais pensé à ce scénario. Comment les langues ayant le type Maybe au lieu d'un caractère null, telles que l'initialisation et la récupération après erreur, dans lesquelles une référence non nulle supposée garantie n'est pas, en fait, dans un état valide?
la source
Type?
(peut-être) etType
(non nul)Maybe T
, cela ne doit pas l'êtreNone
et vous ne pouvez donc pas initialiser son stockage sur le pointeur null.Réponses:
Cette citation pointe vers un problème qui se produit si la déclaration et l'affectation d'identificateurs (ici: membres d'instance) sont séparées l'une de l'autre. En tant qu’esquisse rapide de pseudocode:
Le scénario est maintenant que lors de la construction d'une instance, une erreur sera générée, de sorte que la construction sera abandonnée avant que l'instance ne soit entièrement construite. Ce langage offre une méthode de destruction qui s'exécutera avant la libération de la mémoire, par exemple pour libérer manuellement des ressources non-mémoire. Il doit également être exécuté sur des objets partiellement construits, car des ressources gérées manuellement peuvent déjà avoir été allouées avant l'abandon de la construction.
Avec des valeurs NULL, le destructeur peut tester si une variable a été assignée comme
if (foo != null) foo.cleanup()
. Sans null, l'objet est maintenant dans un état indéfini - quelle est la valeur debar
?Cependant, ce problème existe en raison de la combinaison de trois aspects:
null
ou l'initialisation garantie pour les variables membres.let
instruction telle qu’elle est vue dans les langages fonctionnels) est un moyen facile de forcer l’initialisation garantie - mais limite le langage de différentes manières.Il est facile de choisir une autre conception qui ne présente pas ces problèmes, par exemple en combinant toujours déclaration avec affectation et en faisant en sorte que la langue offre plusieurs blocs de finaliseur au lieu d'une seule méthode de finalisation:
Il n'y a donc pas de problème avec l'absence de null, mais avec la combinaison d'un ensemble d'autres fonctionnalités avec une absence de null.
La question intéressante est maintenant de savoir pourquoi C # a choisi un design mais pas l’autre. Ici, le contexte de la citation énumère de nombreux autres arguments en faveur d'un null dans le langage C #, qui peuvent généralement être résumés comme suit: «familiarité et compatibilité» - et ce sont de bonnes raisons.
la source
null
s: l'ordre de finalisation n'est pas garanti en raison de la possibilité de cycles de référence. Mais je suppose que votreFINALIZE
conception résout également ceci: si ellefoo
a déjà été finalisée, saFINALIZE
section ne fonctionnera tout simplement pas.De la même manière, vous garantissez que toutes les autres données sont dans un état valide.
On peut structurer la sémantique et le flux de contrôle de sorte que vous ne pouvez pas avoir une variable / un champ de quelque type que ce soit sans créer entièrement une valeur pour celle-ci. Au lieu de créer un objet et de laisser un constructeur affecter des valeurs "initiales" à ses champs, vous ne pouvez créer un objet qu'en spécifiant des valeurs pour tous ses champs à la fois. Au lieu de déclarer une variable, puis d'attribuer une valeur initiale, vous pouvez uniquement introduire une variable avec une initialisation.
Par exemple, dans Rust, vous créez un objet de type struct via
Point { x: 1, y: 2 }
au lieu d'écrire un constructeur qui le faitself.x = 1; self.y = 2;
. Bien sûr, cela peut entrer en conflit avec le style de langage que vous avez en tête.Une autre approche complémentaire consiste à utiliser l'analyse de la vivacité pour empêcher l'accès au stockage avant son initialisation. Cela permet de déclarer une variable sans l’initialiser immédiatement, à condition qu’elle soit affectée de manière prouvable avant la première lecture. Il peut également intercepter des cas d’échec,
Techniquement, vous pouvez également définir une initialisation arbitraire par défaut pour les objets, par exemple, mettre à zéro tous les champs numériques, créer des tableaux vides pour les champs de tableau, etc.
la source
Voici comment Haskell le fait: (ce n’est pas vraiment un contrepoint aux déclarations de Lippert puisque Haskell n’est pas un langage orienté objet).
AVERTISSEMENT: une longue réponse d’un fanboy sérieux de Haskell.
TL; DR
Cet exemple montre à quel point Haskell est différent de C #. Au lieu de déléguer la logistique de la construction d'une structure à un constructeur, celle-ci doit être gérée dans le code environnant. Il n’existe aucun moyen pour une valeur nulle (ou
Nothing
en Haskell) de s'afficher là où nous attendons une valeur non nulle car les valeurs nulles ne peuvent apparaître que dans des types d'encapsuleurs spéciaux appelésMaybe
qui ne sont pas interchangeables avec / directement convertibles en normales, types nullable. Pour utiliser une valeur rendue nullable en l'enveloppant dans unMaybe
, nous devons d'abord extraire la valeur à l'aide d'une correspondance de modèle, ce qui nous oblige à dévier le flux de contrôle dans une branche où nous savons avec certitude que nous avons une valeur non nulle.Donc:
Oui.
Int
etMaybe Int
sont deux types complètement séparés. TrouverNothing
dans une plaineInt
serait comparable à trouver la chaîne "poisson" dans un fichierInt32
.Ce n’est pas un problème: les constructeurs de valeurs de Haskell ne peuvent rien faire à part prendre les valeurs qui leur sont données et les assembler. Toute la logique d'initialisation a lieu avant l'appel du constructeur.
Il n'y a pas de finalistes à Haskell, donc je ne peux pas vraiment répondre à cela. Ma première réponse est cependant toujours valable.
Réponse complète :
Haskell n'a pas de valeur null et utilise le
Maybe
type de données pour représenter les valeurs nullables. Peut-être qu'un type de données algabrique est défini comme suit:Pour ceux qui ne connaissent pas Haskell, lisez ceci comme suit: "A
Maybe
est soit unNothing
soit unJust a
". Plus précisément:Maybe
est le constructeur de type : il peut être considéré (à tort) comme une classe générique (oùa
est la variable de type). L'analogie avec C # estclass Maybe<a>{}
.Just
est un constructeur de valeur : c'est une fonction qui prend un argument de typea
et retourne une valeur de typeMaybe a
qui contient la valeur. Donc, le codex = Just 17
est analogue àint? x = 17;
.Nothing
est un autre constructeur de valeur, mais il ne prend aucun argument et le résultatMaybe
renvoyé n'a pas de valeur autre que "Nothing".x = Nothing
est analogue àint? x = null;
(en supposant que nous ayons limité notrea
présence en HaskellInt
, ce qui peut être fait en écrivantx = Nothing :: Maybe Int
).Maintenant que les bases de ce
Maybe
type sont finies, comment Haskell peut-il éviter les problèmes abordés dans la question du PO?Eh bien, Haskell est vraiment différent de la plupart des langues discutées jusqu'à présent, je vais donc commencer par expliquer quelques principes linguistiques de base.
Tout d'abord, à Haskell, tout est immuable . Tout. Les noms font référence à des valeurs et non à des emplacements de mémoire où des valeurs peuvent être stockées (cela constitue à lui seul une énorme source d’élimination des bogues). Contrairement à C #, où la déclaration des variables et l' affectation sont deux opérations distinctes, des valeurs Haskell sont créés en définissant leur valeur (par exemple
x = 15
,y = "quux"
,z = Nothing
), qui ne peut jamais changer. Par conséquent, code comme:N'est pas possible à Haskell. Il n'y a aucun problème avec l'initialisation des valeurs
null
car tout doit être explicitement initialisé à une valeur pour que celle-ci existe.Deuxièmement, Haskell n’est pas un langage orienté objet : c’est un langage purement fonctionnel , il n’existe donc aucun objet au sens strict du terme. Au lieu de cela, il existe simplement des fonctions (constructeurs de valeurs) qui prennent leurs arguments et renvoient une structure fusionnée.
Ensuite, il n'y a absolument aucun code de style impératif. Par cela, je veux dire que la plupart des langues suivent un schéma semblable à celui-ci:
Le comportement du programme est exprimé sous forme d'une série d'instructions. Dans les langages orientés objet, les déclarations de classe et de fonction jouent également un rôle important dans le déroulement du programme, mais l'essentiel est que la "viande" de l'exécution d'un programme se présente sous la forme d'une série d'instructions à exécuter.
En Haskell, ce n'est pas possible. Au lieu de cela, le déroulement du programme est entièrement dicté par les fonctions de chaînage. Même la
do
référence impériale est simplement un sucre syntaxique pour transmettre des fonctions anonymes à l'>>=
opérateur. Toutes les fonctions prennent la forme de:Où
body-expression
peut être n'importe quoi qui évalue à une valeur. Évidemment, il y a plus de fonctionnalités de syntaxe disponibles, mais le point principal est l'absence complète de séquences d'instructions.Enfin, et probablement le plus important, le système de types de Haskell est incroyablement strict. Si je devais résumer la philosophie de conception centrale du système de typage de Haskell, je dirais: "Faites en sorte que tout ce qui est possible se passe mal à la compilation pour que le moins possible se passe mal à l'exécution." Il n'y a aucune conversion implicite que ce soit (vous voulez promouvoir un
Int
en unDouble
? Utilisez lafromIntegral
fonction). Le seul cas possible où une valeur non valide se produit au moment de l'exécution est d'utiliserPrelude.undefined
(ce qui doit simplement être présent et est impossible à supprimer ).Gardant tout cela à l’esprit, examinons l’exemple "cassé" d’Amon et essayons de ré-exprimer ce code en Haskell. Tout d’abord, la déclaration de données (en utilisant la syntaxe d’enregistrement pour les champs nommés):
(
foo
etbar
sont vraiment des fonctions d’accès aux champs anonymes ici au lieu des champs réels, mais nous pouvons ignorer ce détail).Le
NotSoBroken
constructeur de valeur est incapable de prendre des mesures autres que prendre unFoo
et aBar
(qui ne sont pas nullables) et en faire unNotSoBroken
. Il n'y a pas de place pour mettre du code impératif ou même assigner manuellement les champs. Toute la logique d'initialisation doit avoir lieu ailleurs, probablement dans une fonction d'usine dédiée.Dans l'exemple, la construction de
Broken
échoue toujours. Il n'y a aucun moyen de casser leNotSoBroken
constructeur de valeur de la même manière (il n'y a tout simplement rien pour écrire le code), mais nous pouvons créer une fonction fabrique qui est tout aussi défectueuse.(La première ligne est une déclaration de signature de type:
makeNotSoBroken
prend unFoo
et aBar
comme arguments et produit unMaybe NotSoBroken
).Le type de retour doit être
Maybe NotSoBroken
et pas simplementNotSoBroken
parce que nous lui avons dit d'évaluerNothing
, qui est un constructeur de valeur pourMaybe
. Les types ne s'aligneraient tout simplement pas si nous écrivions quelque chose de différent.En plus d'être absolument inutile, cette fonction ne remplit même pas son véritable objectif, comme nous le verrons lorsque nous essayerons de l'utiliser. Créons une fonction appelée
useNotSoBroken
qui attend unNotSoBroken
comme argument:(
useNotSoBroken
accepte aNotSoBroken
comme argument et produit aWhatever
).Et utilisez-le comme ceci:
Dans la plupart des langages, ce type de comportement peut provoquer une exception de pointeur nul. En Haskell, les types ne correspondent pas:
makeNotSoBroken
renvoie unMaybe NotSoBroken
, maisuseNotSoBroken
attend unNotSoBroken
. Ces types ne sont pas interchangeables et la compilation du code échoue.Pour contourner ce problème, nous pouvons utiliser une
case
instruction pour créer une branche basée sur la structure de laMaybe
valeur (à l'aide d'une fonctionnalité appelée correspondance de modèle ):Il est évident que cet extrait doit être placé dans un contexte pour être compilé, mais il montre les bases de la manière dont Haskell gère les éléments null. Voici une explication pas à pas du code ci-dessus:
makeNotSoBroken
est évalué, ce qui garantit de produire une valeur de typeMaybe NotSoBroken
.case
instruction inspecte la structure de cette valeur.Nothing
, le code "gérer la situation ici" est évalué.Just
valeur, l'autre branche est exécutée. Notez que la clause correspondante identifie simultanément la valeur en tant queJust
construction et lie sonNotSoBroken
champ interne à un nom (dans ce cas,x
).x
peut alors être utilisé comme laNotSoBroken
valeur normale qui est.Ainsi, la mise en correspondance de modèles fournit une installation puissante pour appliquer la sécurité de type, car la structure de l'objet est indissociable de la ramification du contrôle.
J'espère que c'était une explication compréhensible. Si cela n’a aucun sens, lancez-vous dans Learn You A Haskell For Great Good! , l'un des meilleurs tutoriels linguistiques en ligne que j'ai jamais lu. J'espère que vous verrez la même beauté dans cette langue que moi.
la source
Je pense que votre citation est un argument d'homme de paille.
Les langues modernes actuelles (y compris le C #) vous garantissent que le constructeur soit complet ou non.
S'il existe une exception dans le constructeur et que l'objet est laissé partiellement non initialisé, le fait d'avoir
null
ouMaybe::none
pour un état non initialisé ne fait aucune différence réelle dans le code du destructeur.De toute façon, vous devrez simplement vous en occuper. Lorsqu'il y a des ressources externes à gérer, vous devez les gérer explicitement de toute façon. Les langues et les bibliothèques peuvent aider, mais vous devrez y réfléchir.
Btw: En C #, la
null
valeur est à peu près équivalente àMaybe::none
. Vous ne pouvez affecternull
que des variables et des membres d'objet déclarés nullables au niveau du type :Ce n'est en aucun cas différent de l'extrait suivant:
Donc, en conclusion, je ne vois pas en quoi l'annulation est opposée aux
Maybe
types. Je dirais même que C # s'est faufilé dans son propreMaybe
type et l'a appeléNullable<T>
.Avec les méthodes d'extension, il est même facile d'obtenir le nettoyage du Nullable pour suivre le modèle monadique:
la source
null
tant que membre implicite de chaque type etMaybe<T>
c'est que, avecMaybe<T>
, vous pouvez également avoir justeT
, qui n'a pas de valeur par défaut.null
n'est pas un membre implicite de tous les types. Pournull
que lebal soit une valeur, vous devez définir explicitement le type à nullable, ce qui rend unT?
(sucre de syntaxe pourNullable<T>
) essentiellement équivalent àMaybe<T>
.C ++ le fait en ayant accès à l'initialiseur qui se produit avant le corps du constructeur. C # exécute l'initialiseur par défaut avant le corps du constructeur, il attribue approximativement 0 à tout,
floats
devient 0.0,bools
devient false, les références deviennent nulles, etc. En C ++, vous pouvez le faire exécuter avec un initialiseur différent afin de garantir qu'un type de référence non null ne soit jamais nul .la source
null
, et le seul moyen d’indiquer l’absence de valeur est d’utiliser unMaybe
type (également appeléOption
) que AFAIK C ++ n’a pas dans la liste. bibliothèque standard. L'absence de null nous permet de garantir qu'un champ sera toujours valide en tant que propriété du système de types . C'est une garantie plus forte que de s'assurer manuellement qu'aucun chemin de code n'existe là où une variable pourrait encore se trouvernull
.