Pourquoi la surcharge avec les types de retour n'est-elle pas autorisée? (au moins dans les langues habituellement utilisées)

9

Je ne connais pas tous les langages de programmation, mais il est clair que généralement la possibilité de surcharger une méthode en tenant compte de son type de retour (en supposant que ses arguments sont du même nombre et du même type) n'est pas prise en charge.

Je veux dire quelque chose comme ça:

 int method1 (int num)
 {

 }
 long method1 (int num)
 {

 }

Ce n'est pas que ce soit un gros problème de programmation, mais à certaines occasions, je l'aurais bien accueilli.

Évidemment, il n'y aurait aucun moyen pour ces langages de supporter cela sans moyen de différencier la méthode qui est appelée, mais la syntaxe pour cela peut être aussi simple que quelque chose comme [int] method1 (num) ou [long] method1 (num) de cette façon, le compilateur saurait qui serait celui qui serait appelé.

Je ne sais pas comment fonctionnent les compilateurs, mais cela ne semble pas si difficile à faire, alors je me demande pourquoi quelque chose comme ça n'est généralement pas implémenté.

Quelles sont les raisons pour lesquelles une telle chose n'est pas prise en charge?

user2638180
la source
Peut-être que votre question serait meilleure avec un exemple où les conversions implicites entre les deux types de retour n'existent pas - par exemple les classes Fooet Bar.
Eh bien, pourquoi une telle fonctionnalité serait-elle utile?
James Youngman
3
@JamesYoungman: Par exemple, pour analyser des chaînes en différents types, vous pouvez avoir une méthode int read (String s), float read (String s), etc. Chaque variation surchargée de la méthode effectue une analyse pour le type approprié.
Giorgio
Soit dit en passant, ce n'est qu'un problème avec les langues typées statiquement. Avoir plusieurs types de retour est assez routinier dans des langages typés dynamiquement comme Javascript ou Python.
Gort le robot
1
@StevenBurnap, euh, non. Avec par exemple JavaScript, vous ne pouvez pas du tout surcharger les fonctions. Ce n'est donc en fait qu'un problème avec les langages qui prennent en charge la surcharge de noms de fonction.
David Arno

Réponses:

19

Cela complique la vérification de type.

Lorsque vous autorisez uniquement la surcharge en fonction des types d'arguments et ne permettez que la déduction des types de variables à partir de leurs initialiseurs, toutes vos informations de type circulent dans une seule direction: vers le haut de l'arborescence de syntaxe.

var x = f();

given      f   : () -> int  [upward]
given      ()  : ()         [upward]
therefore  f() : int        [upward]
therefore  x   : int        [upward]

