web-dev-qa-db-fra.com

Pourquoi les opérateurs définis par l'utilisateur ne sont-ils pas plus courants?

Une caractéristique qui me manque dans les langages fonctionnels est l'idée que les opérateurs ne sont que des fonctions, donc l'ajout d'un opérateur personnalisé est souvent aussi simple que l'ajout d'une fonction. De nombreux langages procéduraux autorisent les surcharges d'opérateurs, donc dans un certain sens, les opérateurs sont toujours des fonctions (c'est très vrai dans D où l'opérateur est passé sous forme de chaîne dans un modèle paramètre).

Il semble que lorsque la surcharge d'opérateur est autorisée, il est souvent trivial d'ajouter des opérateurs personnalisés supplémentaires. J'ai trouvé ce billet de blog , qui soutient que les opérateurs personnalisés ne fonctionnent pas bien avec la notation infixe en raison des règles de priorité, mais l'auteur donne plusieurs solutions à ce problème.

J'ai regardé autour de moi et je n'ai trouvé aucun langage procédural prenant en charge les opérateurs personnalisés dans le langage. Il existe des hacks (tels que des macros en C++), mais ce n'est pas la même chose que la prise en charge des langues.

Puisque cette fonctionnalité est assez triviale à implémenter, pourquoi n'est-elle pas plus courante?

Je comprends que cela peut conduire à un code laid, mais cela n'a pas empêché les concepteurs de langage d'ajouter des fonctionnalités utiles qui peuvent être facilement utilisées par le passé (macros, opérateur ternaire, pointeurs dangereux).

Cas d'utilisation réels:

  • Implémenter des opérateurs manquants (par exemple, Lua n'a pas d'opérateurs au niveau du bit)
  • Mimic D's ~ (concaténation de tableau)
  • DSL
  • Utilisation | comme sucre de syntaxe de style pipe Unix (en utilisant des coroutines/générateurs)

Je suis également intéressé par les langues qui autorisent les opérateurs personnalisés, mais je suis plus intéressé par pourquoi il a été exclu. J'ai pensé à créer un langage de script pour ajouter des opérateurs définis par l'utilisateur, mais je me suis arrêté quand j'ai réalisé que je ne l'avais vu nulle part, donc il y a probablement une bonne raison pour laquelle les concepteurs de langage plus intelligents que moi ne l'ont pas autorisé.

98
beatgammit

Il existe deux écoles de pensée diamétralement opposées dans la conception de langages de programmation. L'un est que les programmeurs écrivent un meilleur code avec moins de restrictions, et l'autre est qu'ils écrivent un meilleur code avec plus de restrictions. À mon avis, la réalité est que les bons programmeurs expérimentés prospèrent avec moins de restrictions, mais que les restrictions peuvent bénéficier à la qualité du code des débutants.

Les opérateurs définis par l'utilisateur peuvent créer un code très élégant entre des mains expérimentées et un code absolument horrible par un débutant. Donc, que votre langue les inclue ou non dépend de l'école de pensée de votre concepteur de langue.

136
Karl Bielefeldt

Étant donné le choix entre concaténer des tableaux avec ~ ou avec "myArray.Concat (secondArray)", je préférerais probablement ce dernier. Pourquoi? Parce que ~ est un caractère complètement dénué de sens qui n'a que sa signification - celle de la concaténation de tableaux - donnée dans le projet spécifique où il a été écrit.

Fondamentalement, comme vous l'avez dit, les opérateurs ne sont pas différents des méthodes. Mais alors que les méthodes peuvent recevoir des noms lisibles et compréhensibles qui contribuent à la compréhension du flux de code, les opérateurs sont opaques et situationnels.

C'est pourquoi je n'aime pas non plus l'opérateur . De PHP (concaténation de chaînes) ou la plupart des opérateurs dans Haskell ou OCaml, bien que dans ce cas, certaines normes universellement acceptées émergent pour les langages fonctionnels.

83

Étant donné que cette fonctionnalité est assez simple à implémenter, pourquoi n'est-elle pas plus courante?

Votre prémisse est fausse. Ce n'est pas "assez banal à mettre en œuvre". En fait, cela apporte un sac de problèmes.

Jetons un œil aux "solutions" suggérées dans le billet:

  • Pas de priorité. L'auteur lui-même dit: "Ne pas utiliser de règles de priorité n'est tout simplement pas une option."
  • analyse sémantique. Comme le dit l'article, cela nécessiterait que le compilateur ait beaucoup de connaissances sémantiques. L'article ne propose pas vraiment de solution pour cela et laissez-moi vous dire que ce n'est tout simplement pas trivial. Les compilateurs sont conçus comme un compromis entre puissance et complexité. En particulier, l'auteur mentionne une étape de pré-analyse pour collecter les informations pertinentes, mais la pré-analyse est inefficace et les compilateurs s'efforcent de minimiser les passes d'analyse.
  • Aucun opérateur d'infixe personnalisé. Eh bien, ce n'est pas une solution.
  • Solution hybride. Cette solution comporte de nombreux (mais pas tous) des inconvénients de l'analyse consciente sémantique. En particulier, étant donné que le compilateur doit traiter les jetons inconnus comme représentant potentiellement des opérateurs personnalisés, il ne peut souvent pas produire de messages d'erreur significatifs. Il peut également nécessiter la définition dudit opérateur pour procéder à l'analyse (pour collecter les informations de type, etc.), nécessitant à nouveau une passe d'analyse supplémentaire.

Dans l'ensemble, il s'agit d'une fonctionnalité coûteuse à mettre en œuvre, à la fois en termes de complexité de l'analyseur et en termes de performances, et il n'est pas certain qu'elle apporterait de nombreux avantages. Bien sûr, il y a certains avantages à la capacité de définir de nouveaux opérateurs mais même ceux-ci sont litigieux (il suffit de regarder les autres réponses en faisant valoir que le fait d'avoir de nouveaux opérateurs n'est pas ' t une bonne chose).

71
Konrad Rudolph

Ignorons pour le moment l'argument "les opérateurs sont abusés pour nuire à la lisibilité" et concentrons-nous sur les implications de la conception du langage.

Les opérateurs Infix ont plus de problèmes que de simples règles de priorité (bien que pour être franc, le lien auquel vous faites référence banalise l'impact de cette décision de conception). La première est la résolution des conflits: que se passe-t-il lorsque vous définissez a.operator+(b) et b.operator+(a)? Préférer l'un sur l'autre conduit à rompre la propriété commutative attendue de cet opérateur. Lancer une erreur peut conduire à des modules qui fonctionneraient autrement se casser une fois ensemble. Que se passe-t-il lorsque vous commencez à lancer des types dérivés dans le mix?

Le fait est que les opérateurs ne sont pas seulement des fonctions. Les fonctions sont autonomes ou appartiennent à leur classe, ce qui donne une préférence claire sur le paramètre (le cas échéant) auquel appartient la répartition polymorphe.

Et cela ignore les divers problèmes d'emballage et de résolution qui se posent aux opérateurs. La raison pour laquelle les concepteurs de langues limitent (dans l'ensemble) la définition de l'opérateur d'infixe est parce qu'elle crée une pile de problèmes pour la langue tout en offrant des avantages discutables.

Et franchement, parce qu'ils sont pas triviaux à mettre en œuvre.

25
Telastyn

Je pense que vous seriez surpris de la fréquence à laquelle les surcharges d'opérateur sont mises en œuvre sous une forme ou une autre. Mais ils ne sont pas couramment utilisés dans de nombreuses communautés.

Pourquoi utiliser ~ pour concaténer un tableau? Pourquoi pas tilisez << comme Ruby ne ? Parce que les programmeurs avec lesquels vous travaillez ne sont probablement pas Ruby programmeurs. Ou programmeurs D). Alors, que font-ils lorsqu'ils rencontrent votre code? Ils doivent aller chercher ce que signifie le symbole.

Je travaillais avec un très bon développeur C # qui avait également un goût pour les langages fonctionnels. À l'improviste, il a commencé à introduire monades en C # au moyen de méthodes d'extension et en utilisant la terminologie standard des monades. Personne ne pouvait contester que certains de ses codes étaient plus tordus et encore plus lisibles une fois que vous saviez ce que cela signifiait, mais cela signifiait que tout le monde devait apprendre la terminologie monade avant que le code ait du sens .

Assez juste, tu crois? Ce n'était qu'une petite équipe. Personnellement, je ne suis pas d'accord. Chaque nouveau développeur était destiné à être confondu par cette terminologie. Avons-nous pas assez de problèmes pour apprendre un nouveau domaine?

D'un autre côté, j'utiliserai volontiers le ?? operator en C # parce que je m'attends à ce que les autres développeurs C # sachent ce que c'est, mais je ne le surchargerais pas dans un langage qui ne le supportait pas par défaut.

19
pdr

Je peux penser à quelques raisons:

  • Ils ne sont pas triviaux à implémenter - autoriser des opérateurs personnalisés arbitraires peut rendre votre compilateur beaucoup plus complexe, surtout si vous autorisez les règles de priorité, de fixité et d'arité définies par l'utilisateur. Si la simplicité est une vertu, la surcharge de l'opérateur vous éloigne d'une bonne conception du langage.
  • Ils abusés - principalement par des codeurs qui pensent qu'il est "cool" de redéfinir les opérateurs et de commencer à les redéfinir pour toutes sortes de classes personnalisées. Avant longtemps, votre code est jonché d'une charge de symboles personnalisés que personne d'autre ne peut lire ou comprendre car les opérateurs ne suivent pas les règles conventionnelles bien comprises. Je n'achète pas l'argument "DSL", à moins que votre DSL ne soit un sous-ensemble de mathématiques :-)
  • Ils nuisent à la lisibilité et à la maintenabilité - si les opérateurs sont régulièrement remplacés, il peut devenir difficile de repérer lorsque cette fonctionnalité est utilisée, et les codeurs sont obligés de se demander continuellement ce que fait un opérateur. Il est préférable de donner des noms de fonction significatifs. Taper quelques caractères supplémentaires n'est pas cher, les problèmes de maintenance à long terme sont coûteux.
  • Ils peuvent briser les attentes de performances implicites. Par exemple, je m'attendrais normalement à ce que la recherche d'un élément dans un tableau soit O(1). Mais avec la surcharge de l'opérateur, someobject[i] Pourrait facilement être une opération O(n) selon l'implémentation de l'opérateur d'indexation.

En réalité, il y a très peu de cas où la surcharge de l'opérateur a des utilisations justifiables par rapport à la simple utilisation de fonctions régulières. Un exemple légitime pourrait être la conception d'une classe de nombres complexes à l'usage des mathématiciens, qui comprennent les façons bien comprises dont les opérateurs mathématiques sont définis pour les nombres complexes. Mais ce n'est vraiment pas un cas très courant.

Quelques cas intéressants à considérer:

  • Lisps: en général, ne fait aucune distinction entre les opérateurs et les fonctions - + Est juste une fonction régulière. Vous pouvez définir les fonctions comme vous le souhaitez (généralement, il existe un moyen de les définir dans des espaces de noms séparés pour éviter tout conflit avec le + Intégré), y compris les opérateurs. Mais il y a une tendance culturelle à utiliser des noms de fonction significatifs, donc cela ne se fait pas beaucoup abuser. De plus, dans la notation de préfixe LISP, elle a tendance à être utilisée exclusivement, il y a donc moins de valeur dans le "sucre syntaxique" fourni par les surcharges d'opérateurs.
  • Java - interdit la surcharge de l'opérateur. C'est parfois ennuyeux (pour des choses comme le cas des nombres complexes), mais en moyenne, c'est probablement la bonne décision de conception pour Java qui est conçu comme un simple, à usage général OOP langauge. Java est en fait assez facile à maintenir pour les développeurs peu/moyennement qualifiés en raison de cette simplicité.
  • C++ a une surcharge d'opérateur très sophistiquée. Parfois, cela est abusé (cout << "Hello World!" N'importe qui?) Mais l'approche est logique étant donné le positionnement de C++ en tant que langage complexe qui permet une programmation de haut niveau tout en vous permettant de vous rapprocher très près du métal pour les performances, vous pouvez donc par exemple écrire une classe de nombres complexes qui se comporte exactement comme vous le souhaitez sans compromettre les performances. Il est entendu que c'est votre propre responsabilité si vous vous tirez une balle dans le pied.
