Pourquoi ne pas annoter les paramètres des fonctions?

28

Pour répondre à cette question, supposons que le coût de l'ambiguïté dans l'esprit d'un programmeur soit beaucoup plus cher que quelques frappes supplémentaires.

Compte tenu de cela, pourquoi laisserais-je mes coéquipiers s'en tirer sans annoter leurs paramètres de fonction? Prenez le code suivant comme exemple de ce qui pourrait être un morceau de code beaucoup plus complexe:

let foo x y = x + y

Maintenant, un examen rapide de l'info-bulle vous montrera que F # a déterminé que vous vouliez que x et y soient des entiers. Si c'est ce que vous vouliez, alors tout va bien. Mais je ne sais pas si c'est ce que vous vouliez. Et si vous aviez créé ce code pour concaténer deux chaînes ensemble? Et si je pense que vous vouliez probablement ajouter des doubles? Ou si je ne veux pas avoir à passer la souris sur chaque paramètre de fonction pour déterminer son type?

Prenons maintenant ceci comme exemple:

let foo x y = "result: " + x + y

F # suppose maintenant que vous avez probablement l'intention de concaténer des chaînes, donc x et y sont définis comme des chaînes. Cependant, en tant que pauvre schmuck qui gère votre code, je pourrais regarder cela et me demander si vous aviez peut-être l'intention d'ajouter x et y (ints) ensemble, puis d'ajouter le résultat à une chaîne à des fins d'interface utilisateur.

Certes, pour de tels exemples simples, on pourrait laisser tomber, mais pourquoi ne pas appliquer une politique d'annotation de type explicite?

let foo (x:string) (y:string) = "result: " + x + y

Quel mal y a-t-il à être sans ambiguïté? Bien sûr, un programmeur pourrait choisir les mauvais types pour ce qu'il essaie de faire, mais au moins je sais qu'ils le voulaient, que ce n'était pas seulement une erreur.

C'est une question sérieuse ... Je suis encore très nouveau sur F # et je trace la voie pour mon entreprise. Les normes que j'adopterai seront probablement la base de tout futur codage F #, intégré dans le copier-coller sans fin qui, j'en suis sûr, imprègnera la culture pour les années à venir.

Donc ... y a-t-il quelque chose de spécial dans l'inférence de type F # qui en fait une fonctionnalité précieuse à conserver, à annoter uniquement lorsque cela est nécessaire? Ou les experts F # ont-ils l'habitude d'annoter leurs paramètres pour des applications non triviales?

JDB
la source
La version annotée est moins générale. C'est pour la même raison que vous utilisez de préférence IEnumerable <> dans les signatures et non SortedSet <>.
Patrick
2
@Patrick - Non, ce n'est pas le cas, car F # déduit le type de chaîne. Je n'ai pas changé la signature de la fonction, je l'ai seulement rendue explicite.
JDB
1
@Patrick - Et même si c'était vrai, pouvez-vous expliquer pourquoi c'est une mauvaise chose? Peut-être que la signature devrait être moins générale, si c'est ce que le programmeur avait prévu. Peut-être que la signature plus générale cause des problèmes, mais je ne suis pas sûr de la spécificité du programmeur sans une grande quantité de recherches.
JDB
1
Je pense qu'il est juste de dire que, dans les langages fonctionnels, vous préférez la généralité et annotez dans le cas plus spécifique; par exemple, lorsque vous souhaitez de meilleures performances et que vous pouvez les obtenir en utilisant l'indication de type. L'utilisation de fonctions annotées partout vous obligerait à écrire des surcharges pour chaque type spécifique, ce qui peut ne pas être garanti dans le cas général.
Robert Harvey

Réponses:

30

