Ne pas connaître les noms de paramètres d'une fonction lorsque vous l'appelez

13

Voici un problème de programmation / langage sur lequel j'aimerais avoir votre avis.

Nous avons développé des conventions que la plupart des programmeurs (devraient) suivre qui ne font pas partie de la syntaxe des langages mais servent à rendre le code plus lisible. Ce sont bien sûr toujours un sujet de débat mais il y a au moins quelques concepts de base que la plupart des programmeurs trouvent agréables. Nommer vos variables de manière appropriée, nommer en général, rendre vos lignes pas excessivement longues, éviter les longues fonctions, les encapsulations, ces choses.

Cependant, il y a un problème sur lequel je n'ai encore trouvé personne commentant et qui pourrait bien être le plus important du groupe. C'est le problème des arguments qui sont anonymes lorsque vous appelez une fonction.

Les fonctions proviennent des mathématiques où f (x) a une signification claire car une fonction a une définition beaucoup plus rigoureuse qu'elle ne le fait habituellement en programmation. Les fonctions pures en mathématiques peuvent faire beaucoup moins que ce qu'elles peuvent en programmation et elles sont un outil beaucoup plus élégant, elles ne prennent généralement qu'un seul argument (qui est généralement un nombre) et elles renvoient toujours une valeur (également généralement un nombre). Si une fonction prend plusieurs arguments, ce ne sont presque toujours que des dimensions supplémentaires du domaine de la fonction. En d'autres termes, un argument n'est pas plus important que les autres. Ils sont explicitement ordonnés, bien sûr, mais à part cela, ils n'ont pas d'ordre sémantique.

En programmation, cependant, nous avons plus de fonctions définissant la liberté, et dans ce cas, je dirais que ce n'est pas une bonne chose. Une situation courante, vous avez une fonction définie comme ceci

func DrawRectangleClipped (rectToDraw, fillColor, clippingRect) {}

En regardant la définition, si la fonction est écrite correctement, c'est parfaitement clair quoi. Lors de l'appel de la fonction, vous pourriez même avoir une magie d'intellisense / complétion de code en cours dans votre IDE / éditeur qui vous dira quel devrait être l'argument suivant. Mais attendez. Si j'en ai besoin lorsque j'écris l'appel, n'y a-t-il pas quelque chose qui nous manque ici? La personne qui lit le code n'a pas l'avantage d'un IDE et à moins qu'elle ne passe à la définition, elle ne sait pas lequel des deux rectangles passés comme arguments est utilisé pour quoi.

Le problème va encore plus loin. Si nos arguments proviennent d'une variable locale, il peut y avoir des situations où nous ne savons même pas quel est le deuxième argument car nous ne voyons que le nom de la variable. Prenons par exemple cette ligne de code

DrawRectangleClipped(deserializedArray[0], deserializedArray[1], deserializedArray[2])

Cela est atténué à divers degrés dans différentes langues, mais même dans des langues strictement typées et même si vous nommez vos variables de manière judicieuse, vous ne mentionnez même pas le type de la variable lorsque vous la passez à la fonction.

Comme c'est généralement le cas avec la programmation, il existe de nombreuses solutions potentielles à ce problème. Beaucoup sont déjà implémentés dans les langues populaires. Paramètres nommés en C # par exemple. Cependant, tout ce que je sais présente des inconvénients importants. Nommer chaque paramètre sur chaque appel de fonction ne peut pas conduire à un code lisible. On dirait presque que nous dépassons peut-être les possibilités que nous offre la programmation en texte brut. Nous sommes passés du texte JUST dans presque tous les domaines, mais nous codons toujours le même. Plus d'informations sont nécessaires pour être affichées dans le code? Ajoutez plus de texte. Quoi qu'il en soit, cela devient un peu tangentiel, donc je vais m'arrêter ici.

Une réponse que j'ai obtenue au deuxième extrait de code est que vous décompressez probablement d'abord le tableau dans certaines variables nommées, puis les utilisez, mais le nom de la variable peut signifier beaucoup de choses et la façon dont elle est appelée ne vous indique pas nécessairement la façon dont elle est censée être interprété dans le contexte de la fonction appelée. Dans la portée locale, vous pouvez avoir deux rectangles nommés leftRectangle et rightRectangle car c'est ce qu'ils représentent sémantiquement, mais il n'a pas besoin de s'étendre à ce qu'ils représentent lorsqu'ils sont donnés à une fonction.

