264 votes

Surcharge des fonctions par type de retour ?

Pourquoi les langages courants typés statiquement ne supportent-ils pas la surcharge des fonctions/méthodes par type de retour ? Je n'en connais aucun qui le fasse. Cela ne semble pas moins utile ou raisonnable que de supporter la surcharge par type de paramètre. Comment se fait-il que ce soit si peu populaire ?

2 votes

535voto

A. Rex Points 17899

Contrairement à ce que d'autres disent, la surcharge par type de retour es possible et es fait par certaines langues modernes. L'objection habituelle est que dans un code comme

int func();
string func();
int main() { func(); }

vous ne pouvez pas dire lequel func() Ce problème peut être résolu de plusieurs façons :

  1. Disposer d'une méthode prévisible pour déterminer quelle fonction est appelée dans une telle situation.
  2. Lorsqu'une telle situation se produit, il s'agit d'une erreur de compilation. Cependant, il faut avoir une syntaxe qui permette au programmeur de désambiguïser, par exemple int main() { (string)func(); } .
  3. Je n'ai pas d'effets secondaires. Si vous n'avez pas d'effets secondaires et que vous n'utilisez jamais la valeur de retour d'une fonction, le compilateur peut éviter d'appeler la fonction en premier lieu.

Deux des langues que je pratique régulièrement ( ab )utiliser la surcharge par type de retour : Perl y Haskell . Laissez-moi vous décrire ce qu'ils font.

Sur Perl il existe une distinction fondamentale entre scalaire y liste (et d'autres, mais nous ferons comme s'il y en avait deux). Chaque fonction intégrée en Perl peut faire des choses différentes en fonction du contexte contexte dans lequel il est appelé. Par exemple, le join force le contexte de liste (sur la chose jointe) alors que l'opérateur scalar L'opérateur force le contexte scalaire, donc compare :

print join " ", localtime(); # printed "58 11 2 14 0 109 3 13 0" for me right now
print scalar localtime(); # printed "Wed Jan 14 02:12:44 2009" for me right now.

Chaque opérateur en Perl fait quelque chose dans un contexte scalaire et quelque chose dans un contexte de liste, et ils peuvent être différents, comme illustré. (Ce n'est pas seulement pour les opérateurs aléatoires comme localtime . Si vous utilisez un tableau @a dans un contexte de liste, il renvoie le tableau, tandis que dans un contexte scalaire, il renvoie le nombre d'éléments. Ainsi, par exemple print @a imprime les éléments, tandis que print 0+@a imprime la taille). En outre, chaque opérateur peut force un contexte, par exemple l'addition + force le contexte scalaire. Chaque entrée dans man perlfunc documente ceci. Par exemple, voici une partie de l'entrée pour glob EXPR :

Dans un contexte de liste, renvoie une liste (éventuellement vide) d'expansions de noms de fichiers sur la valeur de EXPR comme l'interpréteur de commandes standard shell Unix /bin/csh ferait. Dans contexte scalaire, glob itère à travers de telles expansions de nom de fichier, retournant undef lorsque la liste est épuisée.

Maintenant, quelle est la relation entre la liste et le contexte scalaire ? Et bien, man perlfunc dit

N'oubliez pas la règle importante suivante : Il n'y a pas de règle qui relie le comportement d'une expression dans un à son comportement dans un contexte scalaire ou vice versa. Elle peut faire deux choses totalement différentes. Chaque opérateur et fonction décide quelle type de valeur qu'il serait le plus appropriée de retourner dans un scalaire. Certains opérateurs renvoient la longueur de la liste qui aurait été retournée dans un contexte de liste. Certains opérateurs opérateurs renvoient la première valeur de la la liste. Certains opérateurs renvoient la dernière valeur de la liste. Certains opérateurs opérateurs renvoient un compte des opérations opérations réussies. En général, ils font ce que ce que vous voulez, sauf si vous voulez de la cohérence.

Il ne s'agit donc pas simplement d'avoir une seule fonction, puis de faire une simple conversion à la fin. En fait, j'ai choisi la localtime exemple pour cette raison.

