6
votes

Comment désagréger les définitions de l'opérateur entre objets / classes dans un langage de programmation?

Je conçois mon propre langage de programmation (appelé Lima, si vous vous en souciez sur www.bettrud.com), et j'essaie d'envelopper ma tête autour de la mise en œuvre de la surcharge de l'opérateur. Je déciderai de lier les opérateurs sur des objets spécifiques (sa langue basée sur le prototype). (C'est aussi une langue dynamique, où 'var' est comme "Var" en JavaScript - une variable pouvant contenir tout type de valeur).

Par exemple, ce serait un objet avec un opérateur redéfini +: xxx

J'espère que c'est assez évident de quoi cela fait. L'opérateur est défini lorsque X est à la fois l'opérande droit et gauche (en utilisant auto pour noter ceci).

Le problème est de quoi faire lorsque vous avez deux objets qui définissent un opérateur d'une manière ouverte comme celle-ci. Par exemple, que faites-vous dans ce scénario: xxx

ainsi une réponse facile à cette question est "bien duh, ne fais pas de définitions ambiguës", mais ce n'est pas que Facile. Et si vous incluez un module ayant un type d'objet, puis défini un type d'objet B.

Comment créez-vous une langue qui protège contre d'autres objets qui détournent ce que vous voulez faire avec vos opérateurs?

C ++ comporte surcharge de l'opérateur défini comme "membres" des classes. Comment C ++ traite-t-il d'ambiguïté comme celui-ci?


10 commentaires

De plus, si vous avez un mot clé var , j'espère que vous ne faites pas la même erreur que JavaScript et de faire des variables globales par défaut, sauf indication contraire var . Cela conduit à tant de bugs (toute variable que vous oubliez d'utiliser var est automatiquement partagée avec toutes les autres variables du programme par le même nom).


Vous devez le déclarer, sinon sa erreur. Donc, ne pas faire la même erreur; )


Je ne suis pas tout à fait clair ce que vos objectifs réels sont avec la langue. Vous suggérez que vous êtes intéressé par la performance, mais cela ressemble également à votre typing de canard, ce qui fonctionne généralement contre la performance. J'aime le chaînage de a , mais à un coup d'œil rapide, je n'ai pas remarqué comment vous gérez les opérations arithmétiques dont le résultat pourrait être supérieur à celui des types d'opérande.


@supercat L'objectif est d'être le meilleur des deux mondes. Maximisez la productivité du développement principalement et utilisez des modules d'optimisation créés par l'utilisateur pour optimiser les performances de manière entièrement automatisée. Le programmeur a le pouvoir de définir les entrées attendues au programme (ligne de commande, appels réseau, etc.) qui donnent des optimiseurs les informations dont ils ont besoin pour bien fonctionner. En ce qui concerne les opérations arithmétiques, les valeurs de Lima n'ont effectivement pas de "types", et il n'y a pas de limite supérieure sur les tailles de nombres (autres que des contraintes de mémoire typiques).


N'hésitez pas à laisser un commentaire au bas de la documentation: BTETRUD.COM/LIMA/LIMA -Documentation.html au lieu de si


@Bt: Une salle de discussion peut être plus utile si vous souhaitez créer un et poster un lien. Certains problèmes pour commencer avec: que vous attendriez-vous à la valeur de 1.0 / 3.0 ? Du 2.0 ^ 0.5 ? Comment 2 ^ (2 ^ 0.5) <10 ^ 0,427572070255 être évalué? Vous semblez favoriser le point flottant décimal de précision arbitraire. Savez-vous pourquoi les langages de programmation ne font pas?


Créé un: US21.Chatzy.com/55855297782806 . Je sais pourquoi la programmation Langauges Habituellement Pas. 1.0 / 3.0 est identique à 1/3 et est laissé conceptuellement inévalué jusqu'à ce que la destination finale de ces données soit connue. Si vous l'imprimez à l'écran avec 2 précision décimale (et c'est la seule façon dont vous l'utilisez), Lima pourrait décider d'utiliser un flotteur unique de précision pour stocker la valeur avant impression. Même avec le reste. Si une précision arbitraire est nécessaire, cela peut aller jusqu'à calculer ces valeurs symboliquement (comme Mathematica).


@Bt: Vous ne m'avez pas ping, alors je viens de remarquer que la salle de discussion.


