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
Investigate method & constructor overloading (AdHoc Polymorphism) #816
Comments
This feature unblock some standard library methods like |
Currently we can partially emulate this via generics but this not intellisense-friendly and not possible for constructors |
I know that we want to try to match TS as much as possible, especially because the IDE support, but one area where we can be an improvement over TS is with proper method overloading. When there is still an intersection of the types used at the call site and the various signatures, e.g. here it would be class Field {
public name: string = "<unknown>";
public surname: string = "<unknown>";
constructor() {}
constructor(name: string) {
this.name = name;
}
constructor(name: string, surname: string = "<unknown>") {
this.name = name;
this.surname = surname;
}
constructor(name: i32, surname: string = "<unknown>") {
this.name = name.toString();
this.surname = surname;
}
} I do understand the downside to moving away from complete TS support in IDE's, but in my opinion the above is much clearer and we could even allow for them to call each other. e.g. class Field {
public name: string = "<unknown>";
public surname: string = "<unknown>";
constructor() {}
constructor(name: string) {
this.name = name;
}
constructor(name: string, surname?: string) {
constructor(name);
if (!!surname){
this.surname = surname;
}
}
constructor(name: i32, surname?: string) {
constructor(name.toString(), surname);
}
} The benefit here is that they could be used in interfaces this way too. |
Maybe we can make breaking with TS optional. So if someone wants it, they can do that, or if they want compatibility, they'd use TS's notation and accept the limitations? |
@dcodeIO yeah, makes sense. @willemneal Great! Just one suggestion class Field {
public name: string = "<unknown>";
public surname: string = "<unknown>";
constructor() {}
constructor(name: string) {
this.name = name;
}
constructor(name: string, surname?: string) {
this(name); // or this.constructor ?
if (surname) this.surname = surname;
}
constructor(name: i32, surname?: string) {
this(name.toString(), surname);
}
} Which will be more consistent with |
Thanks! |
Wouldn't
be
since as it stands, this can already be merged with the existing constructor (without surname)? |
So as I promised on recent meeting I suggest new approach for overloading methods which partially compatible with TS. Example: import utils from './utils';
export class Color {
static fromNum(color: u32): Color {
return new Color(
(color >> 24) & 0xFF,
(color >> 16) & 0xFF,
(color >> 8) & 0xFF,
(color >> 0) & 0xFF
); // implicitly call `constructor(a: u32, r: u32, g: u32, b: u32);`
}
static fromStr(color: string): Color {
return new Color(utils.str2num(color)); // implicitly call Color.fromNum
}
@method(Color.fromNum) constructor(color: number);
@method(Color.fromStr) constructor(color: string);
constructor(a: u32, r: u32, g: u32, b: u32);
constructor() {
// default implementation
this.a = a; this.r = r; this.g = g; this.b = b;
}
/*private?*/ setColorFromNum(color: number) { /*...*/ }
/*private?*/ setColorFromStr(color: string) { /*...*/ }
@method(this.setColorFromNum) setColor(color: number): void;
@method(this.setColorFromStr) setColor(color: string): void;
setColor(a: u32, r: u32, g: u32, b: u32): void {
/*...*/
}
} link to playground Instead also not sure about Thoughts? |
Might work, but looks quite complicated. Personally I'd most likely do something more reliable like this: export class Color {
static create<T>(value: T): Color {
if (isInteger<T>()) { ... }
else if (isString<T>()) { ... }
else ERROR("...");
}
constructor(a: u32, r: u32, g: u32, b: u32)
this.a = a; this.r = r; this.g = g; this.b = b;
}
} If arguments differ in more than just a class Color {
constructor(a: u32, r: u32, g: u32, b: u32)
this.a = a; this.r = r; this.g = g; this.b = b;
}
constructor(s: string) {
this(...);
}
} |
Yes, but what if you want different implementation for different arity (number of args)? |
Ah, sorry, covered this in an edit :) |
class Color {
constructor(a: u32, r: u32, g: u32, b: u32)
this.a = a; this.r = r; this.g = g; this.b = b;
}
constructor(s: string) {
this(...);
}
} Will be ideal but it completely uncompatible with TS. Perhaps with our own language server it will be possible, but we loose portability mode EDIT In other hand we already has precedence with top level decorators and operator overloads |
There's also the option to support union types in scenarios like this (just not for variables), compiling them similar to generics: constructor(a: string | u32, r: u32 = 0, g: u32 = 0, b: u32 = 0)
if (isString(a)) {
...
} else {
this.a = a; this.r = r; this.g = g; this.b = b;
}
} giving us a |
Yeah, but it much more complicated and potentially error-prone approach |
Perhaps we also can export class Color {
constructor(s: string);
constructor(a: u32, r: u32, g: u32, b: u32);
constructor() {
// inject arguments[0..3] and arguments.length statically?
// so either arguments.length == 1 and arguments[0] -> string
// or arguments.length == 4 and arguments[0..3] -> u32
}
} |
Yeah. And in this case all checks better do statically. That's most TypeScrip-tish way |
That's only compatible with extending class Foo extends Function {
constructor(...args) {
super(...args)
this()
}
}
new Foo('console.log("foo")') // logs "foo". My vote is for not straying so far away from TS. Can we have an option to enforce strict TS-compatible syntax so that we can get a compile error when not writing TS-compatible code? I think that'd be great! |
Another possible alternative: export class Color {
@overload((s: string) => this);
constructor(a: u32, r: u32, g: u32, b: u32) {
}
@overload((color: string) => void)
setColor(a: u32, r: u32, g: u32, b: u32): void {
// @ts-ignore
if (color instanceof string) {
// parse from string
} else {
// use a, r, g, b
}
}
} And with unions and multi-values: export class Color {
constructor(a: u32 | string, r?: u32, g?: u32, b?: u32) {
}
setColor(a: u32 | string, r?: u32, g?: u32, b?: u32): void {
if (a instanceof string) {
// parse from string
} else {
// use a, r, g, b
}
}
} |
Constructor overloading can be very useful if your compilation target is only webassembly. Obviously that some switch will be better for those who don't want to compile sources to javascript. Sometime this is very handy because the language itself become more powerful. |
I think AS just needs unions. Then using TypeScript overload syntax will be relatively easy. Also, supporting multiple (compatible and incompatible) syntaxes would make more maintainance burden, right? |
Whichever approach is taken it should be compile time dispatch. Using if/else means it is runtime dispatch which does not have any benefits. Regarding the syntax, I suggest breaking TS syntax here which is in my opinion very ugly and strange. I like this syntax the most: |
Note that with strict types, conditional branches are eliminated during compile time based on what types the compiler detects are used at the call sites.
We should not break from TypeScript syntax (in any way we already have, we should provide compatible alternatives). The compatibility with existing tooling is a huge benefit of AssemblyScript. The syntactic difference is only a small inconvenience, easy for any engineer to live with, and I believe it would not be worth the potential fragmentation (more on that below). |
This opens the door to possible community fragmentation: It may be nice for authors to choose compatible or incompatible syntax (and for every incompatible syntax we'd expect there to be a compatible syntax option), but what happens when downstream library users import incompatible code into their environments? Suppose a library user wants compatibility with existing TypeScript tooling (VS Code, ESLint, etc) and they wish to write all their code using compatible features. What happens when they import a library that doesn't use compatible syntax? ^ If this doesn't work, it will cause fragmentation.
Without declaration files, then TypeScript will need to type check sources from inside node_modules and will throw errors on incompatible syntax. To solve this fragmentation issue, we would need to ensure we can emit compatible declaration output regardless of the source syntax. It could be possible that some incompatible syntax may not have any way to be represented compatibly in declaration output, and we would need to absolutely avoid doing that or otherwise cause permanent fragmentation. At that point, someone would surely hard-fork AssemblyScript. Furthermore, who would be willing to ensure that compatible and incompatible features output the same declaration output? This is work someone will need to take on. Instead that effort could be used on the easier path forward: using compatible syntax and working on supporting more TS-compatible language features (closures, unions, etc), while not worrying about declaration emit. |
I know that |
Well, the description of AssemblyScript is "Definitely not a TypeScript to WebAssembly compiler". Trying to stay compatible with TypeScript's handling of functions/methods that is designed for JavaScript is just continuing a mistake in the design. I am all for backward compatibility as long as it is not a technical debt. AssemblyScript is young. It should not limit itself to something that was decided in 1995 for JavaScript. |
This little bit verbose but valid TypeScript syntax for constructor overloading. Will be nice to have this
The text was updated successfully, but these errors were encountered: