9

I don't know about all programming languages, but it's clear that usually the possibility of overloading a method taking into consideration its return type (assuming its arguments are the same number and type) is not supported.

I mean something like this:

 int method1 (int num)
 {

 }
 long method1 (int num)
 {

 }

It's not that it's a big issue for programming but on some occasions I would have welcome it.

Obviously there would be no way for those languages to support that without a way to differentiate what method is being called, but the syntax for that can be as simple as something like [int] method1(num) or [long] method1(num) that way the compiler would know which would be the one that would be called.

I don't know about how compilers work but that doesn't look to be that difficult to do, so I wonder why something like that isn't usually implemented.

Which are the reasons why something like that is not supported?

  • Maybe your question would be better with with an example where implicit conversions between the two return types do not exist - e.g. classes Foo and Bar. – Tibo Apr 28 '16 at 21:18
  • Well, why would such a feature be useful? – James Youngman Apr 28 '16 at 21:18
  • 3
    @JamesYoungman: For example to parse strings into different types, you can have a method int read(String s), float read(String s), and so on. Each overloaded variation of the method performs a parse for the appropriate type. – Giorgio Apr 28 '16 at 21:24
  • This is only an issue with statically typed languages, by the way. Having multiple return types is pretty routine in dynamically typed languages like Javascript or Python. – Steven Burnap Apr 29 '16 at 1:44
  • 1
    @StevenBurnap, um, no. With eg JavaScript, you can't do function overloading at all. So this is actually only an issue with languages that support function name overloading. – David Arno Apr 29 '16 at 8:54
17

It complicates type checking.

When you only permit overloading based on argument types, and only permit deducing variable types from their initialisers, all of your type information flows in one direction: up the syntax tree.

var x = f();

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

When you permit type information to travel in both directions, such as deducing a variable’s type from its usage, you need a constraint solver (such as Algorithm W, for Hindley–Milner type systems) in order to determine the 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]

Here, we needed to leave the type of x as an unresolved type variable ∃T, where all we know about it is that it’s parseable. Only later, when x is used at a concrete type, do we have enough information to solve the constraint and determine that ∃T = int, which propagates type information down the syntax tree from the call expression into x.

If we failed to determine the type of x, either this code would be overloaded as well (so the caller would determine the type) or we would have to report an error about the ambiguity.

From this, a language designer can conclude:

  • It adds complexity to the implementation.

  • It makes typechecking slower—in pathological cases, exponentially so.

  • It’s harder to produce good error messages.

  • It’s too different from the status quo.

  • I don’t feel like implementing it.

  • 1
    Also: It will be so hard to understand that in some cases your compiler will make choices that the programmer absolutely didn't expect, leading to hard to find bugs. – gnasher729 Apr 29 '16 at 16:30
  • 1
    @gnasher729: I disagree. I regularly use Haskell, which has this feature, and I’ve never been bitten by its choice of overload (viz. typeclass instance). If something is ambiguous, it just forces me to add a type annotation. And I still chose to implement full type inference in my language because it’s incredibly useful. This answer was me playing devil’s advocate. – Jon Purdy Apr 29 '16 at 22:31
4

Because it's ambiguous. Using C# as an example:

var foo = method(42);

Which overload should we use?

Ok, perhaps that was a bit unfair. The compiler can't figure out what type to use if we don't tell it in your hypothetical language. So, implicit typing is impossible in your language and there goes anonymous methods and Linq along with it...

How about this one? (Slightly redefined signatures to illustrate the point.)

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

....

long foo = method(42);

Should we use the int overload, or the short overload? We simply don't know, we'll have to specify it with your [int] method1(num) syntax. Which is a bit of a pain to parse and write to be honest.

long foo = [int] method(42);

The thing is, it's surprisingly similar syntax to a generic method in C#.

long foo = method<int>(42);

(C++ and Java have similar features.)

In short, language designers chose to solve the problem in a different way in order to simplify the parsing and enable much more powerful language features.

