-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Comments
Possible duplicate of #3442. |
@HerringtonDarkholme Also, the two are not mutual exclusive. |
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? |
I haven't talked about ordering.
It is in the source code that the transpiler compiles. You should compare the transpiled code to the source. 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. |
name mangling proposal: md5-hash the normalized function signatureI 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?
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 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. |
@froh Because from JS POV there's only one possible function, I don't think exposing the name-mangled overloads would be a good idea. |
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. |
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. But, I can't even see the benefit of such scheme. 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? |
Ideas of class methods overloading implementation:
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# ;). |
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 |
@achugaev93 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.
@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. |
@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. |
You introduced the comparison in no uncertain terms
Please explain how this is relevant.
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. |
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? |
It is not half way if that is an explicit non goal: |
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. |
@achugaev93 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. |
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 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" ? |
@RyanCavanaugh Thank you very much |
@aluanhaddad 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. |
I am assuming this proposal is not meant for overloads that only target primitives like
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. |
How come? The emitted code is almost identical to the written code (there's no really code generation). Did you read it? I think you are confusing this proposal with some other one. |
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 |
@HerringtonDarkholme They could be named |
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) |
@aluanhaddad yep |
@aluanhaddad
Is it so hard for you to accept? I'm not assuming anything, but that this is an unrelated issue. They could be named however the design team would see appropriate. @HerringtonDarkholme 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? |
@HerringtonDarkholme overloaded functions must be defined for specific type. This means you should not use types like CanMove | Cat. |
Indeed he did. See #12041 (comment) |
compiler should select method with closest type:
|
@achugaev93 |
@aluanhaddad |
Different language, different runtime, different rules.
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. |
It would be greate typescript follow general OOP rules. Other languages already passed this way successully. Why TypeScript can't? |
No it is not. |
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. |
So make an exception for methods overloading in the design decisions. |
Sometimes it's necessary to break rules ;) |
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. 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. |
@aluanhaddad 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) |
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. |
The decision is made different proposal, with demerits which I think this proposal working around them |
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.
|
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. 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 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. 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? |
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. |
@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. |
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! |
What's the sarcasm for? We can and should apply different heuristics for environments with compile-time method resolution vs those without. |
But, TS suppose to support common patterns in JS, while helping to eliminate common bugs. My suggestion also helps in partitioning such overload into different functions, making the code clearer. |
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.
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.
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.
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.
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.
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
An overload must be called from reachable code in the entry-overload:
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:
We can very simply generate this code:
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.
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.
Should transpiled to something like
But I don't think this would be a common case.
I really really hope you would consider my suggestion.
The text was updated successfully, but these errors were encountered: