La méthode est-elle en train de surcharger autre chose que du sucre syntaxique? [fermé]


19

La surcharge de méthode est-elle un type de polymorphisme? Cela me semble être simplement la différenciation de méthodes portant le même nom et des paramètres différents. Ainsi , stuff(Thing t)et stuff(Thing t, int n)sont des méthodes totalement différentes dans la mesure où le compilateur et l' exécution sont concernés.

Cela crée l'illusion, du côté de l'appelant, que c'est la même méthode qui agit différemment sur différents types d'objets - le polymorphisme. Mais ce n'est qu'une illusion, car en fait stuff(Thing t)et ce stuff(Thing t, int n)sont des méthodes complètement différentes.

La méthode est-elle en train de surcharger autre chose que du sucre syntaxique? Suis-je en train de manquer quelque chose?


Une définition courante du sucre syntaxique est qu'il est purement local . Cela signifie que changer un morceau de code en son équivalent «sucré», ou vice versa, implique des changements locaux qui n'affectent pas la structure globale du programme. Et je pense que la surcharge de méthode correspond précisément à ce critère. Regardons un exemple pour démontrer:

Considérez une classe:

class Reader {
    public String read(Book b){
        // .. translate the book to text
    }
    public String read(File b){
        // .. translate the file to text
    }
}

Considérons maintenant une autre classe qui utilise cette classe:

/* might not be the best example */
class FileProcessor {
    Reader reader = new Reader();
    public void process(File file){
        String text = reader.read(file);
        // .. do stuff with the text
    }
}

D'accord. Voyons maintenant ce qui doit changer si nous remplaçons la surcharge de méthode par des méthodes régulières:

Les readméthodes de Readerchangement vers readBook(Book)et readFile(file). Il suffit de changer leurs noms.

Le code appelant FileProcessorchange légèrement: reader.read(file)devient reader.readFile(file).

Et c'est tout.

Comme vous pouvez le voir, la différence entre utiliser la surcharge de méthode et ne pas l'utiliser est purement locale . Et c'est pourquoi je pense qu'il peut être considéré comme du sucre syntaxique pur.

J'aimerais entendre vos objections si vous en avez, peut-être que je manque quelque chose.


48
En fin de compte, toute fonctionnalité de langage de programmation n'est qu'un sucre syntaxique pour l'assembleur brut.
Philipp

31
@Philipp: Désolé, mais c'est une déclaration vraiment stupide. Les langages de programmation tirent leur utilité de la sémantique et non de la syntaxe. Des fonctionnalités comme un système de saisie vous offrent de réelles garanties, même si elles peuvent vous obliger à en écrire davantage .
back2dos

3
Demandez-vous ceci: la surcharge de l'opérateur est-elle uniquement du sucre syntaxique? Quelle que soit la réponse à cette question que vous maintenez vraie, c'est aussi la réponse à la question que vous avez posée;)
back2dos

5
@ back2dos: Tout à fait d'accord avec vous. J'ai lu trop souvent la phrase "tout n'est que du sucre syntaxique pour l'assembleur", et c'est clairement faux. Le sucre syntaxique est une syntaxe alternative (peut-être plus agréable) pour une syntaxe existante qui n'ajoute aucune nouvelle sémantique.
Giorgio

6
@Giorgio: c'est vrai! Il y a une définition précise dans l'article historique de Matthias Felleisen sur l'expressivité. Fondamentalement: le sucre syntaxique est purement local. Si vous devez modifier la structure globale du programme pour supprimer l'utilisation de la fonction de langue, ce n'est pas du sucre syntaxique. C'est-à-dire que la réécriture du code OO polymorphe dans l'assembleur implique généralement l'ajout d'une logique de répartition globale, qui n'est pas purement locale, donc l'OO n'est pas "juste du sucre syntaxique pour l'assembleur".
Jörg W Mittag

Réponses:


29

Pour répondre à cela, vous avez d'abord besoin d'une définition du «sucre syntaxique». J'irai avec Wikipédia :

En informatique, le sucre syntaxique est la syntaxe d'un langage de programmation conçu pour faciliter la lecture ou l'expression. Il rend le langage «plus doux» pour un usage humain: les choses peuvent être exprimées plus clairement, plus concis ou dans un style alternatif que certains préfèrent.

[...]

Plus précisément, une construction dans une langue est appelée sucre syntaxique si elle peut être supprimée de la langue sans aucun effet sur ce que la langue peut faire

Ainsi, selon cette définition, des fonctionnalités telles que les varargs de Java ou la compréhension de Scala sont du sucre syntaxique: elles se traduisent en fonctionnalités de langage sous-jacentes (un tableau dans le premier cas, des appels à map / flatmap / filter dans le second), et les supprimer serait pas changer les choses que vous pouvez faire avec la langue.

La surcharge de méthode, cependant, n'est pas du sucre syntaxique dans cette définition, car sa suppression changerait fondamentalement la langue (vous ne seriez plus en mesure de répartir vers un comportement distinct basé sur des arguments).

Certes, vous pouvez simuler une surcharge de méthode tant que vous disposez d'un moyen d'accéder aux arguments d'une méthode et pouvez utiliser une construction "if" basée sur les arguments qui vous sont fournis. Mais si vous considérez ce sucre syntaxique, vous devriez considérer que tout ce qui se trouve au-dessus d'une machine de Turing est également du sucre syntaxique.


22
Supprimer la surcharge ne changerait pas ce que la langue peut faire. Vous pouvez toujours faire exactement les mêmes choses qu'avant; il vous suffirait de renommer quelques méthodes. C'est un changement plus trivial que les boucles désucrantes.
Doval

9
Comme je l'ai dit, vous pourriez adopter l'approche selon laquelle tous les langages (y compris les langages machine) sont simplement du sucre syntaxique au-dessus d'une machine de Turing.
kdgregory

9
Comme je le vois, la surcharge de méthode vous permet simplement de faire sum(numbersArray)et sum(numbersList)au lieu de sumArray(numbersArray)et sumList(numbersList). Je suis d'accord avec Doval, cela ressemble à du simple sucre syntaxique.
Aviv Cohn

3
La plupart de la langue. Essayez la mise en œuvre instanceof, les classes, l' héritage, les interfaces, les génériques, la réflexion ou spécificateurs d'accès à l' aide if, whileet les opérateurs booléens, avec exactement le même sémantique . Pas de cas d'angle. Notez que je ne vous mets pas au défi de calculer les mêmes choses que les utilisations spécifiques de ces constructions. Je sais déjà que vous pouvez calculer n'importe quoi en utilisant la logique booléenne et la ramification / boucle. Je vous demande d'implémenter des copies parfaites de la sémantique de ces fonctionnalités de langage, y compris toutes les garanties statiques qu'elles fournissent (les vérifications de temps de compilation doivent encore être effectuées au moment de la compilation.)
Doval

6
@Doval, kdgregory: Pour définir le sucre syntaxique, vous devez le définir par rapport à certaines sémantiques. Si la seule sémantique que vous avez est "Qu'est-ce que ce programme calcule?", Alors il est clair que tout n'est que du sucre syntaxique pour une machine de Turing. D'un autre côté, si vous avez une sémantique dans laquelle vous pouvez parler des objets et de certaines opérations sur ceux-ci, la suppression d'une certaine syntaxe ne vous permettra plus d'exprimer ces opérations, même si le langage peut encore être Turing-complete.
Giorgio

13

Le terme sucre syntaxique se réfère généralement aux cas où la caractéristique est définie par une substitution. Le langage ne définit pas ce que fait une fonctionnalité, mais définit plutôt qu'elle est exactement équivalente à autre chose. Ainsi, par exemple, pour chaque boucle

for(Object alpha: alphas) {
}

Devient:

for(Iterator<Object> iter = alpha.iterator(); iter.hasNext()) {
   alpha = iter.next();
}

Ou prenez une fonction avec des arguments variables:

void foo(int... args);

foo(3, 4, 5);

Ce qui devient:

void Foo(int[] args);

foo(new int[]{3, 4, 5});

Il y a donc une substitution triviale de syntaxe pour implémenter la fonctionnalité en termes d'autres fonctionnalités.

Examinons la surcharge de méthode.

void foo(int a);
void foo(double b);

foo(4.5);

Cela peut être réécrit comme:

void foo_int(int a);
void foo_double(double b);

foo_double(4.5);

Mais ce n'est pas équivalent à cela. Dans le modèle de Java, c'est quelque chose de différent. foo(int a)n'implémente pas de foo_intfonction à créer. Java n'implémente pas la surcharge de méthode en donnant des noms amusants aux fonctions ambiguës. Pour compter comme sucre syntaxique, java devrait prétendre que vous avez vraiment écrit foo_intet foo_doublefonctionne, mais ce n'est pas le cas.


2
Je pense que personne n'a jamais dit que la transformation du sucre syntaxique devait être triviale. Même si c'était le cas, je trouve l'affirmation But, the transformation isn't trivial. At the least, you have to determine the types of the parameters.très sommaire car les types n'ont pas besoin d'être déterminés ; ils sont connus au moment de la compilation.
Doval

