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és Maybe
qui ne sont pas interchangeables avec / directement convertibles en normales, types nullable. Pour utiliser une valeur rendue nullable en l'enveloppant dans un Maybe
, 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:
Pouvons-nous toujours savoir qu'une référence non Nullable n'est jamais considérée comme invalide?
Oui. Int
et Maybe Int
sont deux types complètement séparés. Trouver Nothing
dans une plaine Int
serait comparable à trouver la chaîne "poisson" dans un fichier Int32
.
Qu'en est-il dans le constructeur d'un objet avec un champ de type référence non nullable?
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.
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?
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:
data Maybe a = Just a | Nothing
Pour ceux qui ne connaissent pas Haskell, lisez ceci comme suit: "A Maybe
est soit un Nothing
soit un Just 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 # est class Maybe<a>{}
.
Just
est un constructeur de valeur : c'est une fonction qui prend un argument de type a
et retourne une valeur de type Maybe a
qui contient la valeur. Donc, le code x = Just 17
est analogue à int? x = 17;
.
Nothing
est un autre constructeur de valeur, mais il ne prend aucun argument et le résultat Maybe
renvoyé n'a pas de valeur autre que "Nothing". x = Nothing
est analogue à int? x = null;
(en supposant que nous ayons limité notre a
présence en Haskell Int
, ce qui peut être fait en écrivant x = 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:
ReferenceType x;
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:
do thing 1
add thing 2 to thing 3
do thing 4
if thing 5:
do thing 6
return thing 7
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:
<optional explicit type signature>
functionName arg1 arg2 ... argn = body-expression
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 un Double
? Utilisez la fromIntegral
fonction). Le seul cas possible où une valeur non valide se produit au moment de l'exécution est d'utiliser Prelude.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):
data NotSoBroken = NotSoBroken {foo :: Foo, bar :: Bar }
( foo
et bar
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 un Foo
et a Bar
(qui ne sont pas nullables) et en faire un NotSoBroken
. 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 le NotSoBroken
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.
makeNotSoBroken :: Foo -> Bar -> Maybe NotSoBroken
makeNotSoBroken foo bar = Nothing
(La première ligne est une déclaration de signature de type: makeNotSoBroken
prend un Foo
et a Bar
comme arguments et produit un Maybe NotSoBroken
).
Le type de retour doit être Maybe NotSoBroken
et pas simplement NotSoBroken
parce que nous lui avons dit d'évaluer Nothing
, qui est un constructeur de valeur pour Maybe
. 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 un NotSoBroken
comme argument:
useNotSoBroken :: NotSoBroken -> Whatever
( useNotSoBroken
accepte a NotSoBroken
comme argument et produit a Whatever
).
Et utilisez-le comme ceci:
useNotSoBroken (makeNotSoBroken)
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 un Maybe NotSoBroken
, mais useNotSoBroken
attend un NotSoBroken
. 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 la Maybe
valeur (à l'aide d'une fonctionnalité appelée correspondance de modèle ):
case makeNotSoBroken of
Nothing -> --handle situation here
(Just x) -> useNotSoBroken x
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:
- Tout d'abord,
makeNotSoBroken
est évalué, ce qui garantit de produire une valeur de type Maybe NotSoBroken
.
- L'
case
instruction inspecte la structure de cette valeur.
- Si la valeur est
Nothing
, le code "gérer la situation ici" est évalué.
- Si la valeur correspond à une
Just
valeur, l'autre branche est exécutée. Notez que la clause correspondante identifie simultanément la valeur en tant que Just
construction et lie son NotSoBroken
champ interne à un nom (dans ce cas, x
). x
peut alors être utilisé comme la NotSoBroken
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.