11
mikera

Étant donné que cette fonctionnalité est assez simple à implémenter, pourquoi n'est-elle pas plus courante?

Il n'est pas trivial à implémenter (sauf si implémenté trivialement). Il n'obtient pas non plus grand-chose, même s'il est mis en œuvre idéalement: les gains de lisibilité de la lacune sont compensés par les pertes de lisibilité dues à la méconnaissance et à l'opacité. En bref, c'est rare car cela ne vaut généralement pas le temps des développeurs ou des utilisateurs.

Cela dit, je peux penser à trois langues qui le font, et ils le font de différentes manières:

  • Racket, un schéma, quand ce n'est pas tout, S-expression-y vous permet et attend que vous écriviez ce qui équivaut à l'analyseur pour la syntaxe que vous souhaitez étendre (et fournit des crochets utiles pour rendre cela traitable).
  • Haskell, un langage de programmation purement fonctionnel, permet de définir tout opérateur qui se compose uniquement de ponctuation, et vous permet de fournir un niveau de fixité (10 disponibles) et une associativité. Des opérateurs ternaires, etc. peuvent être créés à partir d'opérateurs binaires et de fonctions d'ordre supérieur.
  • Agda, un langage de programmation typé de manière dépendante, est extrêmement flexible avec les opérateurs (papier ici ) permettant à la fois si-alors et si-alors-autre d'être définis comme opérateurs dans le même programme, mais son lexer, analyseur , et l'évaluateur sont tous fortement couplés en conséquence.
8
Alex R

L'une des principales raisons pour lesquelles les opérateurs personnalisés sont découragés est que n'importe quel opérateur peut/peut faire n'importe quoi.

Par exemple, la surcharge de décalage gauche de cstream est très critiquée.

Lorsqu'un langage autorise des surcharges d'opérateur, il est généralement encouragé de garder le comportement de l'opérateur similaire au comportement de base pour éviter toute confusion.

Les opérateurs définis par l'utilisateur rendent également l'analyse beaucoup plus difficile, surtout lorsqu'il existe également des règles de préférence personnalisées.

7
ratchet freak

Nous n'utilisons pas d'opérateurs définis par l'utilisateur pour la même raison que nous n'utilisons pas de mots définis par l'utilisateur. Personne n'appellerait leur fonction "sworp". La seule façon de transmettre votre pensée à une autre personne est d'utiliser un langage partagé. Et cela signifie que les mots et les signes (opérateurs) doivent être connus de la société pour laquelle vous écrivez votre code.

Par conséquent, les opérateurs que vous voyez utilisés dans les langages de programmation sont ceux que nous avons appris à l'école (arithmétique) ou ceux qui ont été établis dans la communauté de programmation, comme par exemple les opérateurs booléens.

4
Vagif Verdi

En ce qui concerne les langages qui prennent en charge une telle surcharge: Scala le fait, en fait d'une manière beaucoup plus propre et meilleure peut C++. La plupart des caractères peuvent être utilisés dans les noms de fonction, vous pouvez donc définir des opérateurs comme! + * = ++, si vous le souhaitez. Il existe un support intégré pour infix (pour toutes les fonctions prenant un seul argument). Je pense que vous pouvez également définir l'associativité de ces fonctions. Vous ne pouvez cependant pas définir la priorité (uniquement avec trucs laids, voir ici ).

4
kutschkem

Une chose qui n'a pas encore été mentionnée est le cas de Smalltalk, où tout (y compris les opérateurs) est un message envoyé. Les "opérateurs" comme +, | Et ainsi de suite sont en fait des méthodes unaires.