Lorsque vous autorisez les informations de type à voyager dans les deux sens, par exemple en déduisant le type d'une variable de son utilisation, vous avez besoin d'un solveur de contraintes (tel que l'algorithme W, pour les systèmes de type Hindley-Milner) afin de déterminer le type.

var x = parse("123");
print_int(x);

given      parse        : string -> T  [upward]
given      "123"        : string       [upward]
therefore  parse("123") : ∃T           [upward]
therefore  x            : ∃T           [upward]
given      print_int    : int -> ()    [upward]
therefore  print_int(x) : ()           [upward]
therefore  int -> ()    = ∃T -> ()     [downward]
therefore  ∃T           = int          [downward]
therefore  x            : int          [downward]

Ici, nous devions laisser le type de xcomme une variable de type non résolue ∃T, où tout ce que nous savons à ce sujet est qu'il est analysable. Ce n'est que plus tard, lorsqu'il xest utilisé sur un type concret, que nous avons suffisamment d'informations pour résoudre la contrainte et déterminer cela ∃T = int, qui propage les informations de type dans l'arborescence de syntaxe de l'expression d'appel vers x.

Si nous ne parvenions pas à déterminer le type de x, soit ce code serait également surchargé (de sorte que l'appelant déterminerait le type), soit nous devions signaler une erreur concernant l'ambiguïté.

À partir de là, un concepteur de langage peut conclure:

  • Cela ajoute de la complexité à la mise en œuvre.

  • Cela rend la vérification typographique plus lente - dans les cas pathologiques, de façon exponentielle.

  • Il est plus difficile de produire de bons messages d'erreur.

  • C'est trop différent du statu quo.

  • Je n'ai pas envie de le mettre en œuvre.

Jon Purdy
la source
1
Aussi: Il sera si difficile de comprendre que dans certains cas, votre compilateur fera des choix auxquels le programmeur ne s'attendait absolument pas, ce qui entraînera des difficultés à trouver des bogues.
gnasher729
1
@ gnasher729: Je ne suis pas d'accord. J'utilise régulièrement Haskell, qui a cette fonctionnalité, et je n'ai jamais été mordu par son choix de surcharge (à savoir l'instance de typeclass). Si quelque chose est ambigu, cela m'oblige simplement à ajouter une annotation de type. Et j'ai quand même choisi d'implémenter l'inférence de type complet dans ma langue car c'est incroyablement utile. Cette réponse était que je jouais l'avocat du diable.
Jon Purdy
4

Parce que c'est ambigu. En utilisant C # comme exemple:

var foo = method(42);

Quelle surcharge devrions-nous utiliser?

Ok, c'était peut-être un peu injuste. Le compilateur ne peut pas déterminer quel type utiliser si nous ne le disons pas dans votre langage hypothétique. Ainsi, la frappe implicite est impossible dans votre langue et il y a des méthodes anonymes et Linq avec ...

Celui-ci, ça va? (Signatures légèrement redéfinies pour illustrer ce point.)

short method(int num) { ... }
int method(int num) { ... }

....

long foo = method(42);

Faut-il utiliser la intsurcharge ou la shortsurcharge? Nous ne savons tout simplement pas, nous devrons le spécifier avec votre [int] method1(num)syntaxe. Ce qui est un peu pénible à analyser et à écrire pour être honnête.

long foo = [int] method(42);

Le fait est que sa syntaxe est étonnamment similaire à une méthode générique en C #.

long foo = method<int>(42);

(C ++ et Java ont des fonctionnalités similaires.)

En bref, les concepteurs de langage ont choisi de résoudre le problème d'une manière différente afin de simplifier l'analyse et d'activer des fonctionnalités de langage beaucoup plus puissantes.

Vous dites que vous ne savez pas grand chose sur les compilateurs. Je recommande fortement d'apprendre les grammaires et les analyseurs. Une fois que vous comprendrez ce qu'est une grammaire sans contexte, vous aurez une bien meilleure idée de la raison pour laquelle l'ambiguïté est une mauvaise chose.

Canard en caoutchouc
la source
Bon point sur les génériques, même si vous semblez confondre shortet int.
Robert Harvey
Ouais @RobertHarvey tu as raison. Je me battais pour un exemple pour illustrer ce point. Cela fonctionnerait mieux s'il methodrenvoyait un short ou un int, et le type était défini comme long.
RubberDuck
Cela semble un peu mieux.
RubberDuck
Je n'achète pas vos arguments selon lesquels vous ne pouvez pas avoir d'inférence de type, de sous-programmes anonymes ou de compréhensions monades dans une langue avec surcharge de type de retour. Haskell le fait et Haskell a les trois. Il présente également un polymorphisme paramétrique. Votre point sur long/ int/ shortconcerne davantage la complexité du sous-typage et / ou des conversions implicites que la surcharge de type de retour. Après tout, les littéraux numériques sont surchargés sur leur type de retour en C ++, Java, C♯ et bien d'autres, et cela ne semble pas poser de problème. Vous pouvez simplement créer une règle: par exemple, choisissez le type le plus spécifique / général.
Jörg W Mittag
@ JörgWMittag mon point n'était pas que cela rendrait cela impossible, juste que cela rend les choses inutilement complexes.
RubberDuck
0

Toutes les fonctionnalités linguistiques ajoutent de la complexité, elles doivent donc fournir suffisamment d'avantages pour justifier les pièges inévitables, les cas d'angle et la confusion des utilisateurs que chaque fonctionnalité crée. Pour la plupart des langues, celle-ci ne fournit tout simplement pas suffisamment d'avantages pour la justifier.

Dans la plupart des langues, vous vous attendriez à ce que l'expression method1(2)ait un type défini et une valeur de retour plus ou moins prévisible. Mais si vous autorisez une surcharge sur les valeurs de retour, cela signifie qu'il est impossible de dire ce que cette expression signifie en général sans tenir compte du contexte qui l'entoure. Considérez ce qui se passe lorsque vous avez une unsigned long long foo()méthode dont la mise en œuvre se termine par return method1(2)? Cela devrait-il appeler la longsurcharge -returning ou la intsurcharge -returning ou simplement donner une erreur de compilation?

De plus, si vous devez aider le compilateur en annotant le type de retour, non seulement vous inventez plus de syntaxe (ce qui augmente tous les coûts susmentionnés pour permettre à la fonctionnalité d'exister), mais vous faites en fait la même chose que la création deux méthodes nommées différemment dans un langage "normal". Est [long] method1(2)plus intuitif que long_method1(2)?


D'un autre côté, certains langages fonctionnels comme Haskell avec des systèmes de types statiques très puissants autorisent ce type de comportement, car leur inférence de type est suffisamment puissante pour que vous ayez rarement besoin d'annoter le type de retour dans ces langages. Mais cela n'est possible que parce que ces langages renforcent vraiment la sécurité des types, plus que tout langage traditionnel, tout en exigeant que toutes les fonctions soient pures et référentiellement transparentes. Ce n'est jamais quelque chose qui serait réalisable dans la plupart des langues POO.

Ixrec
la source
2
"en plus d'exiger que toutes les fonctions soient pures et référentiellement transparentes": Comment cela facilite-t-il la surcharge de type retour?
Giorgio
@Giorgio Ce n'est pas le cas - Rust n'impose pas la pureté des fonctions et il peut toujours effectuer une surcharge de type retour (bien que sa surcharge dans Rust soit très différente des autres langages (vous ne pouvez surcharger qu'en utilisant des modèles))
Idan Arye
La partie [long] et [int] devait avoir un moyen d'appeler explicitement la méthode, dans la plupart des cas, la façon dont elle devrait être appelée pourrait être directement déduite du type de variable à laquelle l'exécution de la méthode est affectée.
user2638180
0

Il est disponible dans Swift et fonctionne très bien là-bas. De toute évidence, vous ne pouvez pas avoir un type ambigu des deux côtés, il doit donc être connu à gauche.

Je l'ai utilisé dans une simple API d'encodage / décodage .

public protocol HierDecoder {
  func dech() throws -> String
  func dech() throws -> Int
  func dech() throws -> Bool

Cela signifie que les appels où les types de paramètres sont connus, comme ceux initd'un objet, fonctionnent très simplement:

    private static let typeCode = "ds"
    static func registerFactory() {
        HierCodableFactories.Register(key:typeCode) {
            (from) -> HierCodable in
            return try tgDrawStyle(strokeColor:from.dech(), fillColor:from.dechOpt(), lineWidth:from.dech(), glowWidth: from.dech())
        }
    }
    func typeKey() -> String { return tgDrawStyle.typeCode }
    func encode(to:HierEncoder) {
        to.ench(strokeColor)
        to.enchOpt(fillColor)
        to.ench(lineWidth)
        to.ench(glowWidth)
    }

Si vous portez une attention particulière, vous remarquerez l' dechOptappel ci-dessus. J'ai découvert à la dure que la surcharge du même nom de fonction où le différenciateur renvoyait une option était trop sujette à erreur car le contexte d'appel pouvait introduire une attente, c'était une option.

Andy Dent
la source
-5
int main() {
    auto var1 = method1(1);
}
DeadMG
la source
Dans ce cas, le compilateur pourrait a) rejeter l'appel parce qu'il est ambigu b) choisir le premier / dernier c) laisser le type de var1et procéder à l'inférence de type et dès qu'une autre expression détermine le type d' var1utilisation que pour sélectionner le bonne mise en œuvre. En fin de compte, montrer un cas où l'inférence de type n'est pas triviale prouve rarement un point autre que cette inférence de type est généralement non triviale.
back2dos
1
Pas un argument solide. La rouille, par exemple, fait un usage intensif de l'inférence de type et, dans certains cas, en particulier avec les génériques, ne peut pas dire de quel type vous avez besoin. Dans de tels cas, vous devez simplement donner explicitement le type au lieu de vous fier à l'inférence de type.
8bittree
1
Euh ... cela ne montre pas un type de retour surchargé. method1doit être déclaré pour renvoyer un type particulier.
Gort le robot