Meilleure explication pour les langues sans null

225

De temps en temps, lorsque les programmeurs se plaignent d'erreurs / exceptions nulles, quelqu'un demande ce que nous faisons sans null.

J'ai une idée de base de la fraîcheur des types d'options, mais je n'ai pas les connaissances ou les compétences linguistiques pour les exprimer au mieux. Quelle est une grande explication de ce qui suit écrit d'une manière accessible au programmeur moyen que nous pourrions diriger cette personne vers?

  • L'opportunité de faire en sorte que les références / pointeurs soient annulables par défaut
  • Fonctionnement des types d'options, y compris des stratégies pour faciliter la vérification des cas nuls tels que
    • correspondance de motifs et
    • compréhensions monadiques
  • Solution alternative telle que message mangeant zéro
  • (autres aspects que j'ai ratés)
Roman A. Taycher
la source
11
Si vous ajoutez des balises à cette question pour la programmation fonctionnelle ou F #, vous obtiendrez certainement des réponses fantastiques.
Stephen Swensen
J'ai ajouté une balise de programmation fonctionnelle car le type d'option venait du monde ml. Je préfère ne pas le marquer F # (trop spécifique). BTW quelqu'un avec des pouvoirs de taxonomie doit ajouter des balises de type peut-être ou de type option.
Roman A. Taycher
4
il y a peu de besoin de telles balises spécifiques, je suppose. Les balises sont principalement destinées à permettre aux utilisateurs de trouver des questions pertinentes (par exemple, "des questions que je connais beaucoup et auxquelles je pourrai répondre", et la "programmation fonctionnelle" y est très utile. Mais quelque chose comme "null" ou " type-option "sont beaucoup moins utiles. Peu de gens sont susceptibles de surveiller une balise" type-option "à la recherche de questions auxquelles ils peuvent répondre.;)
jalf
N'oublions pas que l'une des principales raisons de null est que les ordinateurs ont évolué fortement liés à la théorie des ensembles. Null est l'un des ensembles les plus importants de toute la théorie des ensembles. Sans cela, des algorithmes entiers tomberaient en panne. Par exemple, effectuez un tri par fusion. Cela implique de casser une liste en deux fois plusieurs fois. Et si la liste contient 7 articles? Vous le divisez d'abord en 4 et 3. Puis 2, 2, 2 et 1. Puis 1, 1, 1, 1, 1, 1, 1 et .... null! Null a un but, juste un que vous ne voyez pas pratiquement. Il existe plus pour le domaine théorique.
stevendesu du
6
@steven_desu - Je ne suis pas d'accord. Dans les langues «nullables», vous pouvez avoir une référence à une liste vide [], ainsi qu'une référence de liste nulle. Cette question concerne la confusion entre les deux.
stusmith

Réponses:

433

Je pense que le résumé succinct des raisons pour lesquelles la nullité est indésirable est que les États dénués de sens ne devraient pas être représentables .

Supposons que je modélise une porte. Il peut être dans l'un des trois états: ouvert, fermé mais déverrouillé et fermé et verrouillé. Maintenant, je pourrais le modéliser selon les

class Door
    private bool isShut
    private bool isLocked

et il est clair comment mapper mes trois états dans ces deux variables booléennes. Mais cela laisse une quatrième état indésirable disponible: isShut==false && isLocked==true. Étant donné que les types que j'ai sélectionnés comme représentation admettent cet état, je dois déployer des efforts mentaux pour m'assurer que la classe n'entre jamais dans cet état (peut-être en codant explicitement un invariant). En revanche, si j'utilisais un langage avec des types de données algébriques ou des énumérations vérifiées qui me permettaient de définir

type DoorState =
    | Open | ShutAndUnlocked | ShutAndLocked

alors je pourrais définir

class Door
    private DoorState state

et il n'y a plus de soucis. Le système de types s'assurera qu'il n'y a que trois états possibles pour une instance de class Door. C'est ce à quoi les systèmes de types sont bons - excluant explicitement toute une classe d'erreurs au moment de la compilation.

Le problème avec nullest que chaque type de référence obtient cet état supplémentaire dans son espace qui est généralement indésirable. Une stringvariable pourrait être n'importe quelle séquence de caractères, ou ce pourrait être cette nullvaleur supplémentaire folle qui ne correspond pas à mon domaine problématique. Un Triangleobjet a trois Points, qui ont eux-mêmes Xet des Yvaleurs, mais malheureusement le Points ou le Trianglelui-même peuvent être cette valeur nulle folle qui n'a pas de sens pour le domaine graphique dans lequel je travaille. Etc.

Lorsque vous avez l'intention de modéliser une valeur possiblement inexistante, vous devez y adhérer explicitement. Si la façon dont j'ai l'intention de modéliser les gens est que chacun Persona un FirstNameet un LastName, mais seulement certaines personnes ont un MiddleNames, alors je voudrais dire quelque chose comme

class Person
    private string FirstName
    private Option<string> MiddleName
    private string LastName

stringici est supposé être un type non nul. Ensuite, il n'y a pas d'invariants délicats à établir et pas de NullReferenceExceptions inattendus lorsque vous essayez de calculer la longueur du nom d'une personne. Le système de type garantit que tout code traitant des MiddleNamecomptes en rend compte None, tandis que tout code traitant des FirstNamepeut supposer en toute sécurité qu'il y a une valeur.

Ainsi, par exemple, en utilisant le type ci-dessus, nous pourrions créer cette fonction idiote:

let TotalNumCharsInPersonsName(p:Person) =
    let middleLen = match p.MiddleName with
                    | None -> 0
                    | Some(s) -> s.Length
    p.FirstName.Length + middleLen + p.LastName.Length

sans soucis. En revanche, dans une langue avec des références nullables pour des types comme chaîne, puis en supposant

class Person
    private string FirstName
    private string MiddleName
    private string LastName

vous finissez par créer des trucs comme

let TotalNumCharsInPersonsName(p:Person) =
    p.FirstName.Length + p.MiddleName.Length + p.LastName.Length

qui explose si l'objet Personne entrant n'a pas l'invariant de tout étant non nul, ou

let TotalNumCharsInPersonsName(p:Person) =
    (if p.FirstName=null then 0 else p.FirstName.Length)
    + (if p.MiddleName=null then 0 else p.MiddleName.Length)
    + (if p.LastName=null then 0 else p.LastName.Length)

ou peut-être

let TotalNumCharsInPersonsName(p:Person) =
    p.FirstName.Length
    + (if p.MiddleName=null then 0 else p.MiddleName.Length)
    + p.LastName.Length

en supposant que le ppremier / dernier est là mais que le milieu peut être nul, ou peut-être que vous faites des vérifications qui lèvent différents types d'exceptions, ou qui sait quoi. Tous ces choix d'implémentation fous et toutes les choses à penser surgissent parce qu'il y a cette stupide valeur représentable dont vous ne voulez pas ou dont vous n'avez pas besoin.

Null ajoute généralement une complexité inutile. La complexité est l'ennemi de tous les logiciels, et vous devez vous efforcer de réduire la complexité chaque fois que cela est raisonnable.

(Notez bien que même ces exemples simples sont plus complexes. Même si a FirstNamene peut pas l'être null, a stringpeut représenter ""(la chaîne vide), qui n'est probablement pas non plus un nom de personne que nous avons l'intention de modéliser. En tant que tel, même avec des non chaînes nullables, il se peut que nous représentions des valeurs sans signification. Encore une fois, vous pouvez choisir de combattre cela soit via des invariants et du code conditionnel lors de l'exécution, soit en utilisant le système de type (par exemple pour avoir un NonEmptyStringtype). ce dernier est peut-être mal avisé (les "bons" types sont souvent "fermés" sur un ensemble d'opérations courantes, et par exemple NonEmptyStringne sont pas fermés sur.SubString(0,0)), mais cela démontre plus de points dans l'espace de conception. En fin de compte, dans tout système de type donné, il y a une certaine complexité dont il sera très bon de se débarrasser, et une autre complexité qui est intrinsèquement plus difficile à éliminer. La clé de cette rubrique est que dans presque tous les systèmes de types, le passage de "références annulables par défaut" à "références non nullables par défaut" est presque toujours un simple changement qui rend le système de type beaucoup plus efficace pour lutter contre la complexité et excluant certains types d'erreurs et d'états dénués de sens. Il est donc assez fou que tant de langues continuent de répéter cette erreur encore et encore.)

Brian
la source
31
Re: noms - En effet. Et peut-être que vous vous souciez de modéliser une porte qui s'ouvre, mais avec le pêne dormant de la serrure qui sort, empêchant la porte de se fermer. Il y a beaucoup de complexité dans le monde. La clé n'est pas d'ajouter plus de complexité lors de la mise en œuvre de la correspondance entre les «états mondiaux» et les «états de programme» dans votre logiciel.
Brian
59
Quoi, vous n'avez jamais verrouillé les portes ouvertes?
Joshua
58
Je ne comprends pas pourquoi les gens s'énervent sur la sémantique d'un domaine particulier. Brian a représenté les défauts avec null de manière concise et simple, oui, il a simplifié le domaine du problème dans son exemple en disant que tout le monde a des prénoms et des noms. La question a été répondue à un «T», Brian - si vous êtes jamais à Boston, je vous dois une bière pour tout le poste que vous faites ici!
akaphenom
67
@akaphenom: merci, mais notez que tout le monde ne boit pas de bière (je ne suis pas buveur). Mais j'apprécie que vous n'utilisiez qu'un modèle simplifié du monde afin de communiquer votre gratitude, donc je ne vais pas ergoter davantage sur les hypothèses erronées de votre modèle mondial. : P (Tant de complexité dans le monde réel! :))
Brian
4
Étrangement, il y a 3 portes d'État dans ce monde! Ils sont utilisés dans certains hôtels comme portes de toilettes. Un bouton-poussoir agit comme une clé de l'intérieur, qui verrouille la porte de l'extérieur. Il se déverrouille automatiquement dès que le pêne se déplace.
comonad
65

La bonne chose à propos des types d'options n'est pas qu'ils sont facultatifs. C'est que tous les autres types ne le sont pas .

Parfois , nous devons être capables de représenter une sorte d'état "nul". Parfois, nous devons représenter une option "sans valeur" ainsi que les autres valeurs possibles qu'une variable peut prendre. Donc, un langage qui interdit catégoriquement cela va être un peu paralysé.

Mais souvent , nous n'en avons pas besoin, et autoriser un tel état "nul" ne fait que créer de l'ambiguïté et de la confusion: chaque fois que j'accède à une variable de type référence dans .NET, je dois considérer qu'elle peut être nulle .

Souvent, il ne sera jamais réellement nul, car le programmeur structure le code de sorte qu'il ne puisse jamais se produire. Mais le compilateur ne peut pas vérifier cela, et chaque fois que vous le voyez, vous devez vous demander "est-ce que cela peut être nul? Dois-je vérifier null ici?"

Idéalement, dans les nombreux cas où null n'a pas de sens, cela ne devrait pas être autorisé .

C'est difficile à réaliser dans .NET, où presque tout peut être nul. Vous devez vous fier à l'auteur du code que vous appelez pour être à 100% discipliné et cohérent et avoir clairement documenté ce qui peut et ne peut pas être nul, ou vous devez être paranoïaque et tout vérifier .

Cependant, si les types ne peuvent pas être annulés par défaut , vous n'avez pas besoin de vérifier s'ils sont nuls ou non. Vous savez qu'ils ne peuvent jamais être nuls, car le vérificateur de compilateur / type applique cela pour vous.

Et puis nous avons juste besoin d' une porte arrière pour les rares cas où nous faisons besoin de gérer un état nul. Ensuite, un type "option" peut être utilisé. Ensuite, nous autorisons null dans les cas où nous avons pris une décision consciente que nous devons être en mesure de représenter le cas «sans valeur», et dans tous les autres cas, nous savons que la valeur ne sera jamais nulle.

Comme d'autres l'ont mentionné, en C # ou Java par exemple, null peut signifier l'une des deux choses suivantes:

  1. la variable n'est pas initialisée. Idéalement, cela ne devrait jamais se produire. Une variable ne doit exister que si elle est initialisée.
  2. la variable contient des données "facultatives": elle doit pouvoir représenter le cas où il n'y a pas de données . C'est parfois nécessaire. Peut-être que vous essayez de trouver un objet dans une liste, et vous ne savez pas à l'avance si oui ou non il est là. Ensuite, nous devons être capables de représenter qu '"aucun objet n'a été trouvé".

Le deuxième sens doit être préservé, mais le premier doit être entièrement éliminé. Et même le deuxième sens ne devrait pas être celui par défaut. C'est quelque chose que nous pouvons choisir si et quand nous en avons besoin . Mais lorsque nous n'avons pas besoin que quelque chose soit facultatif, nous voulons que le vérificateur de type garantisse qu'il ne sera jamais nul.

jalf
la source
Et dans le deuxième sens, nous voulons que le compilateur nous avertisse (s'arrête?) Si nous essayons d'accéder à ces variables sans vérifier d'abord la nullité. Voici un excellent article sur la prochaine fonctionnalité C # nulle / non nulle (enfin!) Blogs.msdn.microsoft.com/dotnet/2017/11/15/…
Ohad Schneider
44

Jusqu'à présent, toutes les réponses se concentrent sur les raisons pour lesquelles nullc'est une mauvaise chose et sur la façon dont c'est un peu pratique si une langue peut garantir que certaines valeurs ne seront jamais nulles.

Ils suggèrent ensuite que ce serait une idée assez intéressante si vous imposez la non-nullité pour toutes les valeurs, ce qui peut être fait si vous ajoutez un concept comme Optionou Maybepour représenter des types qui n'ont pas toujours une valeur définie. Telle est l'approche adoptée par Haskell.

Ce sont de bonnes choses! Mais cela n'empêche pas l'utilisation de types explicitement annulables / non null pour obtenir le même effet. Pourquoi, alors, Option est-elle toujours une bonne chose? Après tout, Scala prend en charge les valeurs nullables (est doit , donc il peut travailler avec les bibliothèques Java) mais prend en charge Optionsaussi bien.

Q. Quels sont donc les avantages au-delà de la possibilité de supprimer entièrement les valeurs nulles d'une langue?

A. Composition

Si vous effectuez une traduction naïve à partir d'un code sans valeur

def fullNameLength(p:Person) = {
  val middleLen =
    if (null == p.middleName)
      p.middleName.length
    else
      0
  p.firstName.length + middleLen + p.lastName.length
}

au code compatible avec les options

def fullNameLength(p:Person) = {
  val middleLen = p.middleName match {
    case Some(x) => x.length
    case _ => 0
  }
  p.firstName.length + middleLen + p.lastName.length
}

il n'y a pas beaucoup de différence! Mais c'est aussi une façon terrible d'utiliser Options ... Cette approche est beaucoup plus propre:

def fullNameLength(p:Person) = {
  val middleLen = p.middleName map {_.length} getOrElse 0
  p.firstName.length + middleLen + p.lastName.length
}

Ou même:

def fullNameLength(p:Person) =       
  p.firstName.length +
  p.middleName.map{length}.getOrElse(0) +
  p.lastName.length

Lorsque vous commencez à traiter la liste des options, c'est encore mieux. Imaginez que la liste peoplesoit elle-même facultative:

people flatMap(_ find (_.firstName == "joe")) map (fullNameLength)

Comment cela marche-t-il?

//convert an Option[List[Person]] to an Option[S]
//where the function f takes a List[Person] and returns an S
people map f

//find a person named "Joe" in a List[Person].
//returns Some[Person], or None if "Joe" isn't in the list
validPeopleList find (_.firstName == "joe")

//returns None if people is None
//Some(None) if people is valid but doesn't contain Joe
//Some[Some[Person]] if Joe is found
people map (_ find (_.firstName == "joe")) 

//flatten it to return None if people is None or Joe isn't found
//Some[Person] if Joe is found
people flatMap (_ find (_.firstName == "joe")) 

//return Some(length) if the list isn't None and Joe is found
//otherwise return None
people flatMap (_ find (_.firstName == "joe")) map (fullNameLength)

Le code correspondant avec des contrôles nuls (ou même des opérateurs elvis?:) Serait douloureusement long. La véritable astuce ici est l'opération flatMap, qui permet la compréhension imbriquée des options et des collections d'une manière que les valeurs nullables ne peuvent jamais atteindre.

Kevin Wright
la source
8
+1, c'est un bon point à souligner. Un addendum: plus de Haskell-land, flatMapserait appelé (>>=), c'est-à-dire l'opérateur "bind" pour les monades. C'est vrai, les Haskeller aiment flatMaptellement les choses que nous les mettons dans le logo de notre langue.
CA McCann
1
+1 Espérons qu'une expression de Option<T>ne sera jamais, jamais nulle. Malheureusement, Scala est euh, toujours lié à Java :-) (D'un autre côté, si Scala ne jouait pas bien avec Java, qui l'utiliserait? Oo)
Assez facile à faire: 'List (null) .headOption'. Notez que cela signifie une chose très différente d'une valeur de retour de «Aucun»
Kevin Wright
4
Je vous ai donné de la prime car j'aime vraiment ce que vous avez dit sur la composition, que les autres n'ont pas semblé mentionner.
Roman A. Taycher
Excellente réponse avec de bons exemples!
thSoft
38

Puisque les gens semblent manquer: nullc'est ambigu.

La date de naissance d'Alice est null. Qu'est-ce que ça veut dire?

La date de décès de Bob est null. Qu'est-ce que ça veut dire?

Une interprétation "raisonnable" pourrait être que la date de naissance d'Alice existe mais est inconnue, alors que la date de décès de Bob n'existe pas (Bob est toujours en vie). Mais pourquoi en sommes-nous arrivés à des réponses différentes?


Un autre problème: nullc'est un cas de bord.

  • Est-ce null = null?
  • Est-ce nan = nan?
  • Est-ce inf = inf?
  • Est-ce +0 = -0?
  • Est-ce +0/0 = -0/0?

Les réponses sont généralement «oui», «non», «oui», «oui», «non», «oui» respectivement. Les "mathématiciens" fous appellent NaN la "nullité" et disent qu'il se compare à lui-même. SQL traite les valeurs nulles comme n'étant égal à rien (elles se comportent donc comme des NaN). On se demande ce qui se passe lorsque vous essayez de stocker ± ∞, ± 0 et NaN dans la même colonne de base de données (il y a 2 53 NaN, dont la moitié sont "négatifs").

Pour aggraver les choses, les bases de données diffèrent dans la façon dont elles traitent NULL, et la plupart d'entre elles ne sont pas cohérentes (voir Gestion NULL dans SQLite pour un aperçu). C'est assez horrible.


Et maintenant pour l'histoire obligatoire:

J'ai récemment conçu une table de base de données (sqlite3) avec cinq colonnes a NOT NULL, b, id_a, id_b NOT NULL, timestamp. Comme il s'agit d'un schéma générique conçu pour résoudre un problème générique pour des applications assez arbitraires, il existe deux contraintes d'unicité:

UNIQUE(a, b, id_a)
UNIQUE(a, b, id_b)

id_an'existe que pour la compatibilité avec une conception d'application existante (en partie parce que je n'ai pas trouvé de meilleure solution), et n'est pas utilisé dans la nouvelle application. En raison de la façon dont fonctionne NULL dans SQL, je peux insérer (1, 2, NULL, 3, t)et (1, 2, NULL, 4, t)et ne pas violer la première contrainte d' unicité (car (1, 2, NULL) != (1, 2, NULL)).

Cela fonctionne spécifiquement en raison de la façon dont NULL fonctionne dans une contrainte d'unicité sur la plupart des bases de données (probablement pour qu'il soit plus facile de modéliser des situations "réelles", par exemple, deux personnes ne peuvent pas avoir le même numéro de sécurité sociale, mais pas toutes les personnes en ont un).


FWIW, sans invoquer d'abord un comportement non défini, les références C ++ ne peuvent pas "pointer vers" null, et il n'est pas possible de construire une classe avec des variables de membre de référence non initialisées (si une exception est levée, la construction échoue).

Sidenote: Parfois, vous pouvez souhaiter des pointeurs s'excluant mutuellement (c'est-à-dire qu'un seul d'entre eux peut être non NULL), par exemple dans un iOS hypothétique type DialogState = NotShown | ShowingActionSheet UIActionSheet | ShowingAlertView UIAlertView | Dismissed. Au lieu de cela, je suis obligé de faire des trucs comme assert((bool)actionSheet + (bool)alertView == 1).

tc.
la source
Les vrais mathématiciens n'utilisent cependant pas le concept de "NaN", rassurez-vous.
Noldorin
@Noldorin: Oui, mais ils utilisent le terme "forme indéterminée".
IJ Kennedy
@IJKennedy: C'est un autre collège, je le sais très bien merci. Certains «NaN» peuvent représenter une forme indéterminée, mais comme FPA ne fait pas de raisonnement symbolique, l'assimiler à une forme indéterminée est assez trompeur!
Noldorin
Qu'est-ce qui ne va pas assert(actionSheet ^ alertView)? Ou votre langue ne peut-elle pas booster?
cat
16

L'opportunité de disposer de références / pointeurs peut être annulée par défaut.

Je ne pense pas que ce soit le principal problème avec les valeurs nulles, le principal problème avec les valeurs nulles est qu'elles peuvent signifier deux choses:

  1. La référence / le pointeur n'est pas initialisé: le problème ici est le même que la mutabilité en général. D'une part, cela rend plus difficile l'analyse de votre code.
  2. La variable étant nulle signifie en fait quelque chose: c'est le cas que les types d'Option formalisent réellement.

Les langages qui prennent en charge les types d'options interdisent ou découragent également généralement l'utilisation de variables non initialisées.

Fonctionnement des types d'options, y compris des stratégies pour faciliter la vérification des cas nuls tels que la correspondance de modèles.

Pour être efficaces, les types d'options doivent être pris en charge directement dans la langue. Sinon, il faut beaucoup de code de chaudière pour les simuler. L'appariement de modèles et l'inférence de type sont deux fonctionnalités clés du langage qui facilitent l'utilisation des types d'options. Par exemple:

En F #:

//first we create the option list, and then filter out all None Option types and 
//map all Some Option types to their values.  See how type-inference shines.
let optionList = [Some(1); Some(2); None; Some(3); None]
optionList |> List.choose id //evaluates to [1;2;3]

//here is a simple pattern-matching example
//which prints "1;2;None;3;None;".
//notice how value is extracted from op during the match
optionList 
|> List.iter (function Some(value) -> printf "%i;" value | None -> printf "None;")

Cependant, dans un langage comme Java sans prise en charge directe des types d'options, nous aurions quelque chose comme:

//here we perform the same filter/map operation as in the F# example.
List<Option<Integer>> optionList = Arrays.asList(new Some<Integer>(1),new Some<Integer>(2),new None<Integer>(),new Some<Integer>(3),new None<Integer>());
List<Integer> filteredList = new ArrayList<Integer>();
for(Option<Integer> op : list)
    if(op instanceof Some)
        filteredList.add(((Some<Integer>)op).getValue());

Solution alternative telle que message mangeant nul

Le «message mangeant zéro» d'Objective-C n'est pas tant une solution qu'une tentative d'alléger le mal de tête de la vérification nulle. Fondamentalement, au lieu de lever une exception d'exécution lorsque vous essayez d'appeler une méthode sur un objet null, l'expression est évaluée à null elle-même. Suspendre l'incrédulité, c'est comme si chaque méthode d'instance commençait par if (this == null) return null;. Mais il y a ensuite une perte d'informations: vous ne savez pas si la méthode a retourné null parce qu'elle est une valeur de retour valide, ou parce que l'objet est réellement nul. Cela ressemble beaucoup à la déglutition d'exceptions et ne fait aucun progrès pour résoudre les problèmes avec null décrits ci-dessus.

Stephen Swensen
la source
Ceci est une bête noire mais c # n'est guère un langage de type c.
Roman A. Taycher
4
J'allais pour Java ici, car C # aurait probablement une meilleure solution ... mais j'apprécie votre bêtise, ce que les gens veulent vraiment dire "un langage avec une syntaxe inspirée de c". Je suis allé de l'avant et j'ai remplacé la déclaration "en forme de c".
Stephen Swensen
Avec linq, à droite. Je pensais à c # et je ne l'ai pas remarqué.
Roman A. Taycher
1
Oui, avec une syntaxe inspirée de c principalement, mais je pense que j'ai également entendu parler de langages de programmation impératifs comme python / ruby ​​avec très peu de syntaxe de type c appelée c-like par les programmeurs fonctionnels.
Roman A. Taycher
11

L'Assemblée nous a apporté des adresses également connues sous le nom de pointeurs non typés. C les a mappés directement en tant que pointeurs typés mais a introduit le null d'Algol en tant que valeur de pointeur unique, compatible avec tous les pointeurs typés. Le gros problème avec null en C est que puisque chaque pointeur peut être nul, on ne peut jamais utiliser un pointeur en toute sécurité sans vérification manuelle.

Dans les langages de niveau supérieur, avoir null est gênant car il véhicule vraiment deux notions distinctes:

  • Dire que quelque chose n'est pas défini .
  • Dire que quelque chose est facultatif .

Avoir des variables indéfinies est à peu près inutile et donne lieu à un comportement indéfini chaque fois qu'elles se produisent. Je suppose que tout le monde conviendra qu'il faut éviter à tout prix d'avoir des choses non définies.

Le second cas est facultatif et est mieux fourni explicitement, par exemple avec un type d'option .


Disons que nous sommes dans une entreprise de transport et que nous devons créer une application pour aider à créer un horaire pour nos chauffeurs. Pour chaque conducteur, nous stockons quelques informations telles que: les permis de conduire dont il dispose et le numéro de téléphone à appeler en cas d'urgence.

En C on pourrait avoir:

struct PhoneNumber { ... };
struct MotorbikeLicence { ... };
struct CarLicence { ... };
struct TruckLicence { ... };

struct Driver {
  char name[32]; /* Null terminated */
  struct PhoneNumber * emergency_phone_number;
  struct MotorbikeLicence * motorbike_licence;
  struct CarLicence * car_licence;
  struct TruckLicence * truck_licence;
};

Comme vous le constatez, dans tout traitement de notre liste de pilotes, nous devrons rechercher des pointeurs nuls. Le compilateur ne vous aidera pas, la sécurité du programme dépend de vos épaules.

Dans OCaml, le même code ressemblerait à ceci:

type phone_number = { ... }
type motorbike_licence = { ... }
type car_licence = { ... }
type truck_licence = { ... }

type driver = {
  name: string;
  emergency_phone_number: phone_number option;
  motorbike_licence: motorbike_licence option;
  car_licence: car_licence option;
  truck_licence: truck_licence option;
}

Disons maintenant que nous voulons imprimer les noms de tous les conducteurs ainsi que leurs numéros de permis de camion.

En C:

#include <stdio.h>

void print_driver_with_truck_licence_number(struct Driver * driver) {
  /* Check may be redundant but better be safe than sorry */
  if (driver != NULL) {
    printf("driver %s has ", driver->name);
    if (driver->truck_licence != NULL) {
      printf("truck licence %04d-%04d-%08d\n",
        driver->truck_licence->area_code
        driver->truck_licence->year
        driver->truck_licence->num_in_year);
    } else {
      printf("no truck licence\n");
    }
  }
}

void print_drivers_with_truck_licence_numbers(struct Driver ** drivers, int nb) {
  if (drivers != NULL && nb >= 0) {
    int i;
    for (i = 0; i < nb; ++i) {
      struct Driver * driver = drivers[i];
      if (driver) {
        print_driver_with_truck_licence_number(driver);
      } else {
        /* Huh ? We got a null inside the array, meaning it probably got
           corrupt somehow, what do we do ? Ignore ? Assert ? */
      }
    }
  } else {
    /* Caller provided us with erroneous input, what do we do ?
       Ignore ? Assert ? */
  }
}

Dans OCaml, ce serait:

open Printf

(* Here we are guaranteed to have a driver instance *)
let print_driver_with_truck_licence_number driver =
  printf "driver %s has " driver.name;
  match driver.truck_licence with
    | None ->
        printf "no truck licence\n"
    | Some licence ->
        (* Here we are guaranteed to have a licence *)
        printf "truck licence %04d-%04d-%08d\n"
          licence.area_code
          licence.year
          licence.num_in_year

(* Here we are guaranteed to have a valid list of drivers *)
let print_drivers_with_truck_licence_numbers drivers =
  List.iter print_driver_with_truck_licence_number drivers

Comme vous pouvez le voir dans cet exemple trivial, il n'y a rien de compliqué dans la version sûre:

  • C'est terser.
  • Vous obtenez de bien meilleures garanties et aucune vérification nulle n'est requise.
  • Le compilateur s'est assuré que vous avez correctement traité l'option

Alors qu'en C, vous auriez pu oublier un échec et un boom nul ...

Remarque: ces exemples de code n'ont pas été compilés, mais j'espère que vous avez eu les idées.

bltxd
la source
Je ne l'ai jamais essayé mais en.wikipedia.org/wiki/Cyclone_%28programming_language%29 prétend autoriser des pointeurs non nuls pour c.
Roman A. Taycher
1
Je ne suis pas d'accord avec votre affirmation selon laquelle personne ne s'intéresse au premier cas. Beaucoup de gens, en particulier ceux des communautés linguistiques fonctionnelles, sont extrêmement intéressés par cela et découragent ou interdisent complètement l'utilisation de variables non initialisées.
Stephen Swensen
Je crois NULLque dans "référence qui peut ne rien indiquer" a été inventé pour une langue algol (Wikipedia est d'accord, voir en.wikipedia.org/wiki/Null_pointer#Null_pointer ). Mais bien sûr, il est probable que les programmeurs d'assemblage aient initialisé leurs pointeurs à une adresse non valide (lire: Null = 0).
1
@Stephen: Nous voulions probablement dire la même chose. Pour moi, ils découragent ou interdisent l'utilisation de choses non initialisées précisément parce qu'il est inutile de discuter de choses non définies car nous ne pouvons rien faire de sensé ou utile avec elles. Cela n'aurait aucun intérêt.
bltxd
2
comme @tc. dit, null n'a rien à voir avec l'assemblage. Dans l'assemblage, les types ne sont généralement pas annulables. Une valeur chargée dans un registre à usage général peut être zéro ou il peut s'agir d'un entier non nul. Mais il ne peut jamais être nul. Même si vous chargez une adresse mémoire dans un registre, sur les architectures les plus courantes, il n'y a pas de représentation distincte du "pointeur nul". C'est un concept introduit dans les langages de niveau supérieur, comme C.
jalf
5

Microsoft Research a un projet intéressant appelé

Spec #

Il s'agit d'une extension C # avec un type non nul et un mécanisme pour vérifier que vos objets ne sont pas nuls , bien que, à mon humble avis, l'application du principe de conception par contrat puisse être plus appropriée et plus utile pour de nombreuses situations gênantes causées par des références nulles.

Jahan
la source
4

Venant de l'arrière-plan .NET, j'ai toujours pensé que null avait un point, c'est utile. Jusqu'à ce que je connaisse les structures et à quel point il était facile de travailler avec elles en évitant beaucoup de code standard. Tony Hoare s'exprimant à QCon Londres en 2009, s'est excusé d'avoir inventé la référence nulle . Pour le citer:

J'appelle cela mon erreur d'un milliard de dollars. C'était l'invention de la référence nulle en 1965. A cette époque, je concevais le premier système de type complet pour les références dans un langage orienté objet (ALGOL W). Mon objectif était de garantir que toute utilisation des références soit absolument sûre, la vérification étant effectuée automatiquement par le compilateur. Mais je n'ai pas pu résister à la tentation de mettre une référence nulle, tout simplement parce qu'elle était si facile à mettre en œuvre. Cela a conduit à d'innombrables erreurs, vulnérabilités et plantages du système, qui ont probablement causé un milliard de dollars de douleur et de dommages au cours des quarante dernières années. Ces dernières années, un certain nombre d'analyseurs de programmes tels que PREfix et PREfast dans Microsoft ont été utilisés pour vérifier les références et donner des avertissements en cas de risque qu'elles ne soient pas nulles. Des langages de programmation plus récents comme Spec # ont introduit des déclarations pour les références non nulles. Telle est la solution que j'ai rejetée en 1965.

Voir aussi cette question chez les programmeurs

nawfal
la source
1

J'ai toujours considéré Null (ou nil) comme l'absence d'une valeur .

Parfois vous le voulez, parfois non. Cela dépend du domaine avec lequel vous travaillez. Si l'absence est significative: pas de deuxième prénom, votre candidature peut agir en conséquence. D'un autre côté, si la valeur nulle ne doit pas être là: le prénom est nul, alors le développeur reçoit l'appel proverbial à 2 heures du matin.

J'ai également vu du code surchargé et trop compliqué avec des vérifications de null. Pour moi, cela signifie l'une des deux choses suivantes:
a) un bug plus haut dans l'arborescence des applications
b) une conception incorrecte / incomplète

Du côté positif - Null est probablement l'une des notions les plus utiles pour vérifier si quelque chose est absent, et les langues sans le concept de null finiront par compliquer les choses quand il sera temps de faire la validation des données. Dans ce cas, si une nouvelle variable n'est pas initialisée, lesdites langues définiront généralement les variables sur une chaîne vide, 0 ou une collection vide. Cependant, si une chaîne vide ou 0 ou une collection vide sont des valeurs valides pour votre application, vous avez un problème.

Parfois, cela a été contourné en inventant des valeurs spéciales / étranges pour que les champs représentent un état non initialisé. Mais que se passe-t-il alors lorsque la valeur spéciale est entrée par un utilisateur bien intentionné? Et n'entrons pas dans le pétrin que cela fera des routines de validation des données. Si le langage soutenait le concept nul, toutes les préoccupations disparaîtraient.

Jon
la source
Salut @ Jon, c'est un peu difficile de te suivre ici. J'ai finalement réalisé que par des valeurs «spéciales / bizarres», vous entendez probablement quelque chose comme «non défini» de Javascript ou «NaN» de l'IEEE. Mais à part cela, vous ne répondez pas vraiment aux questions posées par le PO. Et l'affirmation selon laquelle "Null est probablement la notion la plus utile pour vérifier si quelque chose est absent" est presque certainement fausse. Les types d'options sont une alternative bien considérée et sécurisée à null.
Stephen Swensen
@Stephen - En regardant en arrière sur mon message, je pense que toute la 2e moitié devrait être déplacée vers une question encore à poser. Mais je dis toujours que null est très utile pour vérifier si quelque chose est absent.
Jon
0

Les langages vectoriels peuvent parfois s'en tirer sans avoir de null.

Le vecteur vide sert de null typé dans ce cas.

Joshua
la source
Je pense que je comprends de quoi vous parlez, mais pourriez-vous citer quelques exemples? Surtout en appliquant plusieurs fonctions à une valeur éventuellement nulle?
Roman A. Taycher
Bien appliquer une transformation vectorielle à un vecteur vide entraîne un autre vecteur vide. Pour info, SQL est principalement un langage vectoriel.
Joshua
1
OK, je ferais mieux de clarifier cela. SQL est un langage vectoriel pour les lignes et un langage de valeurs pour les colonnes.
Joshua