@Bt: BTW, il a donc sa propre installation de discussion intégrée.


@supercat hm, ne savait pas que: chat.stackoverflow.com/rooms/info/57376/lima


4 Réponses :


1
votes

en C ++, un op B signifie a.op (b), donc c'est un ambigieux; La commande règle. Si, en C ++, vous souhaitez définir un opérateur dont l'opérande gauche est un type intégré, l'opérateur doit alors être une fonction globale avec deux arguments, pas un membre; Encore une fois, cependant, l'ordre des opérandes détermine la méthode à appeler. Il est illégal de définir un opérateur où les deux opérandes sont de types intégrés.


5 commentaires

Je ne peux pas dire ce que vous dites ici, exactement. Je ne vois pas où j'ai dit quelque chose qui empêche les constructeurs de copies d'être membres; En effet, j'ai pris pour acquis ce que l'OP a dit que la plupart des opérateurs sont des fonctions membres. Non, les types de classe ne sont pas des types intégrés, par définition. La règle est que au moins un opérande de tout opérateur défini par l'utilisateur doit être un type défini par l'utilisateur. En tout état de cause, si vous souhaitez me dire Quelles déclarations je me suis trompé, je suis toutes les oreilles.


@Maesh, il n'est pas faux, il vient de se concentrer sur des types intégrés. La première phrase couvre les types de classe. Il existe deux façons de faire surcharge de l'opérateur en C ++. Vous pouvez soit en faire une méthode de la classe sur le côté gauche, ou en faire une fonction globale qui est surchargée pour spécifier le type des deux arguments. Il dit que les types intégrés ne peuvent être surchargés qu'avec cette dernière approche (puisqu'ils n'ont pas de cours). Il en va de même pour toutes les classes que vous ne pouvez pas modifier la définition de.


@mgiuca Désolé, vous avez tous les deux raison. Supprimer mon commentaire et +1 de moi.


On dirait que c'est semblable à la façon dont Python le gère (selon Mgiuca). Intéressant.


@Bt oui, sauf avec deux différences importantes: a) C ++ ne dispose pas des versions inverse que Python a, et b) C ++ vous permet de définir une fonction de surcharge de l'opérateur en dehors de la classe, ce qui signifie que vous pouvez augmenter le comportement des opérateurs. Pour un type particulier, même si vous n'avez pas accès à l'écriture à la classe.



3
votes

La plupart des langues donneront la priorité à la classe à gauche. C ++, je crois, ne vous permet pas de surcharger des opérateurs sur le côté droit. Lorsque vous définissez opérateur + , vous définissez une addition lorsque ce type est à gauche, pour quoi que ce soit à droite.

En fait, il ne serait pas logique que vous autorisiez votre opérateur pour fonctionner lorsque le type est situé à droite. Cela fonctionne pour +, mais envisagez -. Si le type A définit opérateur - d'une certaine manière, et je fais int x - a y, je ne veux pas de d'opérateur à appeler, car il va calculer la soustraction à l'envers!

in python, qui a plus étendu Règles de surcharge de l'opérateur , il y a un méthode séparée pour la direction inverse. Par exemple, il existe une méthode __ sous __ qui surcharge l'opérateur lorsque ce type se trouve à gauche et un __ __ qui surcharge le - opérateur lorsque ce type est à droite . Ceci est similaire à la capacité, dans votre langue, de laisser le "soi" d'apparaître à gauche ou à droite, mais elle introduit une ambiguïté.

Python donne la priorité à la chose à gauche - cela fonctionne mieux dans une langue dynamique. Si Python rencontre x - y , il appelle d'abord x .__ sous __ (y) pour voir si x sait soustraire y < / code>. Cela peut produire un résultat ou renvoyer une valeur spéciale notamplementad . Si Python trouve que notimplementé a été renvoyé, il essa une autre voie. Il appelle y .__ RSUB __ (x) , qui aurait été programmé en sachant que y était sur le côté droit. Si cela renvoie également notimplementé , alors un typeError est soulevé, car les types étaient incompatibles pour cette opération.

Je pense que c'est la stratégie de surcharge d'opérateur idéale pour les langages dynamiques.

