Le périple vers mon premier module utilitaire npm
Parfois, il m’arrive de me demander :
Mais, comment font tous ces autres, pour publier des utilitaires aussi chouettes pour la communauté JavaScript ?
Mais, oui, comment donc ? La réponse est simple. Ils ou elles n’ont pas trouvé leur bonheur parmi les mille et un modules npm ou autres librairies sur les Internets. Forcé·e·s de mettre les mains dans le cambouis et de tenter de coder soi-même ce dont on a besoin.
Au commencement, une simple fonction
Cette histoire débute alors que j’étais en train de mesurer la performance sur l’une de mes page web. Je tentais de lister les endroits où mon code s’engorgeait et je cherchais à améliorer l’architecture qui avait amenée ma page à cette performance catastrophique.
De la page, j’ai atterri sur un composant React. Du composant, je suis arrivée à l’implémentation. De l’implémentation, j’ai tenté d’extraire un bout de la logique dans une petite fonction à part.
Et là, ce fut le début de la fin…
Mon problème était le suivant : je cherchais à vérifier si une valeur de type string
représentait un « nombre ».
Petit focus sur cette phrase :
- « une valeur de type
string
» : cela explicite le fait que l’argument passé à la fonction sera de typestring
. - « un nombre » : en JavaScript,
number
est une primitive et des tas de notations peuvent retourner unnumber
— la notation scientifique exponentielle, l’écriture en base hexadécimale ou encore l’opérateur unaire (+). Mon but était de me focaliser sur les nombres écrits en notation fixe (celle à laquelle nous sommes habitués) et en base décimale.
typeof 42e42
// => 'number'
typeof 0x2a
// => 'number'
typeof Infinity
// => 'number'
typeof +''
// => 'number'
typeof NaN
// => 'number'
// What the hell? (╯°□°)╯︵ ┻━┻
Je me suis lancée dans quelques recherches sur les Internets. Jetant un oeil à notre bon vieux jQuery.isNumeric
. Ou encore le très bon module is-number
. Et quelques autres questions du même acabit sur Stack Overflow.
Au final, je me suis résolue à voir ce que je pouvais coder avec la fonction globale de haut niveau parseFloat
.
Le grand saut dans un module npm
J’ai bricolé une fonction qui semblait être en adéquation avec mon cas d’usage :
function(value) {
var float = parseFloat(value)
if (isNaN(float)) return false
if (!isFinite(float)) return false
return String(float) === String(value)
}
- la fonction
parseFloat
me permettait d’exclure tous les arguments qui n’étaient pas de typestring
ounumber
. Par exemple, pour un tableau ou une fonction, cela retournaitNaN
dont le cas était géré par mon premier “early return”. - Mon second “early return” s’occupait du cas de la propriété globale
Infinity
. - L’égalité finale vérifiait si l’argument passé était strictement identique à celui transformé en
number
pour exclure la notation scientifique exponentielle. Pour donner un exemple :'42'
pouvait retournertrue
tandis que'42e4'
ne le pouvait pas.
Les premiers pas vers mon module
Je me suis demandée alors où je pouvais ranger cette fonction utilitaire. Dans mon composant à l’intérieur d’une méthode d’instance ? Ou carrément dans mon projet pour pouvoir l’utiliser ailleurs ? Si je trouvais une telle utilité à ma fonction, alors d’autres développeurs ou développeuses pouvaient y trouver leur compte aussi. Ou pas.
Dans tous les cas, moi, je me voyais utiliser cette fonction dans bien d’autres projets. J’ai donc décidé d’extraire complètement la fonction et de créer un module npm. Naïvement, je pensais que le code de mon module était presqu’abouti, puisqu’il servait déjà le cas d’usage de mon projet. Sans surprise, je me fourvoyais complètement.
J’ai choisi d’appeler mon module is-plain-number
, la fonction étant un prédicat qui vérifiait si une valeur était un nombre « simple » (en anglais « plain »), sous-entendu un nombre en notation fixe et en base décimale.
Ainsi, je publiais la première version de mon module :
- sans dépendances comme le module n’était qu’une simple fonction,
- avec une suite de tests qui décrivait tous les cas d’usage que je voulais couvrir,
- un README fiable pour permettre aux autres développeurs de comprendre rapidement et efficacement le sujet du module.
Je dois souligner l’importance de ces trois points. Évidemment, cela sert aux personnes qui allaient utiliser mon module. Mais, de surcroît, cela m’a énormément aidée à comprendre mon propre code, l’expliquer et le remettre en question.
import isPlainNumber from 'is-plain-number'
isPlainNumber('42') // => true
isPlainNumber('42.2') // => true
isPlainNumber('42e4') // => false
isPlainNumber('0x2a') // => false
En relisant le code de mon module, un petit quelque chose m’a gênée — à la fois, dans le nom et, à la fois, dans la suite de tests. Lorsque l’on cherche à nommer un utilitaire, la réflexion se porte sur l’objectif qu’il doit remplir, mais également sur tous les cas d’usage qu’il doit couvrir. Par conséquent, également sur tout ce qu’il y a à tester.
Quel était le but premier de ma fonction ?
Je suis alors revenue sur le module is-plain-number
et je l’ai totalement remis en question.
Une nouvelle version de mon module
Comme je l’ai dit précédemment, mon cas d’usage personnel était de vérifier si une valeur de type string
était un nombre écrit en notation fixe et en base décimale. Ah ! Nous voilà bien avancés :
« Notation fixe et base décimale » ? En JavaScript, qu’est-ce que cela implique dans l’affichage des nombres ?
Retournons quelques instants à ma fonction :
function(value) {
var float = parseFloat(value)
if (isNaN(float)) return false
if (!isFinite(float)) return false
return String(float) === String(value)
}
La dernière ligne de la fonction est une égalité stricte entre un number
(retourné par parseFloat
) que l’on convertit en string
et l’argument passé à la fonction, lui aussi converti en string
. Avec cette implémentation, mon idée était d’exclure la notation scientifique exponentielle :
var value = '42e4' // => '42e2'
var float = parseFloat(value) // => 420000
return String(float) === String(value)
// => false
// => String(float) => String(420000) => '420000'
// => String(value) => String('42e24') => '42e24'
Toutefois, cela fonctionne uniquement avec des arguments de type string
:
var value = 42e4 // => 420000
var float = parseFloat(value) // => 420000
return String(float) === String(value)
// => true
// => String(float) => String(420000) => '420000'
// => String(value) => String(420000) => '420000'
Sur le coup, je n’ai pas saisi le cas d’usage que j’avais laissé passer pour les arguments de type number
. En JavaScript, les valeurs de type number
sont stockées dans un format binaire à virgule flottante (appelé « binary floating point » en anglais), mais sont affichées principalement en notation fixe et en base décimale. Cette spécificité du langage m’a fait comprendre qu’il n’y avait aucun moyen de savoir si la valeur originelle de type number
écrite par un utilisateur était en base hexadécimale ou en notation scientifique exponentielle.
Cette méconnaissance du langage me balançait en pleine figure l’incohérence de mon module : ce dernier retournait true
pour un argument de type number
même s’il n’était pas écrit en notation fixe et en base décimale. Mais, il retournait false
pour les exacts mêmes cas si l’argument était de type string
.
Et vous savez quoi, je déteste les incohérences ಠ_ಠ.
Je suis repartie de mon cas d’usage d’origine, en laissant les arguments de type number
de côté et j’ai écrit un nouveau module : is-string-a-number.
import isPlainNumber from 'is-plain-number'
import isStringANumber from 'is-string-a-number'
isPlainNumber('42') // true
isStringANumber('42') // true
isPlainNumber('42e4') // false
isStringANumber('42e4') // false
isPlainNumber(42e4) // true
isStringANumber(42e4) // false
Les cas particuliers
En tirant le fil de ma toute nouvelle connaissance sur les nombres en JavaScript, d’autres problèmes ont surgi.
Effectivement, j’avais découvert que l’affichage d’une valeur de type number
est principalement en notation fixe. Cependant, elle peut également, dans certains cas, être en notation exponentielle. Mon égalité stricte ne couvre donc pas tous les cas que j’aurais voulu gérer :
var value = '4200000000000000000000' // => '4200000000000000000000'
var float = parseFloat(value) // => 4.2e+21
return String(float) === String(value)
// => false
// => String(float) => String(4.2e+21) => '4.2e+21'
// => String(value) => String('4200000000000000000000') => '4200000000000000000000'
Et oui, mon module ne fonctionne pas pour les nombres plus grand que 10e21
et les nombres qui commencent par 0.
suivis de plus de 5 zéros.
Pour comprendre davantage cette problématique, je vous invite à lire le très bon article d’Axel Rauschmayer qui explique l’algorithme utilisé pour afficher les nombres en JavaScript. En attendant, je cherche toujours une solution pour gérer ce cas.
Par ailleurs, j’ai également soulevé un autre problème avec les très grands nombres. Le standard ECMAScript ne possède qu’un seul type pour représenter les nombres : les nombres de 64-bit à virgule flottante. Les très grands nombres (également appelés BigInt par une des propositions au TC39) ne peuvent pas être représentés avec ce dernier. Pour le moment, j’ai choisi de ne pas gérer ce cas : mon module ne peut pas fonctionner pour les très grands nombres et voilà tout.
Mais alors, ton module, tu en es contente ?
Après ce long périple inachevé, vous êtes en droit de vous demander si je suis toujours à l’aise avec mon module. Sans hésitation, je réponds :
Oui.
Oui, parce que ces quelques étapes, que je vous ai narrées, ne sont que le début d’un projet. Je peux encore itérer sur les cas particuliers et améliorer l’implémentation. Par-dessus tout, j’ai énormément, énormément appris. De l’implémentation aux erreurs en passant par la production-même du module, j’ai gagné en expérience sur de nombreux points qui semblent évidents mais pas toujours simples à gérer.
Rédiger un bon README
Je ne le dirai jamais assez : les bons README sont essentiels. Ils permettent aux utilisateurs de comprendre rapidement l’utilité d’un module et comment ce dernier peut répondre complètement à leurs besoins (ou pas du tout).
J’ai passé beaucoup de temps à écrire le README de is-string-a-number
, surtout la partie concernant le contexte (nommé « motivation » en anglais). Cela m’a permis de comprendre en quoi je trouvais que mon module avait une utilité. L’interprétation y est subjective et je peux totalement me tromper. Mais, au moins, j’explique le contexte dans lequel j’ai codé et, si d’autres développeurs ou développeuses ne sont pas d’accord avec moi, ils ou elles peuvent contribuer ou m’aider à me remettre en question avec ces informations.
Écrire une suite complète de tests
Bien évidemment, ma mini-fonction venait avec sa suite de tests. Ces tests servaient à vérifier tous les types de valeurs possibles que l’on pouvait passer à la fonction.
Il est intéressant de noter ici, que, lorsque j’ai écrit ces tests la première fois, lors du jet initial de mon module, quelque chose me semblait déjà incohérent. Les test vérifiaient que '42e24'
retournait false
alors que 42e24
retournait true
. Ça sentait déjà le sapin, n’est-ce pas ?
Un test n’est pas un bon test lorsqu’il ne teste pas la bonne fonctionnalité, à savoir ce que vous voulez vraiment. Et, dans mon cas, c’est en partie ce qui m’a amenée à ré-écrire mon module.
Remettre son code en question
Quand il y a un doute, c’est qu’il n’y a pas de doute.
— Solène Maître
Quand j’ai compris que le but de mon module n’était pas clair pour moi et que j’étais passée à côté de quelque chose de très important à propos du langage, j’ai arrêté de coder et je me suis mise à lire tout ce que je pouvais sur les nombres en JavaScript. Grâce au blog d’Axel Rauschmayer, j’ai appris des tas de choses sur les nombres en JavaScript : comment ils sont affichés, comment ils sont encodés et même des trucs rigolos sur le zéro.
Ce savoir m’a permis de mieux définir le périmètre de mon module :
- ce qu’il pouvait gérer,
- comment il fonctionnait et l’origine des cas particuliers,
- ce que je pouvais améliorer.
Quant à publier ?
Je pense qu’il reste important de faire des recherches avant de coder et de partager une librairie, aussi petite soit-elle. Nous n’avons pas besoin de réinventer la roue. De nombreuses personnes ont codé des choses formidables avant nous. L’humilité est essentielle pour comprendre les cas d’usage et les problèmes qui ont été résolus par d’autres auparavant.
Toutefois, cela ne doit pas nous empêcher d’essayer et de créer de nouvelles choses. Si un petit bout de code peut être utile, ne serait-ce qu’à nous-même, ça peut valoir le coup.
Cela a été une très bonne expérience de contribuer à la communauté JavaScript avec ma petite librairie open-source. Si vous avez une idée qui n’a pas encore été abordée ou qui n’a pas été abordée comme vous la voyez, allez quémander des avis, réfléchissez‑y à deux fois, puis lancez-vous : publiez votre propre module !
Au final, peut-être, comme moi, vous trouverez nombre de failles dans votre code, mais c’est cela qui fait la richesse du monde open-source : l’expérience et la collaboration.
Un grand merci à Sunny Ripert, Florent Duveau et l’équipe des 24 jours de web pour la relecture de cet article et leur immense soutien !
2 commentaires sur cet article
HTeuMeuLeu, le 7 décembre 2018 à 13:18
Merci pour cet article, j'ai adoré. Juste une question sur ça : « J’ai appris des tas de choses sur les nombres en JavaScript […] et même des trucs rigolos sur le zéro. ». Tu as des exemples ? J'ai trop envie de savoir maintenant.
Fanny Cheung, le 8 décembre 2018 à 12:47
Après relecture, le mot "rigolos" n'était peut-être pas le plus adapté ! Je dirais plutôt intriguant. En JavaScript, on stock les nombres en suivant le standard IEEE_754. Ce choix fait, qu'en JavaScript, il existe deux zéros : un zéro positif
+0
et un zéro négatif-0
car l'un des bit pour l'encodage est réservé au signe du nombre (+ ou -).Ce concept étant plutôt à l'encontre des mathématiques (0 n'est ni positif, ni négatif), le langage utilise des tentatives pour cacher cette implémentation. Par exemple, dans la console, si on écrit
+0 === -0
, la sortie de cette égalité seratrue
alors que ce sont deux valeurs différentes. Ou encore,(-0).toString()
renverra la même chose que(+0).toString()
. Dans la plupart des cas, cela importe peu qu'il existe deux zéros en JavaScript.Un cas où on peut voir le
-0
apparaître et où cela peut avoir un "intérêt, c'est dans le cas où on cherche à savoir de "quel côté" on s'approche du zéro. Un exemple plus explicite est l'utilisation deMath.round
:Math.round(-0.42)
aura en sortie-0
. On sait alors qu'on s'approche du zéro par l'ensemble négatif.Il n’est plus possible de laisser un commentaire sur les articles mais la discussion continue sur les réseaux sociaux :