Ce n'est pas seulement les built-ins qui ont ce comportement. Tout utilisateur peut définir une telle fonction en utilisant wantarray qui permet de faire la distinction entre les contextes list, scalaire et void. Ainsi, par exemple, vous pouvez décider de ne rien faire si vous êtes appelé dans un contexte void.

Maintenant, vous pouvez vous plaindre que ce n'est pas vrai la surcharge par valeur de retour parce que vous n'avez qu'une seule fonction, qui est informée du contexte dans lequel elle est appelée et qui agit ensuite sur cette information. Cependant, c'est clairement équivalent (et analogue à la façon dont Perl ne permet pas la surcharge habituelle littéralement, mais une fonction peut simplement examiner ses arguments). De plus, cela résout joliment la situation ambiguë mentionnée au début de cette réponse. Perl ne se plaint pas de ne pas savoir quelle méthode appeler ; il l'appelle tout simplement. Tout ce qu'il lui reste à faire est de déterminer dans quel contexte la fonction a été appelée, ce qui est toujours possible :

sub func {
    if( not defined wantarray ) {
        print "void\n";
    } elsif( wantarray ) {
        print "list\n";
    } else {
        print "scalar\n";
    }
}

func(); # prints "void"
() = func(); # prints "list"
0+func(); # prints "scalar"

(Note : il se peut que je dise parfois opérateur Perl alors que je veux dire fonction. Ce n'est pas crucial pour cette discussion).

Haskell adopte l'autre approche, à savoir ne pas avoir d'effets secondaires. Il dispose également d'un système de types fort, ce qui vous permet d'écrire du code comme le suivant :

main = do n <- readLn
          print (sqrt n) -- note that this is aligned below the n, if you care to run this

Ce code lit un nombre à virgule flottante à partir de l'entrée standard, et imprime sa racine carrée. Mais qu'est-ce qui est surprenant dans tout cela ? Eh bien, le type de readLn es readLn :: Read a => IO a . Ce que cela signifie, c'est que pour tout type qui peut être Read (formellement, chaque type qui est une instance de la classe Read classe de type), readLn peut le lire. Comment Haskell a-t-il su que je voulais lire un nombre à virgule flottante ? Eh bien, le type de sqrt es sqrt :: Floating a => a -> a ce qui signifie essentiellement que sqrt ne peut accepter que des nombres à virgule flottante comme entrées, et donc Haskell a déduit ce que je voulais.

Que se passe-t-il lorsque Haskell ne peut pas déduire ce que je veux ? Eh bien, il y a plusieurs possibilités. Si je n'utilise pas du tout la valeur de retour, Haskell n'appellera tout simplement pas la fonction en premier lieu. Cependant, si je faire utiliser la valeur de retour, alors Haskell se plaindra qu'il ne peut pas déduire le type :

main = do n <- readLn
          print n
-- this program results in a compile-time error "Unresolved top-level overloading"

Je peux résoudre l'ambiguïté en spécifiant le type que je veux :

main = do n <- readLn
          print (n::Int)
-- this compiles (and does what I want)

Quoi qu'il en soit, ce que toute cette discussion signifie, c'est que la surcharge par valeur de retour est possible et se fait, ce qui répond à une partie de votre question.

L'autre partie de votre question est de savoir pourquoi plus de langues ne le font pas. Je laisse à d'autres le soin d'y répondre. Cependant, quelques commentaires : la raison principale est probablement que la possibilité de confusion est vraiment plus grande ici que dans la surcharge par type d'argument. Vous pouvez également consulter les justifications des différentes langues :

Ada : " Il pourrait sembler que la règle de résolution des surcharges la plus simple consiste à tout utiliser - toutes les informations provenant d'un contexte aussi large que possible - pour résoudre la référence surchargée. Cette règle est peut-être simple, mais elle n'est pas utile. Elle oblige le lecteur humain à parcourir des morceaux de texte arbitrairement grands et à faire des déductions arbitrairement complexes (comme (g) ci-dessus). Nous pensons qu'une meilleure règle est celle qui rend explicite la tâche qu'un lecteur humain ou un compilateur doit effectuer, et qui rend cette tâche aussi naturelle que possible pour le lecteur humain."

C++ (sous-section 7.4.1 de "The C++ Programming Language" de Bjarne Stroustrup) : "Les types de retour ne sont pas pris en compte dans la résolution des surcharges. La raison est de garder la résolution pour un opérateur individuel ou un appel de fonction indépendant du contexte. Pensez-y :

float sqrt(float);
double sqrt(double);

void f(double da, float fla)
{
    float fl = sqrt(da);     // call sqrt(double)
    double d = sqrt(da); // call sqrt(double)
    fl = sqrt(fla);            // call sqrt(float)
    d = sqrt(fla);             // call sqrt(float)
}

Si le type de retour était pris en compte, il ne serait plus possible de regarder un appel de sqrt() en isolation et déterminer quelle fonction a été appelée." (Notez, à titre de comparaison, qu'en Haskell, il n'y a pas de fonction implicite conversions).

Java ( Spécification du langage Java 9.4.1 ) : "L'une des méthodes héritées doit être return-type-substituable pour chaque autre méthode héritée, sinon une erreur de compilation se produit." (Oui, je sais que cela ne donne pas de justification. Je suis sûr que le raisonnement est donné par Gosling dans "the Java Programming Language". Peut-être que quelqu'un en a une copie ? Je parie que c'est le "principe de moindre surprise" en substance). Cependant, un fait amusant à propos de Java : la JVM permet à surcharge par valeur de retour ! Ceci est utilisé, par exemple, dans Scala et peut être consulté directement par le biais de Java aussi bien en jouant avec les internes.

PS. Pour finir, il est en fait possible de surcharger par valeur de retour en C++ avec une astuce. Témoin :

struct func {
    operator string() { return "1";}
    operator int() { return 2; }
};

int main( ) {
    int x    = func(); // calls int version
    string y = func(); // calls string version
    double d = func(); // calls int version
    cout << func() << endl; // calls int version
    func(); // calls neither
}

0 votes

Excellent article, mais vous devriez peut-être préciser ce qu'est la lecture (String -> quelque chose).

0 votes

Le C++ vous permet également de surcharger par valeur retournée const/not const. stackoverflow.com/questions/251159/

3 votes

Pour votre dernière astuce avec la surcharge des opérateurs de coercition, la ligne "cout" fonctionne parfois, mais presque toutes les modifications que je fais au code donnent "surcharge ambiguë pour 'operator<<'".

37voto

Frederick The Fool Points 9092

Si les fonctions étaient surchargées par le type de retour et que vous aviez ces deux surcharges

int func();
string func();

il n'y a aucun moyen pour le compilateur de savoir laquelle de ces deux fonctions appeler en voyant un appel comme celui-ci

void main() 
{
    func();
}

Pour cette raison, les concepteurs de langage interdisent souvent la surcharge des valeurs de retour.

Certains langages (tels que MSIL), cependant, faire permettent la surcharge par type de retour. Ils sont bien sûr confrontés à la même difficulté, mais ils disposent de solutions de contournement, pour lesquelles vous devrez consulter leur documentation.

4 votes

Une petite remarque (votre réponse donne un raisonnement très clair et compréhensible) : ce n'est pas qu'il n'y a pas de moyen ; c'est juste que les moyens seraient maladroits et plus pénibles que la plupart des gens ne le souhaiteraient. Par exemple, en C++, la surcharge aurait probablement été résolue à l'aide d'une horrible syntaxe de cast.

0 votes

Mais Michael, comment un appel comme celui ci-dessus (func() ;) pourrait-il être résolu sans que l'utilisateur lui-même n'aide le compilateur avec des indications supplémentaires ? Je ne pense pas que ce soit possible du tout.

0 votes

La valeur de retour n'est utilisée nulle part, donc il importe peu de savoir laquelle des deux sera appelée. En fait, toute votre fonction main() est un no-op.

28voto

Greg Hewgill Points 356191

Dans une telle langue, comment résoudriez-vous les problèmes suivants :

f(g(x))

si f avait des surcharges void f(int) y void f(string) y g avait des surcharges int g(int) y string g(int) ? Vous auriez besoin d'une sorte de désambiguïsateur.

Je pense que les situations où vous pourriez en avoir besoin seraient mieux servies en choisissant un nouveau nom pour la fonction.

2 votes

Le type de surcharge habituel peut également entraîner des ambiguïtés. Je pense que celles-ci sont normalement résolues en comptant le nombre de casts nécessaires, mais cela ne fonctionne pas toujours.

1 votes

Oui, les conversions standard sont classées en correspondance exacte, promotion et conversion : void f(int) ; void f(long) ; f('a') ; appelle f(int), parce que c'est seulement une promotion, alors que la conversion en long est une conversion. void f(float) ; void f(short) ; f(10) ; nécessiterait une conversion pour les deux : l'appel est ambigu.

0 votes

Si la langue dispose d'une évaluation paresseuse, ce n'est pas un problème aussi important.

21voto

Michael Burr Points 181287

Pour voler un C++ spécifique réponse à une autre question très similaire (doublon ?) :


Les types de retour des fonctions n'entrent pas en jeu dans la résolution des surcharges simplement parce que Stroustrup (je suppose avec l'aide d'autres architectes C++) voulait que la résolution des surcharges soit "indépendante du contexte". Voir 7.4.1 - "Overloading and Return Type" du "C++ Programming Language, Third Edition".

La raison est de garder la résolution pour un opérateur individuel ou un appel de fonction indépendant du contexte.

Ils voulaient qu'elle soit basée uniquement sur la façon dont la surcharge était appelée - et non sur la façon dont le résultat était utilisé (s'il était utilisé). En effet, de nombreuses fonctions sont appelées sans utiliser le résultat ou le résultat est utilisé dans le cadre d'une expression plus large. Un facteur qui, j'en suis sûr, est entré en ligne de compte lorsqu'ils ont pris cette décision est que si le type de retour faisait partie de la résolution, il y aurait de nombreux appels à des fonctions surchargées qui devraient être résolus avec des règles complexes ou pour lesquels le compilateur devrait lancer une erreur indiquant que l'appel était ambigu.