edit: Pour donner un résumé un peu, vous avez une situation ambiguë, alors vous n'avez vraiment que trois choix:

  • Donnez la priorité à un côté ou à l'autre (généralement celui de gauche). Cela empêche une classe avec une surcharge droite du détournement d'une classe avec une surcharge gauche, mais pas l'inverse. (Cela fonctionne mieux dans les langages dynamiques, car les méthodes peuvent décider s'ils peuvent le gérer et reporter de manière dynamique à l'autre.)
  • Faites-en une erreur (comme @Dave suggère dans sa réponse). S'il y a plus de plus d'un choix viable, c'est une erreur de compilateur. (Cela fonctionne mieux dans les langues statiques, où vous pouvez attraper cette chose à l'avance.)
  • n'autorise que la classe gauche-lavie de définir les surcharges de l'opérateur, comme en C ++. (Ensuite, votre classe B serait illégale.)

    La seule autre option consiste à introduire un système complexe de priorité aux surcharges de l'opérateur, mais vous avez dit que vous avez dit que vous souhaitez réduire les frais généraux cognitifs.


6 commentaires

Intéressant, mais le problème ici est que tout en étant logiquement désambiguillé, il augmente toujours la charge cognitive. J'essaie de réduire autant que possible la charge cognitive. Mais +1!


Je vois ... Je viens de réaliser maintenant comment votre code fonctionne: l'opérateur + dans A est quand A est à gauche. L'opérateur + en B est quand b est à droite. Si efficacement, vous avez une version flexible de Python Ajouter et Radd . J'ajouterai un peu plus à ma réponse.


@B t Notez que si vous avez ajouté la chose notamentaire de Python, vous pouvez créer efficacement des cours avec une priorité, comme le fait Python. Par exemple, disons que vous avez une classe INT et une classe de flotteurs, et vous voulez que le flotteur ajout de priorité, quel que soit le flotteur. Eh bien, vous pourriez avoir à la fois des surcharges int et float + sur les côtés gauche et droit, mais avez-vous notimplementalement de retour à moins que cela ne rencontre pas un INT. Par conséquent, il diffuserait de manière dynamique de flotter si d'un côté était un flotteur.


Donc, Lima a en réalité une bonne dactylographie facultative, il n'a donc pas besoin de la chose notimée de faire ce que vous dites, ce serait juste quelque chose comme: opérateur + [auto.b [B]: ret # x + b].


En ce qui concerne votre édition, j'aime l'option 2, mais je crains que toute erreur sur les ambiguïtés exigeait que tout le système soit réécrit (modules tiers et tous). Ce n'est pas une situation que je veux être. L'option 3 signifie des opérateurs à peu près à être commutatives - ce qui est une limitation que je n'aime pas. 1 est correct, mais ce serait bien d'éviter d'en quelque sorte des ambiguïtés si possible de créer un système raisonnable de cette façon.


Je pense que je vais aller avec erreur d'ambiguïté car les dommages sont contenus à ces deux objets (et leurs ancêtres) - ce qui est d'accord avec moi. Merci de m'aider à penser à travers ça!



2
votes

Je vais répondre à cette question en disant "duh, ne fais pas de définitions ambiguës".