Je n'utilise pas F #, mais dans Haskell, il est considéré comme une bonne forme pour annoter (au moins) les définitions de niveau supérieur, et parfois les définitions locales, même si le langage a une inférence de type omniprésente. Ceci pour plusieurs raisons:

  • Lecture
    Lorsque vous voulez savoir comment utiliser une fonction, il est extrêmement utile d'avoir la signature de type disponible. Vous pouvez simplement le lire, plutôt que d'essayer de le déduire vous-même ou de compter sur des outils pour le faire pour vous.

  • Refactorisation
    Lorsque vous souhaitez modifier une fonction, le fait d'avoir une signature explicite vous donne l'assurance que vos transformations préservent l'intention du code d'origine. Dans un langage déduit par type, vous pouvez constater que le code hautement polymorphe vérifie la typologie mais ne fait pas ce que vous vouliez. La signature de type est une «barrière» qui concrétise les informations de type au niveau d'une interface.

  • Performances
    Dans Haskell, le type inféré d'une fonction peut être surchargé (par le biais de classes de type), ce qui peut impliquer une répartition d'exécution. Pour les types numériques, le type par défaut est un entier à précision arbitraire. Si vous n'avez pas besoin de la généralité complète de ces fonctionnalités, vous pouvez améliorer les performances en spécialisant la fonction au type spécifique dont vous avez besoin.

Pour les définitions locales, les letvariables liées et les paramètres formels des lambdas, je trouve que les signatures de type coûtent généralement plus cher en code que la valeur qu'elles ajouteraient. Donc, dans la révision du code, je vous suggère d'insister sur les signatures pour les définitions de niveau supérieur et de simplement demander des annotations judicieuses ailleurs.

