Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suggestion: A better and more 'classic' function overloads for TypeScript #12041

Closed
Shlomibo opened this issue Nov 4, 2016 · 63 comments
Closed
Labels
Duplicate An existing issue was already created

Comments

@Shlomibo
Copy link

Shlomibo commented Nov 4, 2016

TypeScript Version: 2.0.3 / nightly (2.1.0-dev.201xxxxx)

Today's function overloading in TypeScript is a compromise which is the worst of both worlds of dynamic, non-overloaded language like JS, and static, overloaded language such as TS.

Static languages weakness:
In a static language such as TypeScript or C#, most types must be provided with the code. This put some burden on the programmer.

class Example
{
    //            ******          ***
    public static string Overload(int num)
    {
        return num.ToString();
    }
    //            ******          ******      ***
    public static string Overload(string str, int num)
    {
        return str + num.ToString();
    }
}

JS weakness:
In JS, there are no function overloads as they cannot be inferred from each other. All parameters are optional and untyped.
Because of that the programmer has to provider the logic for inferring which functionality is desired, and execute it. If the code should be divided into more specific functions, they must be named differently.

function overload(numOrStr, optionalNum) {
	return typeof numOrStr === 'number'
		? implementation(numOrStr)
		: otherImplementation(numOrStr, num);
}
function implementation(num) {
	return num.toString();
}
function otherImplementation(str, num) {
	return str + num;
}

Nothing in the code would suggest to a future programmer that the last two functions are related.

