229 votes

Meilleure explication pour les langues sans null

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

J'ai une idée de base de l'intérêt des types d'options, mais je n'ai pas les connaissances ou les compétences linguistiques pour l'exprimer au mieux. Qu'est-ce qu'un grand une explication de ce qui suit écrite d'une manière accessible au programmeur moyen et vers laquelle nous pourrions orienter cette personne ?

  • Il n'est pas souhaitable que les références/pointeurs soient nuls par défaut.
  • Comment fonctionnent les types d'options, y compris les stratégies visant à faciliter la vérification des cas nuls tels que
    • le filtrage par motif et
    • compréhensions monadiques
  • Solution alternative telle que le message mangeant nil
  • (autres aspects que j'ai manqués)

11 votes

Si vous ajoutez des balises à cette question pour la programmation fonctionnelle ou F#, vous obtiendrez certainement des réponses fantastiques.

0 votes

J'ai ajouté l'étiquette de programmation fonctionnelle puisque l'option-type vient du monde de la programmation fonctionnelle. Je préfère ne pas le marquer F#(trop spécifique). BTW quelqu'un avec des pouvoirs de taxonomie doit ajouter une étiquette peut-être-type ou option-type.

4 votes

Il n'y a pas vraiment besoin d'étiquettes aussi spécifiques, je suppose. Les balises sont principalement destinées à permettre aux gens de trouver des questions pertinentes (par exemple, "questions dont je sais beaucoup de choses et auxquelles je pourrai répondre", et "programmation fonctionnelle" est très utile à cet égard. Mais quelque chose comme "null" ou "option-type" est beaucoup moins utile. Peu de gens sont susceptibles de surveiller une balise "option-type" à la recherche de questions auxquelles ils peuvent répondre. ;)

438voto

Brian Points 82719

Je pense que le résumé succinct de la raison pour laquelle null est indésirable est que les états sans signification ne devraient pas être représentables .

Supposons que je modélise une porte. Elle peut être dans l'un des trois états suivants : ouverte, fermée mais non verrouillée, et fermée et verrouillée. Je pourrais la modéliser de la façon suivante

class Door
    private bool isShut
    private bool isLocked

et il est clair comment faire correspondre mes trois états à ces deux variables booléennes. Mais cela laisse un quatrième état, non désiré, disponible : isShut==false && isLocked==true . Comme les types que j'ai choisis comme représentation admettent cet état, je dois faire un effort mental pour m'assurer que la classe ne se retrouve 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 permette 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 garantit qu'il n'y a que trois états possibles pour une instance de class Door pour être dans. C'est à cela que servent les systèmes de types : exclure explicitement toute une classe d'erreurs au moment de la compilation.

Le problème avec null est que chaque type de référence obtient cet état supplémentaire dans son espace qui est généralement indésirable. A string La variable peut être n'importe quelle séquence de caractères, ou peut être cette extra folle null qui ne correspond pas à mon domaine de problème. A Triangle a trois Point qui ont eux-mêmes X y Y mais malheureusement, les Point ou le Triangle peut être une valeur nulle qui n'a aucun sens pour le domaine graphique dans lequel je travaille. Etc.

Si vous avez l'intention de modéliser une valeur qui n'existe peut-être pas, vous devez choisir de le faire explicitement. Si la façon dont j'ai l'intention de modéliser les gens est que tout Person a un FirstName et un LastName mais seules certaines personnes ont MiddleName alors j'aimerais dire quelque chose comme

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

string est supposé être un type non nul. Il n'y a donc pas d'invariants délicats à établir et pas de problèmes inattendus. NullReferenceException lorsqu'on essaie de calculer la longueur du nom d'une personne. Le système de types permet de s'assurer que tout code traitant de l'élément MiddleName rend compte de la possibilité qu'il soit None tandis que tout code traitant de l FirstName peut supposer sans risque qu'il y a là 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 aucune inquiétude. En revanche, dans un langage où les références de types tels que les chaînes de caractères peuvent être nulles, il faut supposer que

class Person
    private string FirstName
    private string MiddleName
    private string LastName

vous vous retrouvez à écrire des choses 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 être 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 p garantit que le premier/dernier est présent mais que le milieu peut être nul, ou peut-être que vous effectuez des contrôles qui lèvent différents types d'exceptions, ou qui sait quoi. Tous ces choix d'implémentation fous et ces choses auxquelles il faut penser apparaissent parce qu'il y a cette stupide valeur représentable que vous ne voulez pas ou dont vous n'avez pas besoin.

La valeur nulle ajoute généralement une complexité inutile. La complexité est l'ennemi de tout logiciel, et vous devez vous efforcer de la réduire chaque fois que cela est possible.

(Notez bien que même ces exemples simples sont plus complexes. Même si un FirstName ne peut être null , a string peut 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 chaînes non nulles, il se peut que nous soyons toujours en train de "représenter des valeurs sans signification". Encore une fois, vous pouvez choisir de lutter contre ce problème soit par le biais d'invariants et de code conditionnel au moment de l'exécution, soit en utilisant le système de types (par exemple, pour avoir un NonEmptyString type). Cette dernière solution est peut-être malvenue ( les "bons" types sont souvent "fermés" sur un ensemble d'opérations communes, et par exemple NonEmptyString n'est pas fermé sur .SubString(0,0) ), mais il démontre plus de points dans l'espace de conception. En fin de compte, dans n'importe quel système de types donné, il y a une certaine complexité dont il se débarrassera très bien, et une autre complexité qui est juste intrinsèquement plus difficile à se débarrasser. La clé de ce sujet est que dans presque chaque le passage de "références nulles par défaut" à "références non nulles par défaut" est presque toujours un changement simple qui rend le système de types beaucoup plus apte à combattre la complexité et à exclure certains types d'erreurs et d'états sans signification. Il est donc assez fou que tant de langages répètent cette erreur encore et encore).

2 votes

+1, mais notez que toutes les personnes n'ont pas de nom de famille ("prénom" et "nom de famille" semblent être plus précis de toute façon) ; j'ai entendu parler de personnes qui n'ont qu'un seul nom (ce qui signifie qu'elles ne correspondent pas à la plupart des modèles de données). Les noms arabes peuvent être particulièrement compliqués. Toutes les cartes de crédit n'ont pas non plus 16 chiffres.

31 votes

Re : noms - En effet. Et peut-être vous intéressez-vous à la modélisation d'une porte qui est suspendue ouverte mais dont le pêne dormant dépasse, empêchant la porte de se fermer. Il y a beaucoup de complexité dans le monde. La clé est de ne pas ajouter plus complexité lors de la mise en œuvre de la correspondance entre les "états du monde" et les "états du programme" dans votre logiciel.

2 votes

À propos, pour une bonne lecture sur le sujet de la représentation dans les logiciels, je suggère l'ouvrage épuisé "Abstraction and Specification in Program Development" (de Liskov, utilisant le langage CLU).

65voto

jalf Points 142628

L'avantage des types d'option n'est pas qu'ils sont facultatifs. C'est que tous les autres types ne le sont pas .

Parfois nous devons être en mesure de représenter une sorte d'état "nul". Parfois, nous devons représenter une option "aucune valeur" ainsi que les autres valeurs possibles d'une variable. Par conséquent, un langage qui refuse catégoriquement cette possibilité sera un peu handicapé.

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

Souvent, il ne sera jamais en fait être nulle, car le programmeur structure le code de manière à ce que cela ne puisse jamais se produire. Mais le compilateur ne peut pas le vérifier, et chaque fois que vous le voyez, vous devez vous demander "est-ce que cela peut être null ? Dois-je vérifier si c'est nul 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 en .NET, où presque tout peut être nul. Vous devez compter sur 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 null, ou vous devez être paranoïaque et contrôler tout .

Cependant, si les types ne sont pas nullables par défaut alors 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 compilateur/contrôleur de type l'impose pour vous.

Et puis nous avons juste besoin d'une porte dérobée pour les rares cas où nous hacer doivent gérer un état nul. Dans ce cas, un type "option" peut être utilisé. Nous autorisons alors l'état nul dans les cas où nous avons pris la décision consciente de pouvoir 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 en Java par exemple, null peut signifier l'une des deux choses suivantes :

  1. la variable n'est pas initialisée. Cela devrait, idéalement, jamais se produire. Une variable ne devrait pas existe à moins qu'il ne soit initialisé.
  2. la variable contient certaines données "facultatives" : elle doit pouvoir représenter le cas où il n'y a pas de données . C'est parfois nécessaire. Vous essayez peut-être de trouver un objet dans une liste, et vous ne savez pas à l'avance s'il s'y trouve ou non. Il faut alors pouvoir représenter que "aucun objet n'a été trouvé".

Le second sens doit être préservé, mais le premier doit être entièrement éliminé. Et même le deuxième sens ne devrait pas être le sens 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 optionnel, nous voulons que le vérificateur de type garantie qu'il ne sera jamais nul.

0 votes

Et dans le second sens, nous voulons que le compilateur nous avertisse (arrête ?) si nous essayons d'accéder à de telles variables sans vérifier la nullité au préalable. Voici un excellent article sur la future fonctionnalité null/non-null de C# (enfin !) blogs.msdn.microsoft.com/dotnet/2017/11/15/

45voto

Kevin Wright Points 31665

Toutes les réponses jusqu'ici se concentrent sur le pourquoi null est une mauvaise chose, et que c'est plutôt pratique si un langage peut garantir que certaines valeurs seront jamais être nul.

Ils suggèrent ensuite que ce serait une idée intéressante si l'on imposait la non-nullité pour les éléments suivants todo ce qui peut être fait si vous ajoutez un concept tel que Option o Maybe pour représenter des types qui n'ont pas toujours une valeur définie. C'est l'approche adoptée par Haskell.

C'est tout bon ! Mais cela n'empêche pas l'utilisation de types explicitement nullables/non nuls pour obtenir le même effet. Pourquoi, alors, l'option est-elle toujours une bonne chose ? Après tout, Scala supporte les valeurs nullables (is a pour qu'il puisse fonctionner avec les bibliothèques Java) mais supporte Options également.

Q. Quels sont donc les avantages, outre le fait de pouvoir supprimer complètement les valeurs nulles d'un langage ?

A. Composition

Si vous faites une traduction naïve à partir d'un code nul

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

à un code tenant compte des 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 un terrible façon d'utiliser les 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 les listes d'options, c'est encore mieux. Imaginez que la liste people est lui-même facultatif :

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

Comment cela fonctionne-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 les contrôles de nullité (ou même les opérateurs elvis ? :) serait douloureusement long. Le vrai truc 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.

8 votes

+1, c'est un bon point à souligner. Un addendum : au pays d'Haskell, flatMap s'appellerait (>>=) c'est-à-dire l'opérateur "bind" pour les monades. C'est vrai, les Haskellers aiment flatMap à tel point que nous l'avons mis dans le logo de notre langue.

1 votes

+1 Espérons que l'expression de Option<T> ne serait jamais, jamais nulle. Malheureusement, Scala est euhh, toujours lié à Java :-) (D'un autre côté, si Scala n'était pas compatible avec Java, qui l'utiliserait ? O.o)

0 votes

C'est assez facile à faire : "List(null).headOption". Notez que cela signifie quelque chose de très différent qu'une valeur de retour de 'None'.

38voto

tc. Points 23958

Puisque les gens semblent le manquer : null est ambiguë.

La date de naissance d'Alice est null . Qu'est-ce que cela signifie ?

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 avons-nous obtenu des réponses différentes ?


Un autre problème : null est un cas limite.

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

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

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


Et maintenant, l'histoire obligatoire :

J'ai récemment conçu une table de base de données (sqlite3) comportant 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_a existe uniquement pour des raisons de compatibilité avec une 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 NULL fonctionne en SQL, je peux insérer (1, 2, NULL, 3, t) y (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 faciliter la modélisation des situations du "monde réel", par exemple, deux personnes ne peuvent pas avoir le même numéro de sécurité sociale, mais toutes les personnes n'en ont pas un).


Pour information, sans invoquer d'abord un comportement indé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 membres de référence non initialisées (si une exception est levée, la construction échoue).

Remarque : il peut arriver que l'on veuille des pointeurs mutuellement exclusifs (c'est-à-dire qu'un seul d'entre eux peut être non NULL), par exemple dans un hypothétique iOS type DialogState = NotShown | ShowingActionSheet UIActionSheet | ShowingAlertView UIAlertView | Dismissed . Au lieu de cela, je suis obligé de faire des choses comme assert((bool)actionSheet + (bool)alertView == 1) .

0 votes

Les vrais mathématiciens n'utilisent pas le concept de "NaN", rassurez-vous.

0 votes

@Noldorin : Ils le font, mais ils utilisent le terme "forme indéterminée".

0 votes

@I.J.Kennedy : C'est un collège différent, que je connais assez 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 !

16voto

Stephen Swensen Points 13439

Le caractère indésirable de la nullité par défaut des références/pointeurs.

Je ne pense pas que ce soit le principal problème avec les zéros, le principal problème avec les zéros est qu'ils peuvent signifier deux choses :

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

Les langages qui supportent les types Option interdisent ou découragent également l'utilisation de variables non initialisées.

Comment fonctionnent les types d'options, y compris les stratégies visant à faciliter la vérification des cas nuls, comme la correspondance des modèles.

Pour être efficaces, les types d'option doivent être pris en charge directement dans la langue. Sinon, il faut beaucoup de code passe-partout pour les simuler. La correspondance de motifs et l'inférence de type sont deux caractéristiques clés du langage qui facilitent le travail avec les types Option. Par exemple :

En fa# :

//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;")

Toutefois, dans un langage comme Java, qui ne prend pas directement en charge les types d'option, 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 le message mangeant nil

Le "message eating nil" d'Objective-C n'est pas tant une solution qu'une tentative d'alléger le casse-tête de la vérification des nullités. En gros, au lieu de lever une exception d'exécution en essayant d'invoquer une méthode sur un objet nul, l'expression est évaluée à null elle-même. En suspendant l'incrédulité, c'est comme si chaque méthode d'instance commençait par if (this == null) return null; . Mais il y a alors une perte d'information : vous ne savez pas si la méthode a renvoyé null parce que c'est une valeur de retour valide, ou parce que l'objet est réellement null. Cela ressemble beaucoup à l'avalement d'une exception, et ne fait aucun progrès dans la résolution des problèmes liés à null décrits précédemment.

0 votes

C'est une bête noire, mais le C# est loin d'être un langage similaire au C.

0 votes

Avec linq, c'est ça. Je pensais à C# et je n'avais pas remarqué ça.

1 votes

Oui, avec une syntaxe inspirée du c principalement, mais je pense avoir également entendu parler de langages de programmation impératifs comme python/ruby avec très peu de syntaxe de type c, appelés c-like par les programmeurs fonctionnels.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X