Si je recrée votre exemple en C ++ (à l'aide d'une fonction f code> au lieu de + opérateur et int code> / float code> au lieu de a code> / b code>, mais il n'y a vraiment pas beaucoup de différence) .. . P>

template<class t>
void f(int a, t b)
{
    std::cout << "me! me! me!";
}

template<class t>
void f(t a, float b)
{
    std::cout << "no, me!";
}

int main(void)
{
    f(1, 1.0f);
    return 0;
}


2 commentaires

Je suis d'accord avec ça. Mais je pense que cela a plus de sens pour une langue statique. Dans une langue dynamique, des erreurs d'ambiguïté semblent généralement bizarre au moment de l'exécution. Il est plus habituel que les valeurs par défaut se produisent (comme défaut de la surcharge de gauche).


Donc, à l'origine, je pense que je n'avais pas complètement saisi toutes les possibilités. Mais maintenant je pense que je fais. Vous avez soit une situation où quelqu'un d'autre a défini un objet A, et vous définissez un objet B qui fonctionne avec l'objet A, ou que vous définissez les deux objets. Dans les deux cas, je pense que c'est parfaitement raisonnable d'exiger que l'ambiguïté ne soit pas codée. Je pense que je vais réellement aller avec "Ne fais pas de définitions ambiguës" +1 (ne vous donnez que la réponse car Mgiuca a aidé beaucoup. Et parce que sa réponse donne toutes les options que j'ai - y compris votre option)



1
votes

Je suggérerais que donné x + y , le compilateur doit rechercher les deux x.op_plus (y) et y.op_added_to (x) ; Chaque mise en œuvre devrait inclure un attribut indiquant s'il devrait s'agir d'une mise en œuvre «préférée», «normale», «de replie», et éventuellement indiquant qu'il est "courant". Si les deux mises en œuvre sont définies et que les implémentations sont des priorités différentes (par exemple, «préférées» et «normale»), utilisez le type pour sélectionner une préférence. Si les deux sont définis comme étant de la même priorité, les deux sont "communs", favorisez le formulaire x.op_plus (y) . Si les deux sont définis avec la même priorité et qu'ils ne sont pas à la fois "communs", signalez une erreur.

Je suggérerais que la capacité de hiérarchiser les surcharges et les conversions aurait une caractéristique très importante pour avoir une langue. Il n'est pas utile que les langues squawk sur des surcharges ambiguës dans des cas où les deux candidats feraient la même chose, mais les langues devraient s'affronter dans des cas où deux surcharges possibles auraient différentes significations, chacune d'elles étant utile dans certains contextes. Par exemple, donné quelquefloat == Somedouble ou Somedouble == somelong , un compilateur devrait Squawk, car il peut y avoir une utilité pour savoir si le nombre de personnes numériques Quantités représentées par deux valeurs correspondant, et il peut également y avoir une utilité pour savoir si l'opérande gauche détient la meilleure représentation possible (pour son type) de la valeur dans l'opérande de droite. Java et C # ne signent pas l'ambiguïté dans les deux cas, optant plutôt pour utiliser la première signification pour la première expression et la seconde pour la seconde, même si la signification pourrait être utile dans les deux cas. Je suggérerais qu'il serait préférable de rejeter de telles comparaisons que de les avoir à mettre en œuvre une sémantique incohérente.

Dans l'ensemble, je vous suggérerais de philosophie qu'une bonne conception de la langue devrait laisser un programmeur indiquer ce qui est important et ce qui n'est pas. Si un programmeur sait que certaines "ambiguïtés" ne sont pas des problèmes, mais d'autres sont, il devrait être facile d'avoir le pavillon du compilateur, mais pas le premier.

addendum

J'ai regardé brièvement votre proposition; Il voit que vous attendez des liaisons entièrement dynamiques. J'ai travaillé avec une langue comme celle-là (Hypertalk, vers 1988) et c'était "intéressant". Considérez, par exemple, que "2x" <"3" <4 <10 <"11" <"2x". Double Dispatch peut parfois être utile, mais uniquement dans les cas où les opérateurs surchargent avec différentes sémantiques (par exemple, la chaîne et les comparaisons numériques) sont limitées à fonctionner sur des ensembles disjoints de choses. Interdire les opérations ambiguës au moment de la compilation est une bonne chose, car le programmeur sera en mesure de spécifier ce qui est destiné. Avoir une telle ambiguïté déclencher une erreur de temps d'exécution est une mauvaise chose, car le programmeur peut être passé longtemps au moment de la surface des erreurs. Par conséquent, je ne peux vraiment pas offrir de conseils pour comment faire une double envoi de temps d'exécution pour les opérateurs, sauf pour dire «ne», à moins que la compilation de la compilation, vous limitez les opérandes aux combinaisons dans lesquelles toute surcharge éventuelle aurait toujours la même sémantique. .

Par exemple, si vous avez eu une "liste immuable de nombres" abstraite, avec un membre pour signaler la longueur ou renvoyer le numéro à un index particulier, vous pouvez spécifier que deux instances sont égales si elles ont la même longueur, et chaque pour chaque index, ils renvoient le même numéro. Bien qu'il soit possible de comparer deux cas pour l'égalité en examinant chaque article, cela pourrait être inefficace si par exemple. Un exemple était un type "Bunchoftzeroes" qui a simplement tenu un entier N = 1000000 et ne stocke aucun objet, et l'autre était un "ncopiesofarray" qui détenait n = 500000 et {0,0} comme la matrice à copier . Si de nombreux cas de ces types seront comparés, l'efficacité pourrait être améliorée en disposant de telles comparaisons invoquées une méthode qui, après avoir vérifié la longueur de la matrice globale, vérifie si le tableau "Modèle" contient des éléments non nuls. Si ce n'est pas le cas, il peut être signalé comme étant égal à la matrice BUNCH-of-ZEROES sans avoir à effectuer 1 000 000 comparaisons d'éléments. Notez que l'invocation d'une telle méthode par double expédition ne modifierait pas le comportement du programme - il lui permettrait simplement d'exécuter plus rapidement.


6 commentaires

Merci pour la réponse, mais que toutes ces conditions à considérer sont une tonne de frais généraux cognitifs et rendrait ainsi le système beaucoup plus difficile à raisonner. Je suis d'accord avec vous que le compilateur ne devrait pas se plaindre si les deux options dans une situation ambiguë font la même chose. Je pense que le compilateur devrait être capable de comprendre que Tho et de ne pas compter sur le programmeur lui dire de leur faire confiance en ce qu'ils sont identiques (cela peut causer des problèmes lorsque le programmateur n'aura mal que cela). Lima vous permet de choisir pour utiliser les opérateurs d'appeler en tant que fonctions: méta [obj1] .Opérator [+] [obj1 obj2]


@Bt: Il ne sera pas possible en général pour un compilateur de savoir si deux méthodes font la même chose à moins que quelque chose ne lui dit que. En ce qui concerne la charge cognitive, une certaine complexité va toujours être nécessaire "quelque part"; Tenter de trop simplifier une partie d'une conception entraînera généralement une complexité supplémentaire ou des «comportements surprenants» ailleurs. Par exemple, combien de personnes s'attendraient-elles qu'en Java, Math.Round (2147483646L) donnera 2147483647? Un tel comportement est une conséquence des primitives «classement» de Java, plutôt que d'utiliser des règles plus détaillées pour les conversions de type.


Je suis en désaccord que ce n'est pas possible. Difficile oui, impossible non. En tout état de cause, le programmeur peut savoir si deux fonctions sont identiques, le compilateur devrait pouvoir savoir. Cependant, même si le compilateur ne sait pas si deux fonctions sont identiques ou non, l'inconvénient n'est qu'une erreur et le programmeur doit spécifier l'opérateur de l'objet à utiliser. Je ne suis pas non plus d'accord que la simplification d'une zone conduit nécessairement à des comportements surprenants ailleurs. La complexité n'est pas un jeu à somme nulle. C'est là que l'art de la conception de la langue arrive. Mais n'hésitez pas à donner à ma langue une critique cinglante; )


@Bt: Une bonne conception de la langue identifiera des comportements surprenants ou ennuyeux et tentera de déterminer la quantité minimale de complexité nécessaire pour les atténuer. En outre, je suis souscris à la philosophie que de nombreux efforts consacrés à avoir des ordinateurs infère ce que les gens veulent être mieux dépensés pour faciliter la facilité aux personnes de Spécifier ce qu'ils veulent avec une répétition minimale . En outre, il faut essayer d'avoir des choses qui sont sémantiquement d'utiliser différentes syntaxes différentes, même si elles produisent le même code. Un de mes pépins d'animaux avec Java / .NET Règles de point flottant est que ...


... une déclaration comme quelqueevariable = (flotteur) math.sin (autrevariable); pourrait signifier "Je veux stocker le calcul aussi précisément que possible dans quelqueevariable , que je croire est float "ou" je sais quelqueevariable est un double , mais je veux forcer la valeur à arrondir sur float précision ". L'effet d'exécution de la distribution est identique dans les deux cas, mais en utilisant la même syntaxe pour les deux rendra plus difficile à retravailler le code s'il est nécessaire de modifier quelqueevariable pour utiliser une précision supérieure.


Je suis d'accord avec beaucoup de cela. En fait, le point de vente principal de Lima est qu'il donne au programmeur la capacité de spécifier exactement ce qu'elles signent et ce qu'ils veulent. Par exemple, vous pouvez dire au programme la gamme d'entrées externes que vous attendez, avec quelles probabilités. Cela permet une tonne plus précise infère . Lima a également la possibilité de spécifier la précision (dans) de calculs numériques afin que le compilateur puisse optimiser des choses comme ça. Merci pour l'aide. Et je apprécierait les commentaires sur la langue comme je l'ai spécifié si vous le souhaitez.