TypeScript has both problems, and more...
Because TS is bound to JS, only one overloaded function can have a body, and that function must has parameters that are compatible with all other overloads.
That function must resolve the desired behavior by the the parameters` types and count.

                       ******   ******
function overload(num: number): string;
                       ******       ******   ******
function overload(str: string, num: number): string;
                            ***************        ******   ******
function overload(numOrStr: number | string, num?: number): string {
    return typeof numOrStr === 'number'
    ? implementation(numOrStr)
                                    *   **********                   *
    : otherImplementation(numOrStr, (num as number /* are we sure? */));
}
                             ******
function implementation(num: number) {
    // We must validate our input type.
    if (typeof num !== 'number') {
        throw new Error();
    }
    return num.toString();
}
                                  ******       ******
function otherImplementation(str: string, num: number) {
    // !! Must validate the types of input parms!
    return str + num;
}

Look how much longer it is than both previous examples, but with providing little benefits to code readability and maintenance, if any.
We could shorten it a bit by disabling type checking in the real overload, but then we loose type checking for the compatibility of the overloads.

Either way, there's no checking that the programmer actually handles all the declared overloads.

                       ******   ******
function overload(num: number): string;
                       ******       ******   ******
function overload(str: string, num: number): string;
function overload(numOrStr, num) {
    return typeof numOrStr === 'number'
    ? implementation(numOrStr)
    : otherImplementation(numOrStr, num);
}
                             ******
function implementation(num: number) {
    // We must validate our input type.
    if (typeof num !== 'number') {
        throw new Error();
    }
    return num.toString();
}
                                  ******       ******
function otherImplementation(str: string, num: number) {
    // !! Must validate the types of input parms!
    return str + num;
}

My suggestion: at least lets have validation that overloads are handled, and enjoy the better semantics of the static languages.

Syntax:
Like current TS overloads, all overloads must be specified one after another. No other code can separate them.
Unlike current TS overloads, all overloads must have a body.

// OK, current syntax
function overload(num: number): string;
function overload(str: string, num: number): string;
function overload(numOrStr, num) {
    // code
}
-------------------------------------------------------------
// OK, new syntax
function overload(num: number): string {
    // code
}
function overload(str: string, num: number): string {
    // code
}
function overload(numOrStr, num) {
    // code
}
------------------------------------------------------------
// ERROR. No mix and match
function overload(num: number): string; // Error: missing body
function overload(str: string, num: number): string {
    // code
}
function overload(numOrStr, num) {
    // code
}

function overload(num: number): string {
    // code
}
function overload(str: string, num: number): string; // Error: missing body
function overload(numOrStr, num) {
    // code
}
---------------------------------------------------------------
// ERROR: No code between overloads
function overload(num: number): string {
    // code
}
const iMNotSupposeToBeHere = 'xxx';
function overload(str: string, num: number): string { // Error: duplicate function implementation
    // code
}
function overload(numOrStr, num) {
    // code
}

For readability purpose, I would suggest the first overload would be the entry-overload. That is the function that is exposed to JS code, but is hidden from TS code.
That function would infer the desired behavior, and call the appropriate overload

function overload(numOrStr: number | string, num?: number): string {
    return typeof numOrStr === 'number'
        ? overload(numOrStr)
        : overload(numOrStr, (num as number));
}
function overload(num: number): string {
    // We must validate our input type.
    if (typeof num !== 'number') {
        throw new Error();
    }
    return num.toString();
}
function overload(str: string, num: number): string{
    // !! Must validate the types of input parms!
    return str + num;
}

An overload must be called from reachable code in the entry-overload:

function overload(numOrStr, num) {
    return overload(numOrStr, num);
}
function overload(num: number): string { // ERROR: overload is not referenced from entry function.
    // We must validate our input type.
    if (typeof num !== 'number') {
        throw new Error();
    }
    return num.toString();
}
function overload(str: string, num: number): string{
    // !! Must validate the types of input parms!
    return str + num;
}

In this syntax, the programmer has to provide an implementation that handles a declared overload, and each overload handle its parameters, validates them, and define the desired behavior.
The code implies that all these functions are related, and enforce that they would not be separated.

All other semantics and syntax regarding overload resolution, type checking, generics, etc' should remain the same as it is.

Emitted code:
The overloaded implementations should be moved into the scope of the entry-overload. A bit of name mangling is required because JS cannot support overloads - but any name-mangling may be used
From the previous example:

function overload(numOrStr: number | string, num?: number): string {
    return typeof numOrStr === 'number'
        ? overload(numOrStr)
        : overload(numOrStr, (num as number));
}
function overload(num: number): string {
    // We must validate our input type.
    if (typeof num !== 'number') {
        throw new Error();
    }
    return num.toString();
}
function overload(str: string, num: number): string{
    // !! Must validate the types of input parms!
    return str + num;
}

We can very simply generate this code:

function overload(numOrStr, num) {
    return typeof numOrStr === 'number'
        ? overload$number(numOrStr)
        : overload$string$number(numOrStr, num);
// }  <- Oh, no
    // Well, it is unfortunate
    function overload$number(num) {
        // We must validate our input type.
        if (typeof num !== 'number') {
            throw new Error();
        }
        return num.toString();
    }
    function overload$string$number(str, num){
        // !! Must validate the types of input parms!
        return str + num;
    }
} // Ow, here I am

Notice that the output stays coherent.
Because the parameters are already in the scope of the implementations overloads, and if the respective parameter names are the same, or if renaming them would not introduce a naming conflict; we can omit the parameters and arguments from the implementation overloads.

function overload(numOrStr, num) {
    return typeof numOrStr === 'number'
        ? overload$number()
        : overload$string$number();

    // Well, it is unfortunate
    function overload$number() {
        // We must validate our input type.
        if (typeof num !== 'number') {
            throw new Error();
        }
        return num.toString();
    }
    function overload$string$number(){
        // !! Must validate the types of input parms!
        return str + num;
    }
}

We must also check if the entry-overload hides an otherwise captured variable by closure of the implementations. in this case, the hiding variable should be renamed.

const str = 'abc';

function overload(str, num) {

    return typeof str === 'number'
        ? overload(num)
        : overload(str, num);
}
function overload(num: number): string {
    // We must validate our input type.
    if (typeof num !== 'number') {
        throw new Error();
    }
    return str + num;
}
function overload(str: string, num: number): string{
    // !! Must validate the types of input parms!
}

Should transpiled to something like

const str = 'abc';

function overload(_str, num) {

    return typeof str === 'number'
        ? overload$number(num)
        : overload$string$number(_str, num);

    function overload$number(num) {
        // We must validate our input type.
        if (typeof num !== 'number') {
            throw new Error();
        }
        return str + num;
    }
    function overload$string$number(str, num) {
        // !! Must validate the types of input parms!
    }
}

But I don't think this would be a common case.

I really really hope you would consider my suggestion.

@HerringtonDarkholme
Copy link
Contributor

HerringtonDarkholme commented Nov 4, 2016

Possible duplicate of #3442.
IMHO, compile time generated function is possible contrary to TS's design philosophy

@Shlomibo
Copy link
Author

Shlomibo commented Nov 4, 2016

@HerringtonDarkholme
I'm not sure of that, but it might be true.
Yet, IMHO, this syntax has much better semantics than the current syntax, and it's in par with TS design goals and philosophy (I think).

Also, the two are not mutual exclusive.
You can think of it as basic for that compile time generated function. If they would implement this, in the future, some implementation of compile-time generated entry-overload could fit into it as simple as writing that function yourself.

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Nov 6, 2016

Like current TS overloads, all overloads must be specified one after another. No other code can separate them.

That is not strictly true. Overloads should be specified in order from greatest to least specificity of their argument types but they can come from multiple sources making order non-deterministic.

Looking at the example emitted code,

function overload(numOrStr, num) {
    return typeof numOrStr === 'number'
        ? overload$number(numOrStr)
        : overload$string$number(numOrStr, num);
...

How does the compiler know to emit that code?

I'm not sure I see how this proposal improves the situation since it makes implementing the callee more complicated, but does not improve the experience for the caller (I think the current experience is just fine).

Also, this is a massive breaking change, how should that be handled?

@Shlomibo
Copy link
Author

Shlomibo commented Nov 6, 2016

@aluanhaddad

That is no strictly true. Overloads should be specified in order from greatest to least specificity of their argument types but they can come from multiple sources making order non-deterministic

I haven't talked about ordering.
What can be supplied from multiple sources are declarations, which are irrelevant to this case, and should not be changed (they are specifying what is already exist in code. Not what that is written for the current program.

How does the compiler know to emit that code?

It is in the source code that the transpiler compiles. You should compare the transpiled code to the source.
Just like current implementation the programmer must resolve the desired overload (It's a JS limitation).

What he gets here but not in the current syntax is better separations of concerns. Each overload handles its own data and behavior, like in any other language that provides function overloading.

@froh
Copy link

froh commented Nov 25, 2016

name mangling proposal: md5-hash the normalized function signature

I understand currently overloaded functions compile into just one function, which then at run time has to determine the actual parameter types and dispatch.

I also understand a key issue for proper 1:1 compilation of overloaded functions to individual functions instead is name mangling (like it's done in C++ vs C, quite similar problem)

Has anybody ever considered a two step process?

  1. flatten the signature, normalize and serialize it to a string.

    flatten means expand types down to basic types. normalize means e.g. sort fields alphabetically where order doesn't make a difference. serialize could e.g. be as json.

  2. md5-hash this flattened normalized signature

use that md5 hash as a suffix to the function name. Accompany the compiled output with both the original, un-flattened signature, as well as the flattened one, as generated doc string comment, so other tools can remain useful with someFunction_7b1a4f7194c8063b353e45c96c4107ab vs someFunction_d32239bcb673463ab874e80d47fae504

That should keep a good balance between keeping the generated code human readable (function names won't expand too much) yet as close as possible to 1:1 ES6. It's a bit like compiling C++ to C. If you want signature based function selection at compile time there is no way around name mangling. imho.

@Shlomibo
Copy link
Author

Shlomibo commented Nov 26, 2016

@froh
I feel that I have no saying on how to do the name mangling, except that I agree it is required by JS (as it is required in C++).

Because from JS POV there's only one possible function, I don't think exposing the name-mangled overloads would be a good idea.
It is interesting though what calls between overloads should be look like? Should TS emit code to call directly to that overload, or let the flow go through the entry-overload as all other calls?

@froh
Copy link

froh commented Nov 26, 2016

@Shlomibo

Generated calls should directly call the resolved overload.

I agree the mangling could be any sufficiently unique compression, not just a hash, and md5 was just an example. But the key is: it has to be compact. very compact because (and that's where I kindly disagree) I strongly believe the mangled names then should go into the generated code: each overloaded function is emitted with it's mangled call signature. Each resolved call is emitted as a call to the right function. The compact mangling prevents emitted code from horribly exploding in length (as they would with C++ style mangling :-) ) a standardized mangling will make the emitted code accessible to tools.

I find this less confusing and error prone than having to manually encode run time type checking and dispatch. this manual dispatch thing suggests the classical "if type1 do this, if type2 do that" that I hated to maintain in (cough) cobol back in the days.

@Shlomibo
Copy link
Author

Shlomibo commented Nov 27, 2016

For generated calls to be statically resolved, you have (IMHO) to have dynamic type-checker that ensures that the tun-time types are actually those that were statically resolved.

The problem here is that you don't actually have dynamic type-checker, and at run-time an argument can have a value of entirely different type.
You can't even impose rules to mitigate that because you're probably dependent on some 3rd-party(ies) library that wouldn't conform to such rules.

But, I can't even see the benefit of such scheme.
IMHO exposing implementation details (such as overload implementation) or otherwise extending your public interface supposed to be an intentional act, and therefor explicit.

Now, a library writer whom wants to expose a specific overload may already do so with the current TS syntax, by giving that overload-implementation a name that mostly fits its semantics (rather then relying on some hard-to-predict name-mangling), and exporting that function.

Using my suggestion may only benefit him by ensuring that only the overloads that are actually handled are exposed through the overloaded function.

What are the benefits that are not already easy to achieve with the current syntax, that would be achieved by exposing overload-implementations?

@avchugaev
Copy link
Contributor

Ideas of class methods overloading implementation:

  • TS compiler does not emit additional code that checks arguments length, params types, whatever.
  • TS compiler should emit JS function per each overloaded method, with unique name.
  • TS compiler decides which exactly method should be used based on arguments types (TS).
  • TS compiler emits error when it cannot clearly define which one of generated methods to use (if types definition is too weak / 'any' specified as type / etc). Thus, developers should refactor their code and make types definitions more exact.

TypeScript code:

// Interface declaration

interface IGreeter {
    greet(name: string);
    greet(person: Person);
}

// Class Implementation

class DummyGreeter {
    public greet(name: string) {
        console.log('Hi, ' + name);
    }

    public greet(person: Person) {
        this.greet(person.name);
    }
}

// Usage

let dummyGreeter: DummyGreeter = new DummyGreeter();

dummyGreeter.greet(new Person('Alex'));

Transforms to:

function DummyGreeter {
    
}

DummyGreeter.prototype.greet__1 = function(name) {
    console.log('Hi, ' + name);
};

DummyGreeter.prototype.greet__2 = function(person) {
    this.greet__1(person.name);
};


var dummyGreeter = new DummyGreeter();

dummyGreeter.greet__2(new Person('Alex'));

And everybody's happy. One step to traditional OOP and C# ;).

@avchugaev
Copy link
Contributor

avchugaev commented Jan 9, 2017

I think overloading is required attribute of each traditional OO programming language. Thus many developers will ask for it's implementation in TS again and again and again. As well as final keyword for classes and other features that mature OO programming language have.

@aluanhaddad
Copy link
Contributor

@achugaev93
It is interesting that you bring up C# here because it does in fact handle overloading correctly; handling the distinction between generic overloads as well as the distinction between overloading, overriding, and shadowing; while many other languages, say Java, get this wrong.

I love C# but it is not a model for TypeScript. Even if it were, that wouldn't make TypeScript a "traditional OO language" because C# is fairly non-traditional.
Regardless what makes you think this is a "traditional OO language" as you call it?

flatten means expand types down to basic types. normalize means e.g. sort fields alphabetically where order doesn't make a difference. serialize could e.g. be as json.

@froh I'm not sure that a type can reasonably be expected to be reduced to primitives. Function types, union types, and generic types seem like they might be prohibitively difficult.

@avchugaev
Copy link
Contributor

@aluanhaddad Lets skip discussions about other programming languages and focus on the topic of this thread. Modern and mature model of OOP includes such feature as method overloading. That's a fact. And many TS programmers stocked a box of beer and ready to shout "Hurrah!!!" every time TypeScript got another feature inherent in classical OO programming languages.

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Jan 9, 2017

Lets skip discussions about other programming languages and focus on the topic of this thread.

You introduced the comparison in no uncertain terms

And everybody's happy. One step to traditional OOP and C# ;).

Modern and mature model of OOP includes such feature as method overloading.

Please explain how this is relevant.

And many TS programmers stocked a box of beer and ready to shout "Hurrah!!!" every time TypeScript got another feature inherent in classical OO programming languages.

Its interesting that the etymology of "classical" as used in this context actually arises from the need to distinguish JavaScript and by extension TypeScript from other languages that have fundamentally different semantics. Regardless the programmers you refer to need to learn JavaScript. If they already know JavaScript but think that TypeScript has a different object model then you should inform them that this is not the case.

@avchugaev
Copy link
Contributor

Does this mean that the development of language will stop halfway to the classical OOP? Why such everyday functions as overloading methods, final classes, etc. cannot be implemented in the compiler?

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Jan 10, 2017

It is not half way if that is an explicit non goal:
https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals#non-goals
Also goals prioritize alignment with JavaScript
https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals#goals

@froh
Copy link

froh commented Jan 10, 2017

If I'm not mistaken the value of function overloading is to simplify generic code in complex systems? No need for manual dynamic type dispatch on function arguments because the code for that is generated? So the source code that humans reason about and maintain is simpler, because it is generic.

I have maintained cobol code that required to add new business objects to dispatch tables (if cascades) manually. That was no fun.

So I find function overloading nicely matches goal 2 (structuring for larger pieces of code), and imnsho also non-goal 5 (run-time metadata? nope. encourage statically typed and dispatched code).

at some point you hesitate flattening of types is simple or even feasible (per my suggestion to use a standardized hash of a standardized flattening for mangling) --- well it's about as complex as the types that happen to occur in the program, and it's non-trivial but certainly simple enough to be feasible.

I'd even try to spell it out if I knew it's not for /dev/null.

@Shlomibo
Copy link
Author

Shlomibo commented Jan 12, 2017

@achugaev93
I think that exposing each overload and resolving the call statically - must be limited to intra-module calls.
Any exposed/public name-mangled function/method would impose huge limitations on javascript projects dependent of the code, which cannot infer which implementation serves which overload, and versioning (it makes it too easy to require any dependent code to recompile, not to mention, again, JS consumers).

About compiler-generated functions that would resolve the overload dynamically, I don't think it's mutually-excluded with user-generated such functions.

An early implementation could adapt my suggestion, and later define cases where the compiler might generate such function, and let programmer omit the entry-function on such cases.
Anyway, IMHO, it would still be worth the better compile-time checkings for overloaded functions, and the better separation of concerns.

@mhegazy
Copy link
Contributor

mhegazy commented Jan 12, 2017

This has been already discussed in #3442. I would recommend keeping the discussion in one thread.

As mentioned in #3442 (comment), this is currently out of scope of the TS project.

@mhegazy mhegazy added the Duplicate An existing issue was already created label Jan 12, 2017
@Shlomibo
Copy link
Author

@mhegazy
This is different from that suggestion.
And IMHO aligns with the 1-5 of the design goals of TS

@froh
Copy link

froh commented Jan 13, 2017

@mhegazy it's different from #3442 . That is suggesting dynamic type checks of the caller arguments signature to dispatch to the right function. Here there is the proposal to select the signature to call statically (e.g. via a well defined compact hash on the argument type signature). That is a major difference in the readability of the emitted code. As long as the hashing generates a reasonably short hash, the emitted code will continue to be human-readable, intelligible and thus compliant to both the TS goals and non-goals.

I'm not sure what the best format on github would be to give this pro and con discussion a better format than the forth and back here that is growing longer and longer. we need to juxtapose the pros and cons of having or not having function overloads. and we need to juxtapose implementation alternatives. Do we use some google drive like document or for heavens sake something wiki-ish, where we could have such sections "pro/con overloads", and "implementation alternatives" and "pro/con alternative 1, 2, 3" ?

@aluanhaddad
Copy link
Contributor

@froh #3442 explicitly states that it would be resolved statically.

@Shlomibo
Copy link
Author

@RyanCavanaugh
I believe this suggestion can solve the problems pointed out in these issues and few more
#13235
#13225
While remaining faithful to the goals of TS.
Would you consider looking at it?

Thank you very much

@Shlomibo
Copy link
Author

@aluanhaddad
Being dynamically executed, and exposed as a single function which can be consumed like all JS functions, from any JS code, and without ugly decorations (make the mangling whatever you want. it would still make the calls ugly) that may change from one version of the code to another - is exactly what makes this suggestion different from the others, and in the scope of TS design goals.

I actually oppose any static calls between modules. I believe static calls should be limited to the module itself - making them so limited that I wouldn't bother implement them beyond the overloaded function itself.

@mhegazy
Copy link
Contributor

mhegazy commented Jan 16, 2017

@mhegazy it's different from #3442 . That is suggesting dynamic type checks of the caller arguments signature to dispatch to the right function. Here there is the proposal to select the signature to call statically (e.g. via a well defined compact hash on the argument type signature). That is a major difference in the readability of the emitted code. As long as the hashing generates a reasonably short hash, the emitted code will continue to be human-readable, intelligible and thus compliant to both the TS goals and non-goals.

I am assuming this proposal is not meant for overloads that only target primitives like number and string. So the emitted code needs to handle a full range of inputs, i would assume something like:

  • arg === null for null types
  • typeof arg === for undefined, number, string, function, and object
  • instanceof arg === for classes (may be?)
  • some sort of structural comparison for other types (somehow...)

Now, the complexity of detecting structural types a side, this would be type directed emit, and it would violate one of the TS design goals, namely 8, and 3 (and though subjective 4).

This also breaks one main TS scenario which is isolated module transpilation (i.e. transpiling a single file without loading the whole program).

This proposal is different in details from the one in #3442, but it runs against the same constraints.

The issue of overloads has been one that was discussed in length in the early days of TypeScript; given the constraints we are working within, and the familiarity of JS developers with dynamically checking types of inputs to determine the intended function behavior, the current design seemed like the right place tom be.

@Shlomibo
Copy link
Author

Shlomibo commented Jan 16, 2017

@mhegazy

This proposal is different in details from the one in #3442, but it runs against the same constraints

How come?
Please elaborate on that.

The emitted code is almost identical to the written code (there's no really code generation).
Also, this proposal does not handle the selection and dispatch of the correct overload (beside static calls between overloads - which does not affect modularity): therefore the emitted code needs not handle any of these scenarios.

Did you read it? I think you are confusing this proposal with some other one.

@HerringtonDarkholme
Copy link
Contributor

HerringtonDarkholme commented Jan 16, 2017

Now, the complexity of detecting structural types a side, this would be type directed emit, and it would violate one of the TS design goals, namely 8, and 3.

type directed emit means, your generated code is dependent on the type annotation.

That is, to put it concrete,

function overload(a: string) { ... } // mark 1
function overload(b: number) { ... }  // mark 2

generates

function overload(arg) {
  function overload$string { // this depends on `string` annotation on mark1
   ...
  }
  function overload$number { // this depends on `number` annotation on mark2
  }
}

Which effectively means, if your code changes, only in type annotation, to

function overload(a: boolean) { ... } // mark 1
function overload(b: number) { ... }  // mark 2

generates

function overload(arg) {
  function overload$boolean { // the generated code changes solely because type on mark 1 changes
   ...
  }
  function overload$number { // this depends on `number` annotation on mark2
  }
}

In case English is not your native language, you can read directed's definition. And dependent here

@Shlomibo
Copy link
Author

Shlomibo commented Jan 16, 2017

@HerringtonDarkholme
False.
Because implementation are enclosed inside the entry-overload (which means they don't clutter the namespace/scope) - you could name them however you'd like.

They could be named
imp_1(...)
imp_2(...)
etc' regardless of name of the overloaded function, nor the implementations` params` types.

@HerringtonDarkholme
Copy link
Contributor

Add an ambiguity error

What is ambiguity? What about this?

interface CanMove {move: Function}
class Cat {
  // move() {} // toggle comment on this line
}
function overload(a: CanMove | Cat) {
   if (a instanceof Cat) {
     overload(a) // overload$CanMove or overload$Cat ? 
   }
}
function overload(a: CanMove)
function overload(a: Cat)

@avchugaev
Copy link
Contributor

@aluanhaddad yep

@Shlomibo
Copy link
Author

Shlomibo commented Jan 16, 2017

@aluanhaddad
It is beside the point, as the proposal is not dependent by any mean on a specific mangling.
As I said before:

you could name them however you'd like.

Is it so hard for you to accept?

I'm not assuming anything, but that this is an unrelated issue.
They could could be named imp_1, imp_2... etc.
They could could be named <overloadname>_TS_AWESOME, <overloadname>_TS_GREAT, etc from a list of 100 adjectives.
They could be named as a UUID.

They could be named however the design team would see appropriate.

@HerringtonDarkholme
Have no idea...
I wonder if the man who created C# solved such issues.

Can I write code like

static int X(string s) {... }
static int X(object o){...}
...
X("abc");

Are there solid and deterministic rules to rank and distinguish between overloads?
To check type compatibility of arguments?
Is a class more specific than interface?

@avchugaev
Copy link
Contributor

@HerringtonDarkholme overloaded functions must be defined for specific type. This means you should not use types like CanMove | Cat.

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Jan 16, 2017

@Shlomibo

Have no idea...
I wonder if the man who created C# solved such issues.

Can I write code like

static int X(string s) {... }
static int X(object o){...}
...
X("abc");
Are there solid and deterministic rules to rank and distinguish between overloads?
To check type compatibility of arguments?
Is a class more specific than interface?

Indeed he did. See #12041 (comment)

@avchugaev
Copy link
Contributor

compiler should select method with closest type:

  • class that match argument type
  • super class that match argument type
  • interface.
    Correct me if I'm wrong.

@Shlomibo
Copy link
Author

@achugaev93
And whenever there is no obvious preference... It would be an ambiguity error :)

@Shlomibo
Copy link
Author

Shlomibo commented Jan 16, 2017

@aluanhaddad
It is not a model for TS.
It can model overloads in this context. (as you can surely notice, these would not be C# overloads... what is an entry-overload in C#?!)

@aluanhaddad
Copy link
Contributor

compiler should select method with closest type:

class that match argument type
super class that match argument type
interface.
Correct me if I'm wrong.

Different language, different runtime, different rules.

And whenever there is no obvious preference... It would be an ambiguity error :)

Introducing a massive breaking change by turning the long accepted method of describing a function that can take different sets of arguments at runtime is a non-starter.

@avchugaev
Copy link
Contributor

Different language, different runtime, different rules.

It would be greate typescript follow general OOP rules. Other languages already passed this way successully. Why TypeScript can't?

@Shlomibo
Copy link
Author

@aluanhaddad

Introducing a massive breaking change by turning the long accepted method of describing a function that can take different sets of arguments at runtime is a non-starter

No it is not.
It is introduced with new syntax. Meaning any valid program of today is not using that syntax, and therefore cannot break by this syntax rules

@aluanhaddad
Copy link
Contributor

It would be greate typescript follow general OOP rules. Other languages already passed this way successully. Why TypeScript can't?

Because TypeScript is a superset of JavaScript targeting JavaScript Virtual Machine implementations, such as V8 and Chakra, and aims to not emit semantically different JavaScript based on the static type annotations it adds to the language. TypeScript was designed to align precisely with ECMAScript, both behaviorally and idiomatically, in the value space.
In other words this was a design decision.

@avchugaev
Copy link
Contributor

So make an exception for methods overloading in the design decisions.

@avchugaev
Copy link
Contributor

Sometimes it's necessary to break rules ;)

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Jan 16, 2017

To add to this a bit, TypeScript and C# both have exceptional synergy with their respective runtimes. C#'s fantastic support for precise overloading is achieved through synergy with the CLR.
For example, in the CLR generic type instantiations are reified, which is great, and C# leverages that to allow the following to works perfectly

public static double Sum(IEnumerable<double> values);
public static double? Sum(IEnumerable<double?> values);
public static int Sum(IEnumerable<int> values);
public static int? Sum(IEnumerable<int?> values);

In Java, all of those overloads are compiler errors because it cannot differentiate between them due to generic erasure in the JVM.
Trying to emulate the C# behavior in languages that target the JVM is problematic and complex.

@Shlomibo
Copy link
Author

@aluanhaddad
We're not emulating anything, as the dynamic dispatch is still done by the user, as the underlying runtime (JS) expecting.

We're only using existing rules for the small context where static resolution is acceptable: that is inside the entry-overload (which has no equivalent in C# - because we're not emulating it)

@aluanhaddad
Copy link
Contributor

Just thought it was relevant food for thought on whether or not this is a good idea. This decision has already been firmly established by the TypeScript team.
However, I find the discussion interesting in itself.

@Shlomibo
Copy link
Author

The decision is made different proposal, with demerits which I think this proposal working around them

@mhegazy mhegazy closed this as completed Apr 21, 2017
@rhyek
Copy link

rhyek commented Apr 27, 2017

Traditional method overloading is practically ubiquitous in OOP. Rather than being anal about some design philosophies made who knows how long ago (and which new users don't care about), try to make an exception every now and then when the benefits outweigh the drawbacks which in this case seem to me to be mostly about being afraid to color outside the lines.

It doesn't seem to me, from reading this thread, that it's impossible to implement the feature. I've just started using TS, but have already come across many situations where the lack of it has made me write ugly code that is felt as a hindrance throughout my project.

TS is great. The lack of true method overloading is not.

I am sure the core team itself believes the current implementation isn't the best since you practically don't see any documentation of it. Nevermind.

@idchlife
Copy link

idchlife commented Apr 28, 2017

So, basically current overloading simplified is like:

class A {
  overloaded(a: string | number) {
    if (typeof a === "string") {
      console.log("Got string");
    } else if (typeof a === "number") {
      console.log("Got number");
    }
  }
}

const a = new A();

a.overloaded(2);

a.overloaded("something");

I mean this example I wrote is event easier to write instead of many methods with different parameters and one method in the end with one parameter.
Well, when there are multiple parameters, that's the case, but... Solved by default value of parameter and type aliases.

So basically no overloading at all. Just combined types and developer should write by hand overloading functionality everytime (if this than that)

Am I right?

Further into discussion of ONE TRUE MORTY OVERLOADING

TypeScript denied requests about runtime checks etc, because TS is about compile-time type checking and functionality.

True overloading is not possible because TypeScript would have to add runtime check into compiled code. It's this story again - no runtime checks.
TS have different philosophy, you guys!

Maybe someone can come up with idea, that is compatible with TypeScript and can give us functionality of overloading? Like something with decorators maybe, generics, etc?

@RyanCavanaugh
Copy link
Member

Realistically, JavaScript does not have function overloading and in general I advise people to just not use overloading at all. Go ahead and use union types on parameters, but if you have multiple distinct behavioral entry points, or return types that differ based on inputs, use two different functions! It ends up being clearer for callers and easier for you to write.

@idchlife
Copy link

@RyanCavanaugh right now I stumbled upon this situation. I can have my object from container via generic get(class) or get(name: string). Container has 2 registrations: by class and by name.

The thing is it took too much effort to force this overloading work and I already missing one feature - generic with overloading filled with if else does not return type that was provided in argument but it's default generic type (used in <> in class).

Anyway, long story short: get(), and getByName() right now.

But!

Libraries like this are working nicely with overloading: https://github.com/typeorm/typeorm/blob/master/src/entity-manager/EntityManager.ts#L59

What you are talking about is (if I'm not mistaken): do not use TypeScript overloading.

But some people find it very helpful, as we can learn from library I mentioned.

@rhyek
Copy link

rhyek commented Apr 28, 2017

@RyanCavanaugh :

use two different functions! It ends up being clearer for callers and easier for you to write.

I guess you're right! I should stop using function overloading in Java and C# entirely and go with this approach because they are just too impractical!

@RyanCavanaugh
Copy link
Member

What's the sarcasm for? We can and should apply different heuristics for environments with compile-time method resolution vs those without.

@Shlomibo
Copy link
Author

@RyanCavanaugh

Realistically, JavaScript does not have function overloading and in general I advise people to just not use overloading at all. Go ahead and use union types on parameters, but if you have multiple distinct behavioral entry points, or return types that differ based on inputs, use two different functions! It ends up being clearer for callers and easier for you to write

But, TS suppose to support common patterns in JS, while helping to eliminate common bugs.
And overloading multiple functionalities into single function is still common in JS, and error prone (if you'll forget to call or implement an "overload", nothing would tell you that).
That's the reason TS had to have some support of functions overloading in the first place.

My suggestion also helps in partitioning such overload into different functions, making the code clearer.
It helps to achieve exactly what you say is better (and I tend to agree) with function overloading.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

9 participants