Jon Purdy
la source
3
Cela ressemble à une réponse très raisonnable et bien équilibrée.
JDB
1
Je suis d'accord que la définition des types explicitement définis peut aider lors de la refactorisation, mais je trouve que les tests unitaires peuvent également résoudre ce problème et parce que je crois que le test unitaire doit être utilisé avec la programmation fonctionnelle pour garantir qu'une fonction fonctionne comme prévu avant d'utiliser le fonction avec une autre fonction, cela me permet de laisser les types hors de la définition et d'avoir la certitude que si je fais un changement de rupture, il sera pris. Exemple
Guy Coder
4
Dans Haskell, il est considéré comme correct d'annoter vos fonctions, mais je pense que F # et OCaml idiomatiques ont tendance à omettre les annotations, sauf si cela est nécessaire pour lever l'ambiguïté. Cela ne veut pas dire que c'est nuisible (bien que la syntaxe d'annotation en F # soit plus laide qu'en Haskell).
KChaloux
4
@KChaloux Dans OCaml, il y a déjà des annotations de type dans le .mlifichier interface ( ) (si vous en écrivez une, ce que vous êtes fortement encouragé. Les annotations de type ont tendance à être omises des définitions car elles seraient redondantes avec l'interface.
Gilles ' SO- arrête d'être maléfique '27
1
@Gilles @KChaloux en effet, même dans les .fsifichiers F # signature ( ), avec une mise en garde: F # n'utilise pas de fichiers de signature pour l'inférence de type, donc si quelque chose dans l'implémentation est ambigu, vous devrez toujours l' annoter à nouveau. books.google.ca/…
Yawar Amin
1

Jon a donné une réponse raisonnable que je ne répéterai pas ici. Je vais cependant vous montrer une option qui pourrait satisfaire vos besoins et dans le processus, vous verrez un type de réponse différent d'un oui / non.

Dernièrement, j'ai travaillé avec l'analyse à l'aide de combinateurs d'analyseurs. Si vous connaissez l'analyse, vous savez que vous utilisez généralement un lexer dans la première phase et un analyseur dans la deuxième phase. Le lexer convertit le texte en jetons et l'analyseur convertit les jetons en AST. Maintenant que F # est un langage fonctionnel et que les combinateurs sont conçus pour être combinés, les combinateurs d'analyseur sont conçus pour utiliser les mêmes fonctions à la fois dans le lexeur et dans l'analyseur, mais si vous définissez le type des fonctions de combinateur d'analyseur, vous ne pouvez les utiliser que pour lex ou pour analyser et pas les deux.

Par exemple:

/// Parser that requires a specific item.

// a (tok : 'a) : ('a list -> 'a * 'a list)                     // generic
// a (tok : string) : (string list -> string * string list)     // string
// a (tok : token)  : (token list  -> token  * token list)      // token

ou

/// Parses iterated left-associated binary operator.


// leftbin (prs : 'a -> 'b * 'c) (sep : 'c -> 'd * 'a) (cons : 'd -> 'b -> 'b -> 'b) (err : string) : ('a -> 'b * 'c)                                                                                    // generic
// leftbin (prs : string list -> string * string list) (sep : string list -> string * string list) (cons : string -> string -> string -> string) (err : string) : (string list -> string * string list)  // string
// leftbin (prs : token list  -> token  * token list)  (sep : token list  -> token  * token list)  (cons : token  -> token  -> token  -> token)  (err : string) : (token list  -> token  * token list)   // token

Étant donné que le code est protégé par des droits d'auteur, je ne l'inclurai pas ici, mais il est disponible sur Github . Ne le copiez pas ici.

Pour que les fonctions fonctionnent, elles doivent être laissées avec les paramètres génériques, mais j'inclus des commentaires qui montrent les types inférés en fonction de l'utilisation des fonctions. Cela facilite la compréhension de la fonction de maintenance tout en laissant la fonction générique à l'usage.

Guy Coder
la source
1
Pourquoi n'écririez-vous pas la version générique dans la fonction? Ça ne marcherait pas?
svick
@svick Je ne comprends pas votre question. Voulez-vous dire que j'ai omis les types de la définition de la fonction, mais aurais pu ajouter les types génériques aux définitions de fonction puisque les types génériques ne changeraient pas la signification de la fonction? Si c'est le cas, la raison est plutôt une préférence personnelle. Quand j'ai commencé avec F #, je les ai ajoutés. Lorsque j'ai commencé à travailler avec des utilisateurs plus avancés de F #, ils préféraient les laisser en tant que commentaires, car il était plus facile de modifier le code sans les signatures. ...
Guy Coder
À long terme, la façon dont je les montre comme commentaires fonctionnait pour tout le monde une fois que le code fonctionnait. Quand je commence à écrire une fonction, je mets les types. Une fois que la fonction fonctionne, je déplace la signature de type vers un commentaire et supprime autant de types que possible. Parfois, avec F #, vous devez laisser le type pour faciliter l'inférence. Pendant un certain temps, j'expérimentais avec la création du commentaire avec let et = afin que vous puissiez décommenter la ligne pour le test, puis la commenter à nouveau une fois terminé, mais ils avaient l'air idiot et ajouter un let et = n'est pas si difficile.
Guy Coder
Si ces commentaires répondent à ce que vous demandez, faites-le moi savoir afin que je puisse les déplacer dans la réponse.
Guy Coder
2
Ainsi, lorsque vous modifiez le code, vous ne modifiez pas les commentaires, ce qui entraîne une documentation obsolète? Cela ne me semble pas un avantage. Et je ne vois pas vraiment comment un commentaire en désordre est meilleur qu'un code en désordre. Pour moi, il semble que cette approche combine les inconvénients des deux options.
svick
-8

Le nombre de bugs est directement proportionnel au nombre de caractères dans un programme!

Ceci est généralement indiqué comme le nombre de bogues étant proportionnel aux lignes de code. Mais avoir moins de lignes avec la même quantité de code donne la même quantité de bogues.

Donc, même s'il est agréable d'être explicite, tout paramètre supplémentaire que vous saisissez peut entraîner des bogues.

James Anderson
la source
5
«Le nombre de bogues est directement proportionnel au nombre de caractères dans un programme!» Alors, devrions-nous tous passer à APL ? Être trop laconique est un problème, tout comme être trop verbeux.
svick
5
" Pour répondre à cette question, supposons que le coût de l'ambiguïté dans l'esprit d'un programmeur soit beaucoup plus cher que quelques frappes supplémentaires. "
JDB