You say you don't know much about compilers. I'd highly recommend learning about grammars and parsers. Once you understand what a context free grammar is, you'll have a much better idea about why ambiguity is a bad thing.

  • Good point about generics, though you appear to be conflating short and int. – Robert Harvey Apr 28 '16 at 23:33
  • Yeah @RobertHarvey you're right. I was struggling for an example to illustrate the point. It would work better if method returned a short or int, and the type was defined as a long. – RubberDuck Apr 28 '16 at 23:51
  • That seems a bit better. – RubberDuck Apr 28 '16 at 23:56
  • I don't buy your arguments that you can't have type inference, anonymous subroutines, or monad comprehensions in a language with return type overloading. Haskell does it, and Haskell has all three. It also has parametric polymorphism. Your point about long/int/short is more about the complexities of subtyping and/or implicit conversions than about return type overloading. After all, number literals are overloaded on their return type in C++, Java, C♯, and many others, and that doesn't seem to present a problem. You could simply make up a rule: e.g. pick the most specific/general type. – Jörg W Mittag Apr 29 '16 at 8:17
  • @JörgWMittag my point wasn't that it would make it impossible, just that it makes things unnecessarily complex. – RubberDuck Apr 29 '16 at 8:54
0

All language features add complexity, so they have to provide enough benefit to justify the inevitable gotchas, corner cases, and user confusion that every feature creates. For most languages, this one simply doesn't provide enough benefit to justify it.

In most languages, you would expect the expression method1(2) to have a definite type and a more or less predictable return value. But if you allow overloading on return values, that means it's impossible to tell what that expression means in general without considering the context around it. Consider what happens when you have a unsigned long long foo() method whose implementation ends with return method1(2)? Should that call the long-returning overload or the int-returning overload or simply give a compiler error?

Plus, if you have to help the compiler by annotating the return type, not only are you inventing more syntax (which raises all the aforementioned costs of allowing the feature to exist at all), but you're effectively doing the same thing as creating two differently-named methods in a "normal" language. Is [long] method1(2) more intuitive than long_method1(2)?


On the other hand, some functional languages like Haskell with very strong static type systems do allow this sort of behavior, because their type inference is powerful enough that you rarely need to annotate the return type in those languages. But that's only possible because those languages truly enforce type safety, more so than any traditional language, along with requiring all functions to be pure and referentially transparent. It's not something that would ever be feasible in most OOP languages.

  • 2
    "along with requiring all functions to be pure and referentially transparent": How does this make return type overloading easier? – Giorgio Apr 28 '16 at 21:26
  • @Giorgio It doesn't - Rust doesn't enforce function puritiy and it can still do return type overloading(though her overloading in Rust is very different than other languages(you can only overload by using templates)) – Idan Arye Apr 28 '16 at 22:30
  • The [long] and [int] part was to have a way to explicitely call the method, on most cases how it should be called could be directly infered from the type of the variable that the execution of the method gets assigned to. – user2638180 Apr 28 '16 at 23:11
0

It is available in Swift and works fine there. Obviously you cannot have an ambiguous type on both sides so it has to be known on the left.

I used this in a simple encode/decode API.

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

That means that calls where parameter types are known, such as an init of an object, work very simply:

    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)
    }

If you're paying close attention, you will notice the dechOpt call above. I found out the hard way that overloading the same function name where the differentiator was returning an optional was too prone to error as the calling context could introduce an expectation it was an optional.

-5
int main() {
    auto var1 = method1(1);
}
  • In that case the compiler could a) reject the call because it is ambiguous b) pick the first/last one c) leave the type of var1 and proceed with type inference and as soon as some other expression determines the type of var1 use that for selecting the right implementation. At the bottom line, showing a case where type inference is non-trivial rarely proves a point other than that type inference is generally non-trivial. – back2dos Apr 28 '16 at 22:17
  • 1
    Not a strong argument. Rust, for example, makes heavy use of type inference, and in some cases, especially with generics, can't tell what type you need. In such cases, you simply have to explicitly give the type instead of relying on type inference. – 8bittree Apr 28 '16 at 22:17
  • 1
    Uh...that doesn't demonstrate an overloaded return type. method1 must be declared to return a particular type. – Steven Burnap Apr 29 '16 at 1:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.