3
"Pour être considéré comme du sucre syntaxique, java devrait prétendre que vous avez vraiment écrit les fonctions foo_int et foo_double, mais ce n'est pas le cas." - tant que nous parlons de méthodes de surcharge et non de polymorphismes, quelle serait la différence entre foo(int)/ foo(double)et foo_int/ foo_double? Je ne connais pas vraiment très bien Java mais j'imagine qu'un tel changement de nom se produit vraiment dans JVM (enfin - probablement en utilisant foo(args)plutôt alors foo_args- il le fait au moins en C ++ avec la manipulation de symboles (ok - la manipulation de symboles est techniquement un détail d'implémentation et ne fait pas partie) de la langue).
Maciej Piechotka

2
@Doval: "Je pense que personne n'a jamais dit que la transformation du sucre syntaxique devait être triviale." - C'est vrai, mais ça doit être local . La seule définition utile du sucre syntaxique que je connaisse est tirée du célèbre article de Matthias Felleisen sur l'expressivité du langage, et en gros, il dit que si vous pouvez réécrire un programme écrit en langage L + y (c'est-à-dire un langage L avec une fonctionnalité y ) en langue L (c'est-à-dire un sous-ensemble de cette langue sans fonctionnalité y ) sans changer la structure globale du programme (c'est-à-dire ne faire que des changements locaux), alors y est le sucre syntaxique en L + y et fait
Jörg W Mittag

2
… N'augmente pas l' expressivité de L. Toutefois, si vous ne pouvez pas le faire, à savoir si vous devez apporter des modifications à la structure globale de votre programme, il est pas du sucre syntaxique et ne en fait faire L + y plus expressif que L . Par exemple, Java avec forboucle améliorée n'est pas plus expressif que Java sans lui. (Je dirais que c'est plus agréable, plus concis, plus lisible, et mieux dans l'ensemble, mais pas plus expressif.) Je ne suis pas sûr du cas de surcharge, cependant. Je vais probablement devoir relire le papier pour être sûr. Mon instinct dit qu'il est le sucre syntaxique, mais je ne suis pas sûr.
Jörg W Mittag

2
@MaciejPiechotka, si cela faisait partie de la définition du langage que les fonctions étaient ainsi renommées, et que vous pouviez accéder à la fonction sous ces noms, je pense que ce serait du sucre syntaxique. Mais parce que c'est caché comme détail d'implémentation, je pense que cela le disqualifie d'être du sucre syntaxique.
Winston Ewert

8

Étant donné que la manipulation des noms fonctionne, ne doit-il pas être rien de plus que du sucre syntaxique?

Il permet à l'appelant d'imaginer qu'il appelle la même fonction, alors qu'il ne l'est pas. Mais il pouvait connaître les vrais noms de toutes ses fonctions. Ce n'est que s'il était possible d'obtenir un polymorphisme retardé en passant une variable non typée dans une fonction typée et que son type soit établi de sorte que l'appel puisse passer à la bonne version en fonction de son nom que ce serait une véritable fonctionnalité de langage.

Malheureusement, je n'ai jamais vu une langue faire ça. Lorsqu'il y a ambiguïté, ces compilateurs ne le résolvent pas, ils insistent pour que le rédacteur le résolve pour eux.


La fonctionnalité que vous recherchez là-bas s'appelle "Multiple Dispatch". De nombreux langages le prennent en charge, notamment Haskell, Scala et (depuis 4.0) C #.
Iain Galloway

Je voudrais séparer les paramètres des classes de la surcharge de méthode directe. Dans le cas de surcharge de méthode directe, le programmeur écrit toutes les versions, le compilateur sait juste en choisir une. C'est juste du sucre syntaxique, et est résolu par une simple manipulation de nom, même pour plusieurs envois. --- En présence de paramètres sur les classes, le compilateur génère le code si nécessaire, et cela change complètement cela.
Jon Jay Obermark

2
Je pense que vous vous méprenez. Par exemple, en C #, si l'un des paramètres d'une méthode est dynamicalors la résolution de surcharge se produit au moment de l'exécution, pas au moment de la compilation . C'est ce qu'est la répartition multiple, et elle ne peut pas être reproduite en renommant les fonctions.
Iain Galloway

Plutôt cool. Je peux toujours tester le type de variable, cependant, il ne s'agit donc que d'une fonction intégrée superposée au sucre syntaxique. C'est une fonctionnalité linguistique, mais à peine.
Jon Jay Obermark

7

Selon la langue, c'est du sucre syntaxique ou non.

En C ++ par exemple, vous pouvez faire des choses en utilisant une surcharge et des modèles qui ne seraient pas possibles sans complications (écrivez manuellement toutes les instanciations du modèle ou ajoutez beaucoup de paramètres de modèle).

Notez que la répartition dynamique est une forme de surcharge, résolue dynamiquement sur certains paramètres (pour certaines langues seulement une spéciale, cela , mais pas toutes les langues sont si limitées), et je n'appellerais pas cette forme de surcharge syntaxique de sucre.


Je ne sais pas comment les autres réponses se portent tellement mieux lorsqu'elles sont essentiellement incorrectes.
Telastyn

5

Pour les langues contemporaines, c'est juste du sucre syntaxique; d'une manière complètement indépendante de la langue, c'est plus que cela.

Auparavant, cette réponse indiquait simplement que c'était plus que du sucre syntaxique, mais si vous voyez dans les commentaires, Falco a soulevé le fait qu'il y avait une pièce du puzzle que les langues contemporaines semblaient toutes manquer; ils ne mélangent pas la surcharge de méthode avec la détermination dynamique de la fonction à appeler dans la même étape. Cela sera clarifié plus tard.

Voici pourquoi cela devrait être plus.

Prenons un langage qui prend en charge la surcharge de méthode et les variables non typées. Vous pouvez avoir les prototypes de méthodes suivants:

bool someFunction(int arg);

bool someFunction(string arg);

Dans certaines langues, vous seriez probablement résigné à savoir au moment de la compilation lequel de ceux-ci serait appelé par une ligne de code donnée. Mais dans certaines langues, toutes les variables ne sont pas typées (ou elles sont toutes implicitement typées comme Objectou autre), alors imaginez construire un dictionnaire dont les clés correspondent à des valeurs de différents types:

dict roomNumber; // some hotels use numbers, some use letters, and some use
                 // alphanumerical strings.  In some languages, built-in dictionary
                 // types automatically use untyped values for their keys to map to,
                 // so it makes more sense then to allow for both ints and strings in
                 // your code.

Maintenant, que se passe-t-il si vous souhaitez postuler someFunctionà l'un de ces numéros de chambre? Vous appelez cela:

someFunction(roomNumber[someSortOfKey]);

Est someFunction(int)appelé, ou est someFunction(string)appelé? Ici, vous voyez un exemple où ce ne sont pas des méthodes totalement orthogonales, en particulier dans les langues de niveau supérieur. Le langage doit déterminer - lors de l'exécution - lequel de ces éléments appeler, il doit donc toujours les considérer comme étant au moins un peu la même méthode.

Pourquoi ne pas simplement utiliser des modèles? Pourquoi ne pas simplement utiliser un argument non typé?

Flexibilité et contrôle plus fin. Parfois, utiliser des modèles / arguments non typés est une meilleure approche, mais parfois ils ne le sont pas.

Vous devez penser aux cas où, par exemple, vous pourriez avoir deux signatures de méthode qui prennent chacune un intet un stringcomme arguments, mais où l'ordre est différent dans chaque signature. Vous pouvez très bien avoir une bonne raison de le faire, car l'implémentation de chaque signature peut faire en grande partie la même chose, mais avec une torsion légèrement différente; la journalisation peut être différente, par exemple. Ou même s'ils font exactement la même chose, vous pouvez être en mesure de glaner automatiquement certaines informations uniquement dans l'ordre dans lequel les arguments ont été spécifiés. Techniquement, vous pouvez simplement utiliser des instructions pseudo-switch pour déterminer le type de chacun des arguments transmis, mais cela devient compliqué.

Alors, cet exemple suivant est-il une mauvaise pratique de programmation?

bool stringIsTrue(int arg)
{
    if (arg.toString() == "0")
    {
        return false;
    }
    else
    {
        return true;
    }
}

bool stringIsTrue(Object arg)
{
    if (arg.toString() == "0")
    {
        return false;
    }
    else
    {
        return true;
    }
}

bool stringIsTrue(string arg)
{
    if (arg == "0")
    {
        return false;
    }
    else
    {
        return true;
    }
}

Oui, en gros. Dans cet exemple particulier, cela pourrait empêcher quelqu'un d'essayer de l'appliquer à certains types primitifs et de récupérer un comportement inattendu (ce qui pourrait être une bonne chose); mais supposons simplement que j'ai abrégé le code ci-dessus, et que vous avez en fait des surcharges pour tous les types primitifs, ainsi que pour Objects. Ensuite, ce bit de code suivant est vraiment plus approprié:

bool stringIsTrue(untyped arg)
{
    if (arg.toString() == "0")
    {
        return false;
    }
    else
    {
        return true;
    }
}

Mais que se passe-t-il si vous n'avez besoin de l'utiliser que pour ints et strings, et si vous voulez qu'il renvoie true en fonction de conditions plus simples ou plus compliquées en conséquence? Ensuite, vous avez une bonne raison d'utiliser la surcharge:

bool appearsToBeFirstFloor(int arg)
{
    if (arg.digitAt(0) == 1)
    {
        return true;
    }
    else
    {
        return false;
    }
}

bool appearsToBeFirstFloor(string arg)
{
    string firstCharacter = arg.characterAt(0);
    if (firstCharacter.isDigit())
    {
        return appearsToBeFirstFloor(int(firstCharacter));
    }
    else if (firstCharacter.toUpper() == "A")
    {
        return true;
    }
    else
    {
        return false;
    }
}

Mais bon, pourquoi ne pas simplement donner à ces fonctions deux noms différents? Vous avez toujours la même quantité de contrôle à grain fin, n'est-ce pas?

Parce que, comme indiqué précédemment, certains hôtels utilisent des chiffres, certains utilisent des lettres et certains utilisent un mélange de chiffres et de lettres:

appearsToBeFirstFloor(roomNumber[someSortOfKey]);

// will treat ints and strings differently, without you having to write extra code
// every single spot where the function is being called

Ce n'est toujours pas exactement le même code exact que j'utiliserais dans la vraie vie, mais cela devrait illustrer le point que je fais très bien.

Mais ... Voici pourquoi ce n'est pas plus que du sucre syntaxique dans les langues contemporaines.

Falco a soulevé le point dans les commentaires que les langages actuels ne mélangent fondamentalement pas la surcharge de méthode et la sélection de fonction dynamique dans la même étape. La façon dont j'ai précédemment compris que certaines langues fonctionnaient était que vous pouviez surcharger appearsToBeFirstFloordans l'exemple ci-dessus, puis la langue déterminerait au moment de l'exécution la version de la fonction à appeler, en fonction de la valeur d'exécution de la variable non typée. Cette confusion provient en partie du travail avec des sortes de langages ECMA, comme ActionScript 3.0, dans lequel vous pouvez facilement randomiser quelle fonction est appelée sur une certaine ligne de code lors de l'exécution.

Comme vous le savez peut-être, ActionScript 3 ne prend pas en charge la surcharge de méthode. Quant à VB.NET, vous pouvez déclarer et définir des variables sans affecter explicitement un type, mais lorsque vous essayez de passer ces variables comme arguments à des méthodes surchargées, il ne veut toujours pas lire la valeur d'exécution pour déterminer la méthode à appeler; il veut plutôt trouver une méthode avec des arguments de type Objectou aucun type ou quelque chose comme ça. Ainsi , le intrapport stringexemple ci - dessus ne marcherait pas dans cette langue non plus . C ++ a des problèmes similaires, car lorsque vous utilisez quelque chose comme un pointeur void ou un autre mécanisme comme celui-ci, il vous oblige toujours à lever l'ambiguïté manuellement lors de la compilation.

Donc, comme le dit le premier en-tête ...

Pour les langues contemporaines, c'est juste du sucre syntaxique; d'une manière complètement indépendante de la langue, c'est plus que cela. Rendre la surcharge de méthode plus utile et plus pertinente, comme dans l'exemple ci-dessus, peut en fait être une bonne caractéristique à ajouter à un langage existant (comme cela a été largement implicitement demandé pour AS3), ou il pourrait également servir comme l'un des nombreux piliers fondamentaux pour la création d'un nouveau langage procédural / orienté objet.


3
Pouvez-vous nommer des langages qui gèrent vraiment Function-Dispatch à l'exécution et non à la compilation? TOUTES les langues que je connais nécessitent une certitude au moment de la compilation de la fonction qui est appelée ...
Falco

@Falco ActionScript 3.0 le gère au moment de l'exécution. Vous pouvez, par exemple, utiliser une fonction qui retourne au hasard l'une des trois chaînes, puis utiliser sa valeur de retour pour appeler l'une des trois fonctions au hasard: this[chooseFunctionNameAtRandom](); si elle chooseFunctionNameAtRandom()renvoie l'un ou l'autre "punch","kick" ou "dodge", alors vous pouvez thusly mettre en œuvre un très aléatoire simple élément, par exemple, l'IA d'un ennemi dans un jeu Flash.
Panzercrisis

1
Oui - mais ce sont toutes les deux de véritables méthodes sémantiques pour obtenir une répartition dynamique des fonctions, Java les a également. Mais ils sont différents de la surcharge, la surcharge est du sucre statique et simplement syntaxique, tandis que la répartition dynamique et l'héritage sont de véritables fonctionnalités de langage, qui offrent de nouvelles fonctionnalités!
Falco

1
... J'ai également essayé des pointeurs void en C ++, ainsi que des pointeurs de classe de base, mais le compilateur voulait que je le désambiguïse moi-même avant de le passer à une fonction. Alors maintenant, je me demande s'il faut supprimer cette réponse. Cela commence à ressembler presque toujours aux langages pour combiner le choix de fonction dynamique avec la surcharge de fonction dans la même instruction ou instruction, puis s'éloigner à la dernière seconde. Ce serait une belle fonctionnalité de langage cependant; peut-être que quelqu'un a besoin de faire un langage qui a ça.
Panzercrisis

1
Laissez la réponse rester, pensez peut-être à inclure certaines de vos recherches à partir des commentaires dans la réponse?
Falco

2

Cela dépend vraiment de votre définition du "sucre syntaxique". Je vais essayer de répondre à certaines des définitions qui me viennent à l'esprit:

  1. Une fonctionnalité est du sucre syntaxique lorsqu'un programme qui l'utilise peut toujours être traduit dans un autre qui n'utilise pas la fonctionnalité.

    Ici, nous supposons qu'il existe un ensemble primitif de fonctionnalités qui ne peuvent pas être traduites: en d'autres termes, aucune boucle du type "vous pouvez remplacer la fonctionnalité X en utilisant la fonctionnalité Y" et "vous pouvez remplacer la fonctionnalité Y par la fonctionnalité X". Si l'une des deux est vraie, alors l'autre caractéristique peut être exprimée en termes de caractéristiques qui ne sont pas les premières ou c'est une caractéristique primitive.

  2. Identique à la définition 1, mais avec l'exigence supplémentaire que le programme traduit soit aussi sûr pour le type que le premier, c'est-à-dire qu'en désuchant, vous ne perdez aucune sorte d'information.

  3. La définition de l'OP: une caractéristique est le sucre syntaxique si sa traduction ne change pas la structure du programme mais ne nécessite que des "changements locaux".

Prenons Haskell comme exemple de surcharge. Haskell fournit une surcharge définie par l'utilisateur via des classes de type. Par exemple, les opérations +et *sont définies dans la Numclasse de type et tout type qui possède une instance (complète) de cette classe peut être utilisé avec +. Par exemple:

instance Num a => Num (b, a) where
    (x, y) + (_, y') = (x, y + y')
    -- other definitions

("Hello", 1) + ("World", 3) -- -> ("Hello", 4)

Une chose bien connue des classes de types de Haskell est que vous pouvez vous en débarrasser . C'est-à-dire que vous pouvez traduire n'importe quel programme qui utilise des classes de type dans un programme équivalent qui ne les utilise pas.

La traduction est assez simple:

  • Étant donné une définition de classe:

    class (P_1 a, ..., P_n a) => X a where
        op_1 :: t_1   ... op_m :: t_m
    

    Vous pouvez le traduire en un type de données algébrique:

    data X a = X {
        X_P_1 :: P_1 a, ... X_P_n :: P_n a,
        X_op_1 :: t_1, ..., X_op_m :: t_m
    }
    

    Ici X_P_iet X_op_isont des sélecteurs . C'est-à-dire qu'une valeur de type X aappliquée X_P_1à la valeur retournera la valeur stockée dans ce champ, donc ce sont des fonctions de type X a -> P_i a(ou X a -> t_i).

    Pour une anologie très approximative, vous pouvez penser aux valeurs de type X acomme structs et si if xest de type, X ales expressions:

    X_P_1 x
    X_op_1 x
    

    pourrait être considéré comme:

    x.X_P_1
    x.X_op_1
    

    (Il est facile d'utiliser uniquement des champs positionnels au lieu des champs nommés, mais les champs nommés sont plus faciles à gérer dans les exemples et évitent un code de plaque chauffante).

  • Étant donné une déclaration d'instance:

    instance (C_1 a_1, ..., C_n a_n) => X (T a_1 ... a_n) where
        op_1 = ...; ...;  op_m = ...
    

    Vous pouvez le traduire en une fonction qui, étant donné les dictionnaires des C_1 a_1, ..., C_n a_nclasses, renvoie une valeur de dictionnaire (c'est-à-dire une valeur de type X a) pour le typeT a_1 ... a_n .

    En d'autres termes, l'instance ci-dessus peut être traduite en une fonction comme:

    f :: C_1 a_1 -> ... -> C_n a_n -> X (T a_1 ... a_n)
    

    (Notez que cela npeut être 0).

    Et en fait, nous pouvons le définir comme:

    f c1 ... cN = X {X_P_1=get_P_1_T, X_P_n=get_P_n_T,
                     X_op_1=op_1, ..., X_op_m=op_m}
        where
            op_1 = ...
            ...
            op_m = ...
    

    op_1 = ...pour op_m = ...sont les définitions figurant dans la instancedéclaration et get_P_i_Tsont les fonctions définies par l' P_iinstance du Tgenre (ceux - ci doivent exister parce que P_is sont superclasses de X).

  • Étant donné un appel à une fonction surchargée:

    add :: Num a => a -> a -> a
    add x y = x + y
    

    Nous pouvons explicitement passer les dictionnaires relatifs aux contraintes de classe et obtenir un appel équivalent:

    add :: Num a -> a -> a -> a
    add dictNum x y = ((+) dictNum) x y
    

    Notez comment les contraintes de classe sont simplement devenues un nouvel argument. Le +dans le programme traduit est le sélecteur comme expliqué précédemment. En d'autres termes, la addfonction traduite , étant donné le dictionnaire pour le type de son argument, "décompressera" d'abord la fonction réelle pour calculer le résultat en utilisant (+) dictNumpuis appliquera cette fonction aux arguments.

Ceci est juste un croquis très rapide sur le tout. Si vous êtes intéressé, vous devriez lire les articles de Simon Peyton Jones et al.

Je crois qu'une approche similaire pourrait également être utilisée pour la surcharge dans d'autres langues.

Cependant, cela montre que, si votre définition du sucre syntaxique est (1), alors surcharge est du sucre syntaxique . Parce que vous pouvez vous en débarrasser.

Cependant, le programme traduit perd certaines informations sur le programme d'origine. Par exemple, il n'impose pas que les instances des classes parentes existent. (Même si les opérations pour extraire les dictionnaires du parent doivent toujours être de ce type, vous pouvez passer undefinedou d'autres valeurs polymorphes pour pouvoir créer une valeur pour X ysans construire les valeurs pour P_i y, donc la traduction ne perd pas tout le type de sécurité). Ce n'est donc pas du sucre syntactique selon (2)

Quant à (3). Je ne sais pas si la réponse doit être oui ou non.

Je dirais non parce que, par exemple, une déclaration d'instance devient une définition de fonction. Les fonctions surchargées obtiennent un nouveau paramètre (ce qui signifie qu'il modifie à la fois la définition et tous les appels).

Je dirais que oui, car les deux programmes sont toujours mappés l'un à l'autre, de sorte que la "structure" n'est pas vraiment modifiée.


Cela dit, je dirais que les avantages pragmatiques introduits par la surcharge sont si importants que l'utilisation d'un terme "dérogatoire" tel que "sucre syntaxique" ne semble pas correcte.

Vous pouvez traduire toute la syntaxe Haskell dans un langage Core très simple (ce qui se fait en fait lors de la compilation), de sorte que la plupart de la syntaxe Haskell pourrait être considérée comme du «sucre syntaxique» pour quelque chose qui n'est que du lambda-calcul et un peu de nouvelles constructions. Cependant, nous pouvons convenir que les programmes Haskell sont beaucoup plus faciles à gérer et sont très concis, tandis que les programmes traduits sont beaucoup plus difficiles à lire ou à penser.


2

Si la répartition est résolue au moment de la compilation, en fonction uniquement du type statique de l'expression d'argument, vous pouvez certainement affirmer qu'il s'agit de "sucre syntaxique" remplaçant deux méthodes différentes avec des noms différents, à condition que le programmeur "connaisse" le type statique et pourrait simplement utiliser le bon nom de méthode à la place du nom surchargé. C'est aussi une forme de polymorphisme statique, mais sous cette forme limitée, il n'est généralement pas très puissant.

Bien sûr, il serait gênant de devoir changer les noms des méthodes que vous appelez chaque fois que vous changez le type d'une variable, mais par exemple dans le langage C, il est considéré comme une nuisance gérable, donc C n'a pas de surcharge de fonction (bien que il a maintenant des macros génériques).

Dans les modèles C ++, et dans n'importe quel langage qui fait une déduction de type statique non triviale, vous ne pouvez pas vraiment affirmer qu'il s'agit de "sucre syntaxique" à moins que vous ne souteniez également que la déduction de type statique est du "sucre syntaxique". Ce serait une nuisance de ne pas avoir de modèles, et dans le contexte de C ++, ce serait une "nuisance ingérable", car ils sont tellement idiomatiques pour le langage et ses bibliothèques standard. Donc en C ++ c'est plutôt plus qu'une bonne aide, c'est important pour le style du langage, et donc je pense que vous devez l'appeler plus que "sucre syntaxique".

En Java, vous pourriez considérer cela plus qu'une simple commodité en considérant par exemple le nombre de surcharges de PrintStream.printet PrintStream.println. Mais alors, il existe autant de DataInputStream.readXméthodes que Java ne surcharge pas le type de retour, donc dans un certain sens, c'est juste pour plus de commodité. Ce sont tous des types primitifs.

Je ne me souviens pas ce qui se passe en Java si j'ai des classes Aet Bprolongeant O, je surcharger les méthodes foo(O), foo(A)et foo(B), puis dans un générique que <T extends O>j'appelle foo(t)test une instance de T. Dans le cas où Test- Ace que j'obtiens la répartition basée sur la surcharge ou est-ce comme si j'avais appelé foo(O)?

Dans le premier cas, les surcharges de la méthode Java sont meilleures que le sucre, de la même manière que les surcharges C ++. En utilisant votre définition, je suppose qu'en Java, je pourrais écrire localement une série de vérifications de type (ce qui serait fragile, car de nouvelles surcharges foonécessiteraient des vérifications supplémentaires). En plus d'accepter cette fragilité, je ne peux pas faire de changement local sur le site d'appel pour bien faire les choses, au lieu de cela, je devrais renoncer à écrire du code générique. Je dirais que la prévention du code gonflé pourrait être du sucre syntaxique, mais la prévention du code fragile est plus que cela. Pour cette raison, le polymorphisme statique en général est plus que du sucre syntaxique. La situation dans une langue particulière peut être différente, selon la mesure dans laquelle la langue vous permet de "ne pas connaître" le type statique.


En Java, les surcharges sont résolues au moment de la compilation. Compte tenu de l'utilisation de l'effacement de type, il leur serait impossible de faire autrement. En outre, même sans effacement de type, si T:Animalc'est est de type SiameseCatet existants sont surcharges Cat Foo(Animal), SiameseCat Foo(Cat)et Animal Foo(SiameseCat)qui surcharge doit être sélectionné si Test SiameseCat?
supercat

@supercat: a du sens. J'aurais donc pu trouver la réponse sans m'en souvenir (ou, bien sûr, l'exécuter). Par conséquent, les surcharges Java ne sont pas meilleures que le sucre de la même manière que les surcharges C ++ sont liées au code générique. Il reste possible qu'il y ait une autre manière dont ils sont meilleurs qu'une simple transformation locale. Je me demande si je devrais changer mon exemple en C ++, ou le laisser comme Java-imaginaire-qui-n'est-pas-réel-Java.
Steve Jessop

Les surcharges peuvent être utiles dans les cas où les méthodes ont des arguments facultatifs, mais elles peuvent également être dangereuses. Supposons que la ligne long foo=Math.round(bar*1.0001)*5soit modifiée en long foo=Math.round(bar)*5. Comment cela affecterait-il la sémantique s'il était barégal, par exemple, 123456789L?
supercat

@supercat Je dirais que le vrai danger est la conversion implicite de longà double.
Doval

@Doval: À double?
supercat

1

Il semble que le "sucre syntaxique" semble désobligeant, inutile ou frivole. C'est pourquoi la question déclenche de nombreuses réponses négatives.

Mais vous avez raison, la surcharge de méthode n'ajoute aucune fonctionnalité au langage, à l'exception de la possibilité d'utiliser le même nom pour différentes méthodes. Vous pouvez rendre le type de paramètre explicite, le programme fonctionnera toujours de la même manière.

Il en va de même pour les noms de package. La chaîne n'est qu'un sucre syntaxique pour java.lang.String.

En fait, une méthode comme

void fun(int i, String c);

dans la classe MyClass devrait s'appeler quelque chose comme "my_package_MyClass_fun_int_java_lang_String". Cela identifierait la méthode de manière unique. (La JVM fait quelque chose comme ça en interne). Mais vous ne voulez pas écrire ça. C'est pourquoi le compilateur vous permettra d'écrire fun (1, "one") et d'identifier de quelle méthode il s'agit.

Il y a cependant une chose que vous pouvez faire avec la surcharge: si vous surchargez une méthode avec le même nombre d'arguments, le compilateur trouvera automatiquement la version qui convient le mieux à l'argument donné en faisant correspondre les arguments, non seulement avec des types égaux, mais aussi où l'argument donné est une sous-classe de l'argument déclaré.

Si vous avez deux procédures surchargées

addParameter(String name, Object value);
addParameter(String name, Date value);

vous n'avez pas besoin de savoir qu'il existe une version spécifique de la procédure pour les dates. addParameter ("hello", "world) appellera la première version, addParameter (" now ", new Date ()) appellera la seconde.

Bien sûr, vous devez éviter de surcharger une méthode avec une autre méthode qui fait une chose complètement différente.


1

Fait intéressant, la réponse à cette question dépendra de la langue.

Plus précisément, il existe une interaction entre la surcharge et la programmation générique (*), et selon la façon dont la programmation générique est implémentée, il peut s'agir simplement de sucre syntaxique (Rust) ou absolument nécessaire (C ++).

Autrement dit, lorsque la programmation générique est implémentée avec des interfaces explicites (dans Rust ou Haskell, ce sont des classes de type), alors la surcharge n'est que du sucre syntaxique; ou pourrait même ne pas faire partie de la langue.

D'un autre côté, lorsque la programmation générique est implémentée avec le typage canard (dynamique ou statique), le nom de la méthode est un contrat essentiel, et donc la surcharge est obligatoire pour que le système fonctionne.

(*) Utilisé dans le sens d'écrire une méthode une fois, pour fonctionner sur différents types de manière uniforme.


0

Dans certaines langues, il ne s'agit sans doute que de sucre syntaxique. Cependant, ce dont il s'agit est un sucre qui dépend de votre point de vue. Je vais laisser cette discussion pour plus tard dans cette réponse.

Pour l'instant, je voudrais juste noter que dans certaines langues, ce n'est certainement pas du sucre syntaxique. Du moins pas sans vous obliger à utiliser une logique / algorithme complètement différent pour implémenter la même chose. C'est comme affirmer que la récursivité est du sucre syntaxique (ce qui est le cas puisque vous pouvez écrire tous les algorithmes récursifs avec une boucle et une pile).

Un exemple d'une utilisation très difficile à remplacer vient d'un langage qui, ironiquement, n'appelle pas cette fonctionnalité "surcharge de fonction". Au lieu de cela, on l'appelle "pattern matching" (qui peut être considéré comme un surensemble de surcharge car nous pouvons surcharger non seulement les types mais les valeurs).

Voici l'implémentation naïve classique de la fonction Fibonacci dans Haskell:

fib 0 = 0
fib 1 = 1
fib n = fib (n-1) + fib (n-2)

On peut dire que les trois fonctions peuvent être remplacées par un if / else comme cela se fait couramment dans n'importe quelle autre langue. Mais cela rend fondamentalement la définition tout à fait simple:

fib n = fib (n-1) + fib (n-2)

beaucoup plus compliqué et n'exprime pas directement la notion mathématique de la séquence de Fibonacci.

Il peut donc parfois s'agir de sucre de syntaxe si la seule utilisation est de vous permettre d'appeler une fonction avec des arguments différents. Mais parfois, c'est beaucoup plus fondamental que cela.


Maintenant, pour la discussion de ce que la surcharge de l'opérateur peut être un sucre. Vous avez identifié un cas d'utilisation - il peut être utilisé pour implémenter des fonctions similaires qui prennent différents arguments. Donc:

function print (string x) { stdout.print(x) };
function print (number x) { stdout.print(x.toString) };

peut également être mis en œuvre comme:

function printString (string x) {...}
function printNumber (number x) {...}

ou même:

function print (auto x) {
    if (x instanceof String) {...}
    if (x instanceof Number) {...}
}

Mais la surcharge d'opérateur peut également être un sucre pour l'implémentation d'arguments optionnels (certaines langues ont une surcharge d'opérateur mais pas d'arguments optionnels):

function print (string x) {...}
function print (string x, stream io) {...}

peut être utilisé pour implémenter:

function print (string x, stream io=stdout) {...}

Dans une telle langue (google "langue Ferite"), la suppression de la surcharge de l'opérateur supprime considérablement une fonctionnalité - les arguments facultatifs. Accordé dans les langues avec les deux fonctionnalités (c ++), la suppression de l'une ou de l'autre n'aura aucun effet net car l'une ou l'autre peut être utilisée pour implémenter des arguments facultatifs.


Haskell est un bon exemple de la raison pour laquelle la surcharge des opérateurs n'est pas du sucre syntaxique, mais je pense qu'un meilleur exemple serait de déconstruire un type de données algébrique avec une correspondance de modèle (quelque chose que je sache impossible sans correspondance de modèle).
11684

@ 11684: Pouvez-vous citer un exemple? Honnêtement, je ne connais pas du tout Haskell, mais j'ai trouvé son motif assorti d'une élégance sublime lorsque j'ai vu cet exemple de fib (sur computerphile sur youtube).
slebetman

Étant donné un type de données tel que data PaymentInfo = CashOnDelivery | Adress String | UserInvoice CustomerInfovous pouvez faire correspondre les modèles sur les constructeurs de types.
11684

Comme ceci: getPayment :: PaymentInfo -> a getPayment CashOnDelivery = error "Should have been paid already" getPayment (Adress addr) = -- code to notify administration to send a bill getPayment (UserInvoice cust) = --whatever. I took the data type from a Haskell tutorial and have no idea what an invoice is. J'espère que ce commentaire est quelque peu compréhensible.
11684

0

Je pense que c'est du sucre syntaxique simple dans la plupart des langues (du moins tout ce que je sais ...) car elles nécessitent toutes un appel de fonction sans ambiguïté au moment de la compilation. Et le compilateur remplace simplement l'appel de fonction par un pointeur explicite vers la bonne signature d'implémentation.

Exemple en Java:

String s; int i;
mangle(s);  // Translates to CALL ('mangle':LString):(s)
mangle(i);  // Translates to CALL ('mangle':Lint):(i)

Donc, à la fin, il pourrait être complètement remplacé par une simple macro de compilation avec recherche et remplacement, remplaçant la fonction mangle surchargée par mangle_String et mangle_int - puisque la liste d'arguments fait partie de l'identificateur de fonction éventuel, c'est pratiquement ce qui se passe -> et il ne s'agit donc que de sucre syntaxique.

Maintenant, s'il y a un langage, où la fonction est vraiment déterminée au moment de l'exécution, comme avec les méthodes substituées dans les objets, ce serait différent. Mais je ne pense pas qu'il existe un tel langage, car method.overloading est sujet à ambiguïté, que le compilateur ne peut pas résoudre et qui doit être géré par le programmeur avec un cast explicite. Cela ne peut pas être fait lors de l'exécution.


0

En Java, les informations sont compilées et celle des surcharges qui est appelée est décidée au moment de la compilation.

Ce qui suit est un extrait de sun.misc.Unsafe(l'utilitaire pour Atomics) tel qu'il est affiché dans l'éditeur de fichiers de classe d'Eclipse.

  // Method descriptor #143 (Ljava/lang/Object;I)I (deprecated)
  // Stack: 4, Locals: 3
  @java.lang.Deprecated
  public int getInt(java.lang.Object arg0, int arg1);
    0  aload_0 [this]
    1  aload_1 [arg0]
    2  iload_2 [arg1]
    3  i2l
    4  invokevirtual sun.misc.Unsafe.getInt(java.lang.Object, long) : int [231]
    7  ireturn
      Line numbers:
        [pc: 0, line: 213]

comme vous pouvez le voir, les informations de type de la méthode appelée (ligne 4) sont incluses dans l'appel.

Cela signifie que vous pouvez créer un compilateur java qui prend les informations de type. Par exemple, en utilisant une telle notation, la source ci-dessus serait alors:

@Deprecated
public in getInt(Object arg0, int arg1){
     return getInt$(Object,long)(arg0, arg1);
}

et le casting à long serait facultatif.

Dans d'autres langages compilés de type statique, vous verrez une configuration similaire où le compilateur décidera quelle surcharge sera appelée en fonction des types et l'inclura dans la liaison / l'appel.

L'exception est les bibliothèques dynamiques C où les informations de type ne sont pas incluses et si vous essayez de créer une fonction surchargée, l'éditeur de liens se plaindra.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.