Toutes les méthodes peuvent être remplacées, donc a + b Signifie addition entière si a et b sont tous les deux des entiers, et signifie addition vectorielle si elles sont toutes les deux OrderedCollections.

Il n'y a pas de règles de priorité, car ce ne sont que des appels de méthode. Cela a une implication importante pour la notation mathématique standard: 3 + 4 * 5 Signifie (3 + 4) * 5, Pas 3 + (4 * 5).

(Il s'agit d'une pierre d'achoppement majeure pour les débutants de Smalltalk. La rupture des règles mathématiques supprime un cas spécial, de sorte que toute l'évaluation du code se déroule uniformément de gauche à droite, ce qui rend le langage beaucoup plus simple.)

4
Frank Shearar

Vous vous battez contre deux choses ici:

  1. Pourquoi les opérateurs existent-ils en premier lieu dans les langues?
  2. Quelle est la vertu des opérateurs sur les fonctions/méthodes?

Dans la plupart des langues, les opérateurs ne sont pas vraiment implémentés comme de simples fonctions. Ils peuvent avoir un échafaudage de fonction, mais le compilateur/runtime est explicitement conscient de leur signification sémantique et comment les traduire efficacement en code machine. Cela est beaucoup plus vrai même par rapport aux fonctions intégrées (c'est pourquoi la plupart des implémentations n'incluent pas non plus toutes les surcharges d'appel de fonction dans leur implémentation). La plupart des opérateurs sont des abstractions de niveau supérieur sur les instructions primitives trouvées dans les CPU (ce qui explique en partie pourquoi la plupart des opérateurs sont arithmétiques, booléens ou au niveau du bit). Vous pouvez les modéliser comme des fonctions "spéciales" (appelez-les "primitives" ou "intégrées" ou "natives" ou autre), mais pour ce faire, il faut génériquement un ensemble de sémantique très robuste pour définir de telles fonctions spéciales. L'alternative est d'avoir des opérateurs intégrés qui ressemblent sémantiquement à des opérateurs définis par l'utilisateur, mais qui autrement invoqueraient des chemins spéciaux dans le compilateur. Cela va à l'encontre de la réponse à la deuxième question ...

Mis à part le problème de traduction automatique que j'ai mentionné ci-dessus, au niveau syntaxique, les opérateurs ne sont pas vraiment différents des fonctions. Ce sont des caractéristiques distinctives qui tendent à être concises et symboliques, ce qui suggère une caractéristique supplémentaire importante dont elles doivent être utiles: elles doivent avoir une signification/sémantique largement comprise pour les développeurs. Les symboles courts ne donnent pas beaucoup de sens à moins qu'ils ne soient abrégés pour un ensemble de sémantiques déjà comprises. Cela rend les opérateurs définis par l'utilisateur par nature inutiles, car de par leur nature même, ils ne sont pas si largement compris. Ils ont autant de sens que les noms de fonction à une ou deux lettres.

Les surcharges des opérateurs de C++ fournissent un terrain fertile pour examiner cela. La plupart des "abus" de surcharge d'opérateur se présentent sous la forme de surcharges qui rompent une partie du contrat sémantique qui est largement compris (un exemple classique est une surcharge d'opérateur + telle que a + b! = B + a, ou où + modifie l'une de ses opérandes).

Si vous regardez Smalltalk, qui permet à l'opérateur de surcharger et les opérateurs définis par l'utilisateur, vous pouvez voir comment un langage pourrait s'y prendre et à quel point il serait utile. Dans Smalltalk, les opérateurs sont simplement des méthodes avec différentes propriétés syntaxiques (à savoir, ils sont codés en binaire infixé). Le langage utilise des "méthodes primitives" pour les opérateurs et méthodes accélérés spéciaux. Vous constatez que peu ou pas d'opérateurs définis par l'utilisateur sont créés, et lorsqu'ils le sont, ils ont tendance à ne pas être utilisés autant que l'auteur a probablement voulu qu'ils soient utilisés. Même l'équivalent d'une surcharge d'opérateur est rare, car il s'agit surtout d'une perte nette de définir une nouvelle fonction comme opérateur au lieu d'une méthode, car cette dernière permet l'expression de la sémantique de la fonction.

3
Christopher Smith

J'ai toujours trouvé que les surcharges d'opérateur en C++ étaient un raccourci pratique pour une équipe de développeur unique, mais qui provoque toutes sortes de confusion à long terme simplement parce que les appels de méthode sont "cachés" d'une manière qui n'est pas facile pour que des outils comme doxygen soient séparés, et les gens doivent comprendre les idiomes afin de les utiliser correctement.

Parfois, il est beaucoup plus difficile de donner un sens à ce que vous attendez, même. Il était une fois, dans un grand projet C++ multiplateforme, j'ai décidé que ce serait une bonne idée de normaliser la façon dont les chemins étaient construits en créant un objet FilePath (similaire au File de Java object), qui aurait opérateur/utilisé pour concaténer une autre partie de chemin (donc vous pourriez faire quelque chose comme File::getHomeDir()/"foo"/"bar" et cela ferait la bonne chose sur toutes nos plates-formes prises en charge). Tous ceux qui l'ont vu diraient essentiellement: "Qu'est-ce que c'est? La division des cordes? ... Oh, c'est mignon, mais je ne lui fais pas confiance pour faire la bonne chose."

De même, il y a beaucoup de cas dans la programmation graphique ou dans d'autres domaines où les mathématiques vectorielles/matricielles se produisent souvent où il est tentant de faire des choses comme Matrix * Matrix, Vector * Vector (dot), Vector% Vector (cross), Matrix * Vector ( transformation matricielle), Matrix ^ Vector (transformation matricielle dans un cas spécial en ignorant les coordonnées homogènes - utile pour les normales de surface), etc. jusqu'à confondre davantage la question pour les autres. Cela n'en vaut pas la peine.

1
fluffy

Les surcharges d'opérateur sont une mauvaise idée pour la même raison que les surcharges de méthode sont une mauvaise idée: le même symbole à l'écran aurait des significations différentes selon ce qui l'entoure. Cela rend plus difficile la lecture occasionnelle.

La lisibilité étant un aspect essentiel de la maintenabilité, vous devez toujours éviter de surcharger (sauf dans certains cas très particuliers). Il est préférable que chaque symbole (qu'il soit opérateur ou identifiant alphanumérique) ait une signification unique qui se démarque d'elle-même.

Pour illustrer: lors de la lecture de code inconnu, si vous rencontrez un nouvel identifiant alphanum que vous ne connaissez pas, au moins vous avez l'avantage que vous savez que vous ne le connaissez pas. Vous pouvez ensuite aller le chercher. Cependant, si vous voyez un identifiant ou un opérateur commun dont vous connaissez la signification, vous êtes beaucoup moins susceptible de remarquer qu'il a en fait été surchargé pour avoir une signification complètement différente. Pour savoir quels opérateurs ont été surchargés (dans une base de code qui a largement utilisé la surcharge), vous auriez besoin d'une connaissance pratique du code complet, même si vous ne voulez en lire qu'une petite partie. Cela rendrait difficile la mise à jour de nouveaux développeurs sur ce code, et impossible d'amener des gens pour un petit travail. Cela peut être bon pour la sécurité d'emploi des programmeurs, mais si vous êtes responsable du succès de la base de code, vous devez éviter cette pratique à tout prix.

Parce que les opérateurs sont de petite taille, surcharger les opérateurs permettrait un code plus dense, mais rendre le code dense n'est pas un réel avantage. Une ligne avec deux fois la logique prend deux fois plus de temps à lire. Le compilateur s'en fiche. Le seul problème est la lisibilité humaine. Étant donné que rendre le code compact n'améliore pas la lisibilité, la compacité ne présente aucun avantage réel. Allez-y, prenez l'espace et donnez aux opérations uniques un identifiant unique, et votre code aura plus de succès à long terme.

0
AgilePro