En fait, si vos variables sont nommées dans le contexte de la fonction appelée, vous introduisez moins d'informations que vous ne le pourriez potentiellement avec cet appel de fonction et à un certain niveau si cela conduit à un code pire. Si vous avez une procédure qui aboutit à un rectangle que vous stockez dans rectForClipping, puis une autre procédure qui fournit rectForDrawing, alors l'appel réel à DrawRectangleClipped n'est qu'une cérémonie. Une ligne qui ne signifie rien de nouveau et qui est là juste pour que l'ordinateur sache exactement ce que vous voulez, même si vous l'avez déjà expliqué avec votre nom. Ce n'est pas une bonne chose.

J'aimerais vraiment entendre de nouvelles perspectives à ce sujet. Je suis sûr que je ne suis pas le premier à considérer cela comme un problème, alors comment est-il résolu?

Darwin
la source
2
Je suis confus quant au problème exact ... il semble y avoir plusieurs idées ici, je ne sais pas laquelle est votre point principal.
FrustratedWithFormsDesigner
1
La documentation de la fonction doit vous indiquer ce que font les arguments. Vous pourriez objecter que quelqu'un qui lit le code peut ne pas avoir la documentation, mais alors ne pas vraiment savoir ce que fait le code et quelle que soit la signification qu'il extrait de sa lecture est une supposition éclairée. Dans tout contexte où le lecteur doit savoir que le code est correct, il va avoir besoin de la documentation.
Doval
3
@Darwin Dans la programmation fonctionnelle, toutes les fonctions n'ont toujours qu'un seul argument. Si vous devez passer "plusieurs arguments", le paramètre est généralement un tuple (si vous voulez qu'ils soient ordonnés) ou un enregistrement (si vous ne voulez pas qu'ils soient). De plus, il est trivial de former des versions spécialisées de fonctions à tout moment, vous pouvez donc réduire le nombre d'arguments nécessaires. Étant donné que presque tous les langages fonctionnels fournissent une syntaxe pour les tuples et les enregistrements, regrouper les valeurs est indolore et vous obtenez la composition gratuitement (vous pouvez chaîner des fonctions qui retournent des tuples avec celles qui en prennent.)
Doval
1
@Bergi Les gens ont tendance à généraliser beaucoup plus en PF pure, donc je pense que les fonctions elles-mêmes sont généralement plus petites et plus nombreuses. Je pourrais être loin cependant. Je n'ai pas beaucoup d'expérience à travailler sur de vrais projets avec Haskell et le gang.
Darwin
4
Je pense que la réponse est "Ne nommez pas vos variables" deserializedArray ""?
whatsisname

Réponses:

10

Je suis d'accord que la façon dont les fonctions sont souvent utilisées peut être une partie déroutante de l'écriture de code, et en particulier de la lecture de code.

La réponse à ce problème dépend en partie de la langue. Comme vous l'avez mentionné, C # a nommé des paramètres. La solution d'Objective-C à ce problème implique des noms de méthode plus descriptifs. Par exemple, stringByReplacingOccurrencesOfString:withString:est une méthode avec des paramètres clairs.

Dans Groovy, certaines fonctions prennent des cartes, permettant une syntaxe comme celle-ci:

restClient.post(path: 'path/to/somewhere',
            body: requestBody,
            requestContentType: 'application/json')

En général, vous pouvez résoudre ce problème en limitant le nombre de paramètres que vous passez à une fonction. Je pense que 2-3 est une bonne limite. S'il apparaît qu'une fonction a besoin de plus de paramètres, cela me fait repenser le design. Mais cela peut être plus difficile à répondre de manière générale. Parfois, vous essayez d'en faire trop dans une fonction. Parfois, il est logique d'envisager une classe pour stocker vos paramètres. De plus, dans la pratique, je trouve souvent que les fonctions qui prennent un grand nombre de paramètres en ont normalement beaucoup comme optionnelles.

Même dans un langage comme Objective-C, il est logique de limiter le nombre de paramètres. L'une des raisons est que de nombreux paramètres sont facultatifs. Pour un exemple, voir rangeOfString: et ses variations dans NSString .

Un modèle que j'utilise souvent en Java est d'utiliser une classe de style fluide comme paramètre. Par exemple:

something.draw(new Box().withHeight(5).withWidth(20))

Cela utilise une classe comme paramètre et avec une classe de style fluide, le code est facilement lisible.

L'extrait Java ci-dessus aide également lorsque l'ordre des paramètres peut ne pas être aussi évident. Nous supposons normalement avec des coordonnées que X vient avant Y. Et je vois normalement la hauteur avant la largeur comme une convention, mais ce n'est toujours pas très clair ( something.draw(5, 20)).

