nil, c'est nul !
Nil n’est méchant volontairement
(Socrate. Ou presque.)
Si Aristote faisait du ruby, il aurait très certainement dit quelque chose de ce genre:
“La nature a horreur du nil.”
Vous l’avez sûrement déjà compris, malin comme vous êtes, bande de petits strudels: cet article va parler de l’usage de la valeur nil en Ruby.
# Petits rappels
Histoire que tout le monde soit sur la même page, nil est une valeur spéciale de Ruby. Il s’agit de la seule instance de la classe NilClass:
La valeur nil ne peut pas être clonée (singleton oblige):
Et c’est aussi la seule valeur (autre que false) qui s’évalue comme “fausse”:
La possibilité du nil
(Michel Hashbecq)
On peut retrouver cette valeur spéciale dans un certain nombre de contextes:
Cette liste n’est pas exhaustive, mais elle donne une certaine idée du nombre de situations dans lesquelles on peut se retrouver à manipuler du nil.
Le problème avec nil
Le problème qu’on rencontre, avec nil, c’est qu’à partir du moment où l’on commence à en manipuler, on doit s’attendre à en avoir partout. Et donc tester la nullité de toute valeur. Ce qui donne très vite du code pénible à lire ou avec des exceptions levées à des endroits qui n’ont rien à voir avec la cause réelle de l’exception.
## La gestion de nil entache la lisibilité
Prenons l’exemple du code suivant, qui fait assez peu de choses: il récupère un owner depuis un hash de mapping owner_id<=>owner
, récupère le pays associé, et l’affiche sur la sortie standard après l’avoir décoré au besoin:
Ce code, qui fonctionnellement fait assez peu de choses, est criblé de tests de nullité, ce qui rend sa lisibilité assez pénible. Il est cependant assez facile de le rendre plus lisible, et plus facile à maintenir.
La gestion de l’owner, et le warning final
Ici, on note qu’au lieu d’interrompre le l’exécution de la méthode avec un return, le développeur a utilisé un bloc if qui ne s’exécute que si owner n’est pas nil, dans l’objectif de conserver l’affichage du warning final en sortie de méthode.
Nous avons plusieurs solutions pour gérer cela de manière plus lisibles. En premier lieu, j’ai souvent tendance à utiliser Hash#fetch plutôt que Hash#[], pour gérer les valeurs par défaut de manière explicite (sauf si j’ai décidé de définir une valeur par défaut pour Hash#[] lors de la création du hash).
La version avec un bloc ensure
Ici, on utilise deux astuces:
- On utilise Hash#fetch en lui passant un bloc qui ne contient que l’instruction return, qui va sortir de la méthode si on ne trouve pas d’owner id associé.
- On place le dernier warn dans le block ensure de la méthode. Ce bloc sera exécuté quoi qu’il arrive, une fois la méthode terminée (nota: ça ne change pas la valeur de retour de la méthode).
La version avec séparation des responsabilités d’affichage des warning et de l’owner
Ici, on décide simplement de donner la responsabilité de l’affichage de l’owner à une méthode print_owner_implementation, et la méthode print_owner ne s’occupe que de décorer l’appel de l’implémentation avec les warnings.
#### La version avec une couche d’abstraction supplémentaire et un “null object”:
On peut aussi décider de déporter la responsabilité de l’affichage du nom dans un autre objet.
Ici, j’utilise une structure Owner, qui contient un nom, et qui a la responsabilité de l’afficher lorsqu’on appelle la méthode Owner#print.
Pour faire bonne mesure, et gérer le cas où je n’ai pas d’owner associé à un id, j’ai aussi ajouté une class NullOwner, qui implémente une méthode NullOwner.print, qui ne fait rien du tout. Cette classe est utilisée comme un objet neutre.
La gestion des paramètres optionnels
Une des choses que j’adore avec Ruby est la richesse des possibilités offertes concernant la gestion des paramètres de méthode.
Malheureusement, souvent, on peut croiser du code qui ressemble à ce qui suit:
Ce qui est plutôt dangereux en terme de contrôle de flux. Pour ce bout de code, appeler print_owner('bob')
et print_owner('bob', nil)
revient absolument au même. Et comme nil est assez souvent utilisé comme valeur de retour lorsqu’un méthode n’a pas de résultat ni d’effets de bord, on peut se retrouver à faire juste n’importe quoi.
Je vais donc répéter une chose que je radote assez régulièrement:
N’utilisez pas nil comme une information.
Jamais.
(bordel)
La raison pour cela est simple. Imaginons que j’ai deux listes (théoriquement de la même taille) contenant, l’une des prénoms, et l’autre, le nombre de fois que je veux les afficher. Pour ce faire, je vais utiliser la méthode print_name
de la manière suivante:
C’est élégant, cela fait le job en tenant sur une ligne, mais que se passe-t-il si il y a une incohérence au niveau des données ?
Dans l’exemple précédent, on constate que le fait que notre repeat_list était trop courte n’a posé aucun problème, car Array#zip a juste comblé la valeur manquante par… nil !
Du coup, notre fonction a considéré qu’elle pouvait afficher “bob” une fois, alors que idéalement, elle aurait du jeter une exception.
Ce n’est pas très grave dans le cadre de ma méthode print_name qui affiche juste une chaîne, mais imaginez le potentiel de catastrophe si on parle de système financier ou autre production critique…
Si vraiment vous devez utiliser une paramètre optionnel, avec une valeur neutre, et que vous ne souhaitez pas sortir la responsabilité de l’action dans un objet externe, il vous est possible d’utiliser un symbole qui vous servira de valeur neutre. Sur les grosses bases de code, pour éviter les collisions de symbole, j’ai tendance à utiliser une constante contenant un symbole suffixé d’un md5sum de l’heure pour m’assurer de son unicité (si tout le monde commence à balader et manipuler du :none
partout, on en revient vite au même problème qu’avec nil).
Maintenant, si j’essaye encore le bout de code sensé afficher les noms d’alice et bob, il va afficher deux fois alice, puis jeter une exception pour bob (NoMethodError: undefined method ‘times’ for nil:NilClass), ce qui est déjà plus cohérent.
D’autre part, il y a une chose à savoir avec les paramètres optionnels: vous pouvez utiliser n’importe quoi comme valeur par défaut.
Du coup, c’est bien plus rigolo de jouer avec que de manipuler du nil.
Et la memoization ?
Un raccourci assez pratiqué en ruby consiste à memoizer certaines valeurs calculées de la manière qui suit:
Inconvénient de ce bidule: si votre méthode slow_method retourne nil, et bien votre valeur est recalculée à chaque fois.
Une implémentation un tantinet plus safe serait:
Explication avec un coup d’irb:
Affecter nil à une variable d’instance la définit à la volée, alors que son évaluation renvoie toujours nil.
Conclusion
Du coup, il est tout à fait possible de se débarrasser de ces disgracieux .nil?
qui émaillent le code du début:
Il existe encore plein d’autres moyens pour éviter d’avoir à gérer la nullité de vos valeurs manipulées, mais j’espère que ces quelques conseils permettront à certains d’entre vous d’éviter de s’arracher les cheveux sur des bug cryptiques causés par un nil vicieusement planqué au fin fond de votre code.
Les principes généraux que j’aime bien essayer de partager:
- nil n’est pas une information.
- nil est une absence d’information.
- Utiliser une valeur neutre autre que nil permet de faire la différence entre un contexte normal et anormal d’utilisation d’une méthode.
- Les NullObjects (allez lire Much ado about naught si ce n’est pas déjà fait) sont vos amis.