Une des caractéristiques des langages fonctionnels qui me manque est l'idée que les opérateurs ne sont que des fonctions. 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. Ainsi, dans un certain sens, les opérateurs sont toujours des fonctions (c'est très vrai dans D où l'opérateur est passé en tant que chaîne dans un paramètre de modèle).
Il semble que, lorsque la surcharge d'opérateurs est autorisée, il est souvent trivial d'ajouter des opérateurs personnalisés supplémentaires. J'ai trouvé cet article de blog qui explique que les opérateurs personnalisés ne fonctionnent pas bien avec la notation infix en raison des règles de priorité, mais l'auteur propose plusieurs solutions à ce problème.
J'ai regardé autour de moi et je n'ai trouvé aucun langage de procédure prenant en charge des opérateurs personnalisés dans ce langage. Il y a des hacks (comme les macros en C ++), mais ce n'est pas la même chose que le support du langage.
Puisque cette fonctionnalité est assez simple à 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 langages d’ajouter des fonctionnalités utiles qui peuvent être facilement utilisées (macros, opérateur ternaire, pointeurs non sécurisés).
Cas d'utilisation réels:
- Implémente les opérateurs manquants (par exemple, Lua n'a pas d'opérateurs au niveau des bits)
- Mimic D's
~
(concaténation de tableaux) - DSL
- Utiliser
|
comme sucre de syntaxe de style de tuyau Unix (à l'aide de coroutines / générateurs)
Je suis aussi intéressé par les langues qui font permettre aux opérateurs personnalisés, mais je suis plus intéressé pourquoi il a été exclu. J'ai envisagé de forger un langage de script pour ajouter des opérateurs définis par l'utilisateur, mais je me suis arrêté quand je me suis rendu compte que je ne l'avais vu nulle part. Il y a donc probablement une bonne raison pour laquelle les concepteurs de langage plus intelligents que moi ne l'ont pas permis.
la source
Réponses:
Il existe deux écoles de pensée diamétralement opposées dans la conception du langage de programmation. La première est que les programmeurs écrivent un meilleur code avec moins de restrictions, et l’autre est qu’ils écrivent un meilleur code avec davantage de restrictions. À mon avis, la réalité est que les bons programmeurs expérimentés s'épanouissent avec moins de restrictions, mais que ces 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 vraiment affreux pour un débutant. Donc, si votre langue les inclut ou non, cela dépend de la pensée de votre concepteur de langue.
la source
Format
méthode) et quand il doit refuser ( p.ex. arguments de boxe automatique àReferenceEquals
). Plus le langage de capacité permet aux programmeurs de dire quand certaines inférences seraient inappropriées, plus il peut offrir des inférences pratiques en toute sécurité, le cas échéant.Si vous avez le choix entre concaténer des tableaux avec ~ ou avec "myArray.Concat (secondArray)", je préférerais probablement le dernier. Pourquoi? Parce que ~ est un caractère complètement dépourvu 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.
Comme vous l'avez dit, les opérateurs ne sont fondamentalement pas différents des méthodes. Cependant, bien que des noms lisibles et compréhensibles puissent être attribués aux méthodes, ce qui facilite 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 PHP (concaténation de chaînes) ni la plupart des opérateurs de Haskell ou d'OCaml, bien que dans ce cas, certaines normes universellement acceptées apparaissent pour les langages fonctionnels.la source
+
et<<
certainement ne sont pas définis surObject
(je reçois « pas de match pour l' opérateur + en ... » lorsque vous faites que sur une classe nue en C ++).Votre prémisse est fausse. Ce n'est pas «assez simple à mettre en œuvre». En fait, cela pose de nombreux problèmes.
Jetons un coup d'œil aux «solutions» suggérées dans le post:
Globalement, il s’agit d’une fonctionnalité coûteuse à mettre en œuvre, tant en termes de complexité d’analyseur que de performances, et il n’est pas clair que cela apporterait de nombreux avantages. Bien sûr, la possibilité de définir de nouveaux opérateurs présente certains avantages, mais même ceux-ci sont controversés (il suffit de regarder les autres réponses en affirmant qu’avoir de nouveaux opérateurs n’est pas une bonne chose).
la source
Ignorons l'intégralité de l'argument "opérateurs abusés pour nuire à la lisibilité" pour le moment et concentrons-nous sur les implications en termes de conception de langage.
Les opérateurs Infix ont plus de problèmes que de simples règles de priorité (bien que, pour être franc, le lien que vous référencez 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)
etb.operator+(a)
? Préférer l’un sur l’autre conduit à casser la propriété commutative attendue de cet opérateur. Lancer une erreur peut conduire à des modules qui fonctionneraient autrement seraient cassés une fois ensemble. Qu'advient-il lorsque vous commencez à jeter des types dérivés dans le mélange?Le fait est que les opérateurs ne sont pas que des fonctions. Les fonctions sont autonomes ou appartiennent à leur classe, ce qui indique clairement quel paramètre (le cas échéant) est propriétaire de la répartition polymorphe.
Et cela ne tient pas compte des divers problèmes d’emballage et de résolution posés par les opérateurs. La raison pour laquelle les concepteurs de langages (en gros) limitent la définition d'opérateur infix est parce qu'il crée une pile de problèmes pour le langage tout en offrant des avantages discutables.
Et franchement, car ils ne sont pas triviaux à mettre en œuvre.
la source
+
est mauvais. Mais est-ce vraiment un argument contre des opérateurs définis par l'utilisateur? Cela semble être un argument contre la surcharge des opérateurs en général.boost::spirit
. Dès que vous autorisez des opérateurs définis par l'utilisateur, la situation empire car il n'existe aucun moyen efficace de définir la priorité des mathématiques. J'ai moi-même écrit un peu à ce sujet dans le contexte d'un langage qui cherche spécifiquement à résoudre les problèmes d'opérateurs définis arbitrairement.Je pense que vous seriez surpris de voir combien de fois les surcharges d’opérateurs sont implémentées sous une forme ou une autre. Mais ils ne sont pas couramment utilisés dans beaucoup de communautés.
Pourquoi utiliser ~ pour concaténer un tableau? Pourquoi ne pas utiliser << comme le fait Ruby ? Parce que les programmeurs avec lesquels vous travaillez ne sont probablement pas des programmeurs Ruby. Ou D programmeurs. Alors, que font-ils quand ils rencontrent votre code? Ils doivent aller chercher ce que le symbole signifie.
J'avais l'habitude de travailler avec un très bon développeur C # qui avait également un goût prononcé pour les langages fonctionnels. À l'improviste, il a commencé à introduire les monades en C # à l'aide de méthodes d'extension et en utilisant la terminologie standard des monades . Personne ne pouvait nier que certains de ses codes étaient plus clairs et lisibles encore une fois que l'on savait ce que cela voulait dire, mais cela signifiait que tout le monde devait apprendre la terminologie de la monade avant que le code ne prenne du sens .
Assez juste, vous pensez? Ce n'était qu'une petite équipe. Personnellement, je ne suis pas d'accord. Chaque nouveau développeur était destiné à être dérouté par cette terminologie. N'avons-nous pas assez de problèmes pour apprendre un nouveau domaine?
D'un autre côté, j'utiliserai volontiers l'
??
opérateur en C # car je m'attends à ce que les autres développeurs C # sachent de quoi il s'agit, mais je ne le surchargerais pas dans un langage qui ne le prendrait pas en charge par défaut.la source
??
exemple.Je peux penser à plusieurs raisons:
O(1)
. Mais avec la surcharge d’opérateur,someobject[i]
uneO(n)
opération pourrait facilement dépendre de la mise en œuvre de l’opérateur d’indexation.En réalité, il y a très peu de cas où la surcharge des opérateurs a des utilisations justifiables par rapport à l'utilisation de fonctions standard. Un exemple légitime pourrait être la conception d’une classe de nombres complexes à l’usage des mathématiciens, qui comprennent les méthodes bien comprises de définition des opérateurs mathématiques pour les nombres complexes. Mais ce n'est vraiment pas un cas très commun.
Quelques cas intéressants à considérer:
+
c'est juste une fonction régulière. Vous pouvez définir les fonctions à votre guise (il existe généralement un moyen de les définir dans des espaces de noms distincts pour éviter tout conflit avec les fonctions intégrées+
), y compris les opérateurs. Mais il existe une tendance culturelle à utiliser des noms de fonction significatifs, de sorte que les abus ne sont pas trop importants. En outre, dans Lisp, la notation de préfixe a tendance à être utilisée exclusivement, de sorte que le "sucre syntaxique" a moins de valeur que les surcharges d’opérateurs fournissent.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 du métal pour la performance, vous permettant par exemple d'écrire une classe de nombres complexe 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.la source
Ce n'est pas trivial à implémenter (sauf si trivialement implémenté). De plus, cela ne vous rapporte pas beaucoup, même si elle est mise en œuvre de manière idéale: les gains de lisibilité résultant de la nuance sont compensés par les pertes de lisibilité dues à la non-familiarité et à l'opacité. En bref, c'est peu commun parce que 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 elles le font de différentes manières:
la source
Une des principales raisons pour lesquelles les opérateurs personnalisés sont découragés est qu’aucun opérateur ne peut / ne peut rien faire.
Par exemple
cstream
, on a beaucoup critiqué la surcharge de l'équipe de gauche.Lorsqu'un langage autorise les surcharges d'opérateur, il est généralement encouragé de garder le comportement de l'opérateur similaire au comportement de base afin d'éviter toute confusion.
De plus, les opérateurs définis par l'utilisateur rendent l'analyse beaucoup plus difficile, en particulier lorsqu'il existe également des règles de préférence personnalisées.
la source
+
ajoutez deux choses,-
soustrayez-les,*
multipliez-les. Mon sentiment est que personne ne force le programmeur à faire en sorte que la fonction / méthodeadd
ajoute quelque chose etdoNothing
puisse lancer des armes nucléaires. Eta.plus(b.minus(c.times(d)).times(e)
est beaucoup moins lisible quea + (b - c * d) * e
(bonus supplémentaire - où la première piqûre est une erreur de transcription). Je ne vois pas en quoi le premier est plus significatif ...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 qui 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.
la source
elem
est une excellente idée et que tout le monde devrait comprendre un opérateur, mais d’autres semblent ne pas être d’accord.En ce qui concerne les langages qui supportent une telle surcharge: Scala le fait, de manière beaucoup plus propre et meilleure peut utiliser le langage C ++. La plupart des caractères peuvent être utilisés dans les noms de fonction, vous pouvez donc définir des opérateurs tels que! + * = ++, si vous le souhaitez. Il existe un support intégré pour infixe (pour toutes les fonctions prenant un argument). Je pense que vous pouvez également définir l’associativité de telles fonctions. Vous ne pouvez cependant pas définir la priorité (uniquement avec des tours laides, voir ici ).
la source
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
+
,|
etc., sont en réalité des méthodes unaires.Toutes les méthodes peuvent être remplacées. Cela
a + b
signifie donc l' addition d'entiers sia
etb
sont deux entiers, et l'ajout de vecteurs s'ils sont tous deuxOrderedCollection
s.Il n'y a pas de règles de priorité, car ce ne sont que des appels de méthodes. Ceci a une implication importante pour la notation mathématique standard:
3 + 4 * 5
moyen(3 + 4) * 5
, pas3 + (4 * 5)
.(Il s'agit d'un obstacle majeur pour les débutants Smalltalk. Briser les règles mathématiques supprime un cas particulier, de sorte que toute l'évaluation du code se déroule de manière uniforme de gauche à droite, ce qui simplifie énormément le langage.)
la source
Vous vous battez contre deux choses ici:
Dans la plupart des langues, les opérateurs ne sont pas vraiment implémentés en tant que fonctions simples. Ils peuvent avoir un échafaudage de fonctions, mais le compilateur / runtime est explicitement conscient de leur signification sémantique et de la manière de 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 tout le temps système d'appel de fonction dans leur implémentation). La plupart des opérateurs sont des abstractions de niveau supérieur sur des 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 des bits). Vous pouvez les modéliser en tant que fonctions "spéciales" (appelez-les "primitives" ou "fonctions intégrées" ou "natives" ou autre), mais cela nécessite généralement un ensemble très robuste de sémantiques pour la définition de telles fonctions spéciales. L'alternative consiste à avoir des opérateurs intégrés ressemblant sémantiquement à des opérateurs définis par l'utilisateur, mais invoquant sinon des chemins spéciaux dans le compilateur. Cela va à l’encontre de la réponse à la deuxième question ...
Outre le problème de traduction automatique mentionné ci-dessus, les opérateurs ne sont pas vraiment différents des fonctions au niveau syntaxique. Leurs caractéristiques distinctives tendent à être qu'elles sont concises et symboliques, ce qui laisse entrevoir une caractéristique supplémentaire importante dont elles doivent avoir besoin pour être utiles: elles doivent avoir une signification / sémantique largement comprise par les développeurs. Les symboles courts ne donnent pas beaucoup de sens à moins que ce ne soit un raccourci pour un ensemble de sémantiques déjà compris. Cela rend les opérateurs définis par l'utilisateur inutiles par nature, car de par leur nature même, ils ne sont pas compris de manière très large. Ils ont autant de sens qu'un nom de fonction à une ou deux lettres.
Les surcharges d’opérateurs de C ++ fournissent un terrain fertile pour l’examiner. La plupart des "abus" d'opérateurs 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 examinez Smalltalk, qui autorise la surcharge d'opérateurs et les opérateurs définis par l'utilisateur, vous pouvez voir comment une langue peut s'y prendre et son utilité. Dans Smalltalk, les opérateurs sont simplement des méthodes ayant différentes propriétés syntaxiques (à savoir, elles sont codées en tant qu'infix binary). Le langage utilise des "méthodes primitives" pour des 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 le leur a probablement destinés. Même l'équivalent d'une surcharge d'opérateur est rare, car définir une nouvelle fonction en tant qu'opérateur plutôt qu'en tant que méthode est une perte nette, car cette dernière permet l'expression de la sémantique de la fonction.
la source
J'ai toujours trouvé que les surcharges d'opérateurs en C ++ constituaient un raccourci commode pour une équipe composée d'un développeur unique, mais provoquaient toute une confusion sur le long terme simplement parce que les appels de méthode étaient "masqués" d'une manière qui n'était pas facile. pour que des outils comme le doxygen se séparent et que les gens aient besoin de comprendre les idiomes pour pouvoir les utiliser correctement.
Parfois, il est beaucoup plus difficile de comprendre que ce à quoi vous vous attendiez, même. Il était une fois, dans un grand projet C ++ multiplate-forme, la décision de normaliser la construction des chemins en créant un
FilePath
objet (similaire à l'File
objet Java ), qui aurait pour opérateur / utilisé de concaténer un autre partie de chemin sur elle (afin que vous puissiez faire quelque chose commeFile::getHomeDir()/"foo"/"bar"
et il ferait la bonne chose sur toutes nos plates-formes prises en charge). Tous ceux qui l'ont vu diraient essentiellement: "Qu'est-ce que l'enfer? Division de cordes? ... Oh, c'est mignon, mais je ne lui fais pas confiance pour faire la bonne chose."De même, il existe de nombreux cas en programmation graphique ou dans d’autres domaines où les mathématiques vectorielles / matricielles se produisent souvent dans des situations où il est tentant de faire des choses comme Matrix * Matrix, Vector * Vector (point), Vector% Vector (croix), Matrix * Vector ( transformée matricielle), Matrix ^ Vector (transformée matricielle dans les cas spéciaux en ignorant la coordonnée homogène - utile pour les normales à la surface), etc., mais elle économise un peu de temps d'analyse pour la personne qui a écrit la bibliothèque de mathématiques vectorielles. confondant la question plus pour les autres. Ça n'en vaut pas la peine.
la source
Les surcharges d’opérateurs sont une mauvaise idée, pour la même raison que les surcharges de méthodes: un même symbole à l’écran aurait des significations différentes en fonction de ce qui les entoure. Cela rend plus difficile la lecture occasionnelle.
Étant donné que la lisibilité est un aspect critique de la maintenabilité, vous devez toujours éviter les surcharges (sauf dans des cas très particuliers). Il est de loin préférable que chaque symbole (qu’il s’agisse d’un opérateur ou d’un identificateur alphanumérique) ait une signification unique.
Pour illustrer ceci: lors de la lecture d'un code non familier, si vous rencontrez un nouvel identificateur alphanum que vous ne connaissez pas, vous avez au moins l'avantage de savoir que vous ne le connaissez pas.. Vous pouvez ensuite aller le chercher. Si, toutefois, vous voyez un identifiant ou un opérateur commun dont vous connaissez la signification, vous aurez beaucoup moins de chances de remarquer qu'il a été surchargé pour avoir une signification complètement différente. Pour savoir quels opérateurs ont été surchargés (dans une base de code généralisant la surcharge), vous devez avoir une connaissance pratique du code complet, même si vous ne voulez en lire qu'une petite partie. Cela empêcherait les nouveaux développeurs de se familiariser avec ce code et empêcherait les gens de faire un petit travail. Cela peut s'avérer utile 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.
Étant donné que les opérateurs sont de petite taille, les opérateurs surchargés autoriseraient un code plus dense, mais rendre le code dense ne constitue pas un réel avantage. Une ligne avec deux fois plus de 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 attribuez un identifiant unique aux opérations uniques. Votre code aura plus de succès à long terme.
la source
Des difficultés techniques pour gérer la priorité et l'analyse complexe mise de côté, je pense que certains aspects de ce qu'est un langage de programmation doivent être pris en compte.
Les opérateurs sont généralement des constructions logiques courtes, bien définies et documentées dans le langage de base (comparer, assigner, etc.). Ils sont aussi généralement difficiles à comprendre sans documentation (à comparer
a^b
parxor(a,b)
exemple). Il y a un nombre assez limité d'opérateurs qui pourraient réellement avoir un sens dans la programmation normale (>, <, =, + etc ..).Mon idée est qu'il vaut mieux s'en tenir à un ensemble d'opérateurs bien définis dans un langage, puis autoriser la surcharge de ces opérateurs (dans la mesure où il est recommandé aux opérateurs de faire la même chose, mais avec un type de données personnalisé).
Vos cas d'utilisation de
~
et|
seraient réellement possibles avec une surcharge d'opérateur simple (C #, C ++, etc.). DSL est un domaine d'utilisation valide, mais probablement l'un des seuls domaines valides (de mon point de vue). Je pense cependant qu'il existe de meilleurs outils pour créer de nouveaux langages au sein de celui-ci. L'exécution d'un vrai langage DSL dans un autre langage n'est pas si difficile en utilisant l'un de ces outils compilateur-compilateur. Il en va de même pour "l'argument d'extension LUA". Une langue est probablement définie principalement pour résoudre des problèmes d’une manière spécifique, et non pour servir de base à des sous-langues (des exceptions existent).la source
Un autre facteur à prendre en compte est qu’il n’est pas toujours simple de définir une opération avec les opérateurs disponibles. Je veux dire, oui, pour tout type de numéro, l'opérateur '*' peut avoir un sens, et est généralement implémenté dans le langage ou dans les modules existants. Mais dans le cas des classes complexes typiques que vous devez définir (choses telles que ShipingAddress, WindowManager, ObjectDimensions, PlayerCharacter, etc.), ce comportement n'est pas clair ... Que signifie ajouter ou soustraire un nombre à une adresse? Multiplier deux adresses?
Bien sûr, vous pouvez définir que l'ajout d'une chaîne à une classe ShippingAddress signifie une opération personnalisée telle que "remplacer la ligne 1 dans l'adresse" (au lieu de la fonction "setLine1") et l'ajout d'un nombre correspond à "remplacer le code postal" (au lieu de "setZipCode") , mais alors le code n’est pas très lisible et déroutant. Nous pensons généralement que les opérateurs sont utilisés dans les types / classes de base, car leur comportement est intuitif, clair et cohérent (une fois que vous maîtrisez le langage, au moins). Pensez dans les types tels que Integer, String, ComplexNumbers, etc.
Ainsi, même si la définition des opérateurs peut être très utile dans certains cas spécifiques, leur mise en œuvre dans le monde réel est assez limitée, dans la mesure où 99% des cas dans lesquels cela sera clairement gagné sont déjà implémentés dans le package de langue de base.
la source