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 type string.
  • « un nombre » : en JavaScript, number est une primitive et des tas de notations peuvent retourner un number — 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? (╯°□°)╯︵ ┻━┻

Exemples de notations qui peuvent retourner un number.

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 type string ou number. Par exemple, pour un tableau ou une fonction, cela retournait NaN 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 retourner true 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.

I get it! I finally understand everything. Oops no, false alarm.
Comic de Crimes Against Hugh’s Manatees.

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

Quelques exemples du module ”is-plain-number” tirés du README.

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'

Youpi ! Mon module retourne false quand un argument de type string est écrit en notation scientifique exponentielle.

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'

༼ つ ಥ_ಥ ༽つ Naaaaan ! Mon module retourne ”true” quand un argument de type ”number” est écrit en notation scientifique exponentielle.

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

On peut comparer ici les différences entre le module ”is-plain-number” et le module ”is-string-a-number”.

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'

¯\_(ツ)_/¯ Oups ! Un autre cas où mon égalité ne fonctionne pas !

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.

Dessin de l'autrice devant une pile de « packages » npm.

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

  1. HTeuMeuLeu, le vendredi 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.

  2. Fanny Cheung, le samedi 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é sera ”true” 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 de ”Math.round” : ”Math.round(-0.42)” aura en sortie ”-0”. On sait alors qu’on s’approche du zéro par l’ensemble négatif.