Les paramètres nommés rendent le code plus facile à lire et plus difficile à écrire
Lorsque je lis un morceau de code, les paramètres nommés peuvent introduire un contexte qui rend le code plus facile à comprendre. Considérons par exemple ce constructeur: Color(1, 102, 205, 170)
. Qu'est-ce que ça veut dire? En effet, Color(alpha: 1, red: 102, green: 205, blue: 170)
serait beaucoup plus facile à lire. Mais hélas, le compilateur dit «non» - il veut Color(a: 1, r: 102, g: 205, b: 170)
. Lorsque vous écrivez du code à l'aide de paramètres nommés, vous passez un temps inutile à rechercher les noms exacts - il est plus facile d'oublier les noms exacts de certains paramètres que d'oublier leur ordre.
Cela m'a une fois mordu lorsque j'utilisais une DateTime
API qui avait deux classes frères pour les points et les durées avec des interfaces presque identiques. Bien DateTime->new(...)
accepté un second => 30
argument, le DateTime::Duration->new(...)
voulu seconds => 30
, et similaire pour d'autres unités. Oui, c'est tout à fait logique, mais cela m'a montré que les paramètres nommés ≠ facilité d'utilisation.
Les mauvais noms ne facilitent même pas la lecture
Un autre exemple de la façon dont les paramètres nommés peuvent être mauvais est probablement le langage R. Ce morceau de code crée un tracé de données:
plot(plotdata$n, plotdata$mu, type="p", pch=17, lty=1, bty="n", ann=FALSE, axes=FALSE)
Vous voyez deux arguments positionnels pour les lignes de données x et y , puis une liste de paramètres nommés. Il existe de nombreuses autres options avec des valeurs par défaut, et seules celles qui sont répertoriées dont je voulais modifier ou spécifier explicitement les valeurs par défaut. Une fois que nous ignorons que ce code utilise des nombres magiques et pourrait bénéficier de l'utilisation d'énumérations (si R en avait!), Le problème est que beaucoup de ces noms de paramètres sont plutôt indéchiffrables.
pch
est en fait le caractère de tracé, le glyphe qui sera dessiné pour chaque point de données. 17
est un cercle vide, ou quelque chose comme ça.
lty
est le type de ligne. Voici 1
une ligne continue.
bty
est le type de boîte. Le paramétrer pour "n"
éviter qu'une boîte soit dessinée autour de l'intrigue.
ann
contrôle l'apparence des annotations d'axe.
Pour quelqu'un qui ne sait pas ce que signifie chaque abréviation, ces options sont plutôt déroutantes. Cela révèle également pourquoi R utilise ces étiquettes: pas comme code auto-documenté, mais (étant un langage typé dynamiquement) comme clés pour mapper les valeurs à leurs variables correctes.
Propriétés des paramètres et des signatures
Les signatures de fonction peuvent avoir les propriétés suivantes:
- Les arguments peuvent être ordonnés ou non,
- nommé ou non,
- requis ou facultatif.
- Les signatures peuvent également être surchargées par taille ou type,
- et peut avoir une taille non spécifiée avec varargs.
Différentes langues atterrissent à différentes coordonnées de ce système. En C, les arguments sont ordonnés, sans nom, toujours requis et peuvent être des varargs. En Java, la situation est similaire, sauf que les signatures peuvent être surchargées. Dans l'Objectif C, les signatures sont ordonnées, nommées, requises et ne peuvent pas être surchargées car il s'agit simplement de sucre syntaxique autour de C.
Les langages à typage dynamique avec varargs (interfaces de ligne de commande, Perl,…) peuvent émuler des paramètres nommés facultatifs. Les langues avec surcharge de taille de signature ont quelque chose comme des paramètres optionnels de position.
Comment ne pas implémenter les paramètres nommés
Lorsque nous pensons à des paramètres nommés, nous supposons généralement des paramètres nommés, facultatifs et non ordonnés. Leur mise en œuvre est difficile.
Les paramètres facultatifs peuvent avoir des valeurs par défaut. Ceux-ci doivent être spécifiés par la fonction appelée et ne doivent pas être compilés dans le code appelant. Sinon, les valeurs par défaut ne peuvent pas être mises à jour sans recompiler tout le code dépendant.
Maintenant, une question importante est de savoir comment les arguments sont réellement passés à la fonction. Avec les paramètres ordonnés, les arguments peuvent être passés dans un registre ou dans leur ordre inhérent sur la pile. Lorsque nous excluons les registres pendant un moment, le problème est de savoir comment mettre des arguments facultatifs non ordonnés sur la pile.
Pour cela, nous avons besoin d'un certain ordre sur les arguments facultatifs. Que faire si le code de déclaration est modifié? Comme l'ordre n'est pas pertinent, une réorganisation dans la déclaration de fonction ne doit pas modifier la position des valeurs sur la pile. Nous devons également considérer si l'ajout d'un nouveau paramètre facultatif est possible. Du point de vue des utilisateurs, cela semble être le cas, car le code qui n'utilisait pas ce paramètre auparavant devrait toujours fonctionner avec le nouveau paramètre. Cela exclut donc les commandes comme l'utilisation de l'ordre dans la déclaration ou l'utilisation de l'ordre alphabétique.
Considérez cela également à la lumière du sous-typage et du principe de substitution Liskov - dans la sortie compilée, les mêmes instructions devraient pouvoir invoquer la méthode sur un sous-type avec éventuellement de nouveaux paramètres nommés et sur un supertype.
Implémentations possibles
Si nous ne pouvons pas avoir de commande définitive, nous avons donc besoin d'une structure de données non ordonnée.
La mise en œuvre la plus simple consiste à simplement passer le nom des paramètres avec les valeurs. C'est ainsi que les paramètres nommés sont émulés en Perl ou avec des outils de ligne de commande. Cela résout tous les problèmes d'extension mentionnés ci-dessus, mais peut être une énorme perte d'espace - pas une option dans le code critique pour les performances. De plus, le traitement de ces paramètres nommés est maintenant beaucoup plus compliqué que de simplement extraire des valeurs d'une pile.
En fait, l'espace requis peut être réduit en utilisant le regroupement de chaînes, ce qui peut réduire les comparaisons de chaînes ultérieures aux comparaisons de pointeurs (sauf lorsqu'il ne peut pas être garanti que les chaînes statiques sont réellement regroupées, auquel cas les deux chaînes devront être comparées dans détail).
Au lieu de cela, nous pourrions également passer une structure de données intelligente qui fonctionne comme un dictionnaire d'arguments nommés. C'est bon marché du côté de l'appelant, car le jeu de clés est connu statiquement à l'emplacement de l'appel. Cela permettrait de créer une fonction de hachage parfaite ou de précalculer un trie. L'appelé devra toujours tester l'existence de tous les noms de paramètres possibles, ce qui est quelque peu coûteux. Quelque chose comme ça est utilisé par Python.
C'est donc trop cher dans la plupart des cas
Si une fonction avec des paramètres nommés doit être correctement extensible, un ordre définitif ne peut pas être supposé. Il n'y a donc que deux solutions:
- Intégrez l'ordre des paramètres nommés à la signature et interdisez les modifications ultérieures. Ceci est utile pour le code auto-documenté, mais n'aide pas avec les arguments facultatifs.
- Passez une structure de données clé-valeur à l'appelé, qui doit ensuite extraire des informations utiles. C'est très cher en comparaison, et généralement vu uniquement dans les langages de script sans accent sur les performances.
Autres pièges
Les noms de variables dans une déclaration de fonction ont généralement une signification interne et ne font pas partie de l'interface - même si de nombreux outils de documentation les affichent toujours. Dans de nombreux cas, vous souhaitez des noms différents pour une variable interne et l'argument nommé correspondant. Les langages qui ne permettent pas de choisir les noms visibles en externe d'un paramètre nommé n'en gagnent pas beaucoup si le nom de variable n'est pas utilisé en tenant compte du contexte d'appel.
Un problème avec les émulations d'arguments nommés est le manque de vérification statique du côté de l'appelant. Ceci est particulièrement facile à oublier lors de la transmission d'un dictionnaire d'arguments (en vous regardant, Python). Ceci est important car le passage d' un dictionnaire est une solution commune, par exemple en JavaScript: foo({bar: "baz", qux: 42})
. Ici, ni les types de valeurs ni l'existence ou l'absence de certains noms ne peuvent être vérifiés statiquement.
Émulation de paramètres nommés (dans des langages typés statiquement)
La simple utilisation de chaînes comme clés et de tout objet comme valeur n'est pas très utile en présence d'un vérificateur de type statique. Cependant, les arguments nommés peuvent être émulés avec des structures ou des littéraux d'objet:
// Java
static abstract class Arguments {
public String bar = "default";
public int qux = 0;
}
void foo(Arguments args) {
...
}
/* using an initializer block */
foo(new Arguments(){{ bar = "baz"; qux = 42; }});