Et, Dieu sait que la résolution des surcharges en C++ est suffisamment complexe comme ça...

5voto

Cheery Points 6696

En haskell, c'est possible, même s'il n'y a pas de surcharge de fonctions. Haskell utilise des classes de type. Dans un programme, on pourrait voir :

class Example a where
    example :: Integer -> a

instance Example Integer where  -- example is now implemented for Integer
    example :: Integer -> Integer
    example i = i * 10

La surcharge de fonctions elle-même n'est pas si populaire. La plupart des langages que j'ai vus avec elle sont C++, peut-être Java et/ou C#. Dans tous les langages dynamiques, c'est un raccourci pour :

define example:i
  i type route:
    Integer = [i & 0xff]
    String = [i upper]

def example(i):
    if isinstance(i, int):
        return i & 0xff
    elif isinstance(i, str):
        return i.upper()

Par conséquent, cela n'a pas beaucoup d'intérêt. La plupart des gens ne s'intéressent pas à la question de savoir si un langage peut vous aider à déposer une seule ligne par où que vous l'utilisiez.

La correspondance de motifs est quelque peu similaire à la surcharge de fonctions, et je suppose qu'elle fonctionne parfois de manière similaire. Il n'est cependant pas courant car il n'est utile que pour quelques programmes et est difficile à mettre en œuvre dans la plupart des langages.

Vous voyez, il y a une infinité d'autres fonctionnalités plus faciles à implémenter dans le langage, notamment :

  • La typographie dynamique
  • Support interne pour les listes, les dictionnaires et les chaînes unicode
  • Optimisations (JIT, inférence de type, compilation)
  • Outils de déploiement intégrés
  • Soutien aux bibliothèques
  • Soutien communautaire et lieux de rassemblement
  • Riches bibliothèques standard
  • Bonne syntaxe
  • Read eval print loop
  • Soutien à la programmation réfléchie

3 votes

Haskell a la surcharge. Les classes de types sont la caractéristique du langage qui est utilisée pour définir les fonctions surchargées.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X