J'ai également vu certaines fonctions comme, drawWithHeightAndWidth(5, 20)mais même celles-ci ne peuvent pas prendre trop de paramètres, ou vous commenceriez à perdre en lisibilité.

David V
la source
2
L'ordre, si vous continuez sur l'exemple Java, peut en effet être très délicat. Par exemple, comparez les constructeurs suivants de awt: Dimension(int width, int height)et GridLayout(int rows, int cols)(le nombre de lignes est la hauteur, ce GridLayoutqui signifie a la hauteur en premier et Dimensionla largeur).
Pierre Arlaud
1
De telles incohérences ont également été très critiquées avec PHP ( eev.ee/blog/2012/04/09/php-a-fractal-of-bad-design ), par exemple: array_filter($input, $callback)versus array_map($callback, $input), strpos($haystack, $needle)versusarray_search($needle, $haystack)
Pierre Arlaud
12

La plupart du temps, il est résolu par une bonne dénomination des fonctions, des paramètres et des arguments. Vous avez déjà exploré cela et constaté qu'il présentait cependant des lacunes. La plupart de ces lacunes sont atténuées en gardant les fonctions petites, avec un petit nombre de paramètres, à la fois dans le contexte appelant et dans le contexte appelé. Votre exemple particulier est problématique car la fonction que vous appelez essaie de faire plusieurs choses à la fois: spécifier un rectangle de base, spécifier une région de découpage, le dessiner et le remplir avec une couleur spécifique.

C'est un peu comme essayer d'écrire une phrase en utilisant uniquement les adjectifs. Mettez plus de verbes (appels de fonction), créez un sujet (objet) pour votre phrase, et c'est plus facile à lire:

rect.clip(clipRect).fill(color)

Même si clipRectet coloront des noms terribles (et ils ne devraient pas), vous pouvez toujours discerner leurs types à partir du contexte.

Votre exemple désérialisé est problématique car le contexte d'appel essaie d'en faire trop à la fois: désérialiser et dessiner quelque chose. Vous devez attribuer des noms qui ont du sens et séparer clairement les deux responsabilités. Au minimum, quelque chose comme ça:

(rect, clipRect, color) = deserializeClippedRect()
rect.clip(clipRect).fill(color)

Beaucoup de problèmes de lisibilité sont causés par la tentative d'être trop concis, en sautant les étapes intermédiaires nécessaires aux humains pour discerner le contexte et la sémantique.

Karl Bielefeldt
la source
1
J'aime l'idée d'enchaîner plusieurs appels de fonction pour clarifier le sens, mais n'est-ce pas juste danser autour du problème? C'est essentiellement "Je veux écrire une phrase, mais la langue que je poursuis ne me permet pas de ne pouvoir utiliser que l'équivalent le plus proche"
Darwin
@ Darwin IMHO, ce n'est pas comme si cela pouvait être amélioré en rendant le langage de programmation plus semblable au langage naturel. Les langues naturelles sont très ambiguës et nous ne pouvons les comprendre que dans leur contexte et ne pouvons en fait jamais être sûrs. La chaîne des appels de fonction est bien meilleure car chaque terme (idéalement) a de la documentation et des sources disponibles et nous avons des parenthèses et des points qui rendent la structure claire.
maaartinus
3

En pratique, il est résolu par une meilleure conception. Il est exceptionnellement rare que des fonctions bien écrites prennent plus de 2 entrées, et quand cela se produit, il est rare que ces nombreuses entrées ne puissent pas être agrégées dans un ensemble cohérent. Cela facilite la décomposition des fonctions ou l'agrégation des paramètres, de sorte que vous ne faites pas trop fonctionner une fonction. Une fois qu'il a deux entrées, il devient facile à nommer et beaucoup plus clair sur quelle entrée est laquelle.

Mon langage de jouets avait le concept de phrases pour y faire face, et d'autres langages de programmation plus axés sur le langage naturel ont eu d'autres approches pour y faire face, mais ils ont tous tendance à avoir d'autres inconvénients. De plus, même les phrases ne sont guère plus qu'une belle syntaxe pour que les fonctions aient de meilleurs noms. Il sera toujours difficile de faire un bon nom de fonction quand il faudra un tas d'entrées.

Telastyn
la source
Les phrases semblent vraiment un pas en avant. Je sais que certaines langues ont des capacités similaires mais c'est loin d'être répandu. Sans oublier, avec toute la haine des macros provenant de puristes C (++) qui n'ont jamais utilisé les macros correctement, nous pourrions ne jamais avoir de fonctionnalités comme celles-ci dans les langages populaires.
Darwin
Bienvenue sur le sujet général des langages spécifiques au domaine , quelque chose que j'aimerais vraiment que plus de gens comprennent l'avantage de ... (+1)
Izkata
2

En Javascript (ou ECMAScript ), par exemple, de nombreux programmeurs se sont habitués à

passer des paramètres comme un ensemble de propriétés d'objet nommé dans un seul objet anonyme.

Et comme une pratique de programmation, il est passé des programmeurs à leurs bibliothèques et de là à d'autres programmeurs qui ont commencé à l'aimer et à l'utiliser et à écrire d'autres bibliothèques, etc.

Exemple

Au lieu d'appeler

function drawRectangleClipped (rectToDraw, fillColor, clippingRect)

comme ça:

drawRectangleClipped(deserializedArray[0], deserializedArray[1], deserializedArray[2])

, qui est un style valide et correct, vous appelez

function drawRectangleClipped (params)

comme ça:

drawRectangleClipped({
    rectToDraw: deserializedArray[0], 
    fillColor: deserializedArray[1], 
    clippingRect: deserializedArray[2]
})

, ce qui est valide et correct et agréable par rapport à votre question.

Bien sûr, il doit y avoir des conditions appropriées pour cela - en Javascript, c'est beaucoup plus viable que dans, disons, C. En javascript, cela a même donné naissance à une notation structurelle désormais largement utilisée qui est devenue populaire en tant qu'équivalent plus léger de XML. Cela s'appelle JSON (vous en avez peut-être déjà entendu parler).

Pavel
la source
Je ne connais pas assez ce langage pour vérifier la syntaxe mais, dans l'ensemble, j'aime ce post. Semble assez élégant. +1
IT Alex
Très souvent, cela est combiné avec des arguments normaux, c'est-à-dire qu'il y a 1 à 3 arguments suivis params(contenant souvent des arguments facultatifs et souvent lui-même facultatif) comme, par exemple, cette fonction . Cela rend les fonctions avec de nombreux arguments assez faciles à saisir (dans mon exemple, il y a 2 arguments obligatoires et 6 options).
maaartinus
0

Vous devez alors utiliser objective-C, voici une définition de fonction:

- (id)performSelector:(SEL)aSelector withObject:(id)anObject withObject:(id)anotherObject

Et ici, il est utilisé:

[someObject performSelector:someSelector withObject:someObject2 withObject:someObject3];

Je pense que ruby ​​a des constructions similaires et vous pouvez les simuler dans d'autres langues avec des listes de valeurs-clés.

Pour les fonctions complexes en Java, j'aime définir des variables factices dans le libellé des fonctions. Pour votre exemple gauche-droite:

Rectangle referenceRectangle = leftRectangle;
Rectangle targetRectangle = rightRectangle;
doSomeWeirdStuffWithRectangles(referenceRectangle, targetRectangle);

Cela ressemble à plus de codage, mais vous pouvez par exemple utiliser leftRectangle puis refactoriser le code plus tard avec "Extraire la variable locale" si vous pensez que cela ne sera pas compréhensible pour un futur responsable du code, qui pourrait ou non être vous.

awsm
la source
À propos de cet exemple Java, j'ai écrit dans la question pourquoi je pense que ce n'est pas une bonne solution. Qu'est ce que tu penses de ça?
Darwin
0

Mon approche consiste à créer des variables locales temporaires - mais pas seulement à les appeler LeftRectangeet RightRectangle. J'utilise plutôt des noms un peu plus longs pour donner plus de sens. J'essaie souvent de différencier les noms autant que possible, par exemple de ne pas les appeler tous les deux something_rectangle, si leur rôle n'est pas très symétrique.

Exemple (C ++):

auto& connector_source = deserializedArray[0]; 
auto& connector_target = deserializedArray[1]; 
auto& bounding_box = deserializedArray[2]; 
DoWeirdThing(connector_source, connector_target, bounding_box)

et je pourrais même écrire une fonction ou un modèle d'emballage à une ligne:

template <typename T1, typename T2, typename T3>
draw_bounded_connector(
    T1& connector_source, T2& connector_target,const T3& bounding_box) 
{
    DoWeirdThing(connector_source, connector_target, bounding_box)
}

(ignorez les esperluettes si vous ne connaissez pas C ++).

Si la fonction fait plusieurs choses étranges sans bonne description - alors elle doit probablement être refactorisée!

einpoklum
la source