Assemblyscript: Investigate method & constructor overloading (AdHoc Polymorphism)

Created on 5 Sep 2019  路  19Comments  路  Source: AssemblyScript/assemblyscript

This little bit verbose but valid TypeScript syntax for constructor overloading. Will be nice to have this

class Field {
  public name: string;
  public surname: string;

  constructor();
  constructor(name: string);
  constructor(name: string, surname: string);
  constructor(code: i32);
  constructor(name?: number | string, surname?: string) { // this could be ignored by AS. Just need for linter / diagnostiq
    if (name instanceof String && surname instanceof String) {
      this.name    = name;
      this.surname = surname;
    } else if (name instanceof String && !surname) {
      this.name    = name;
      this.surname = "<unknown>";
    } else if (name instanceof i32) {
      this.name    = name.toString();
      this.surname = "<unknown>";
    } else {
      this.name    = "<unknown>";
      this.surname = "<unknown>";
    }
  }
}

console.log(new Field('Jonh','Smith')); // Field { name: 'Jonh', surname: 'Smith' }
console.log(new Field('Jonh')); // Field { name: 'Jonh', surname: '<unknown>' }
console.log(new Field(123)); // Field { name: '123', surname: '<unknown>' }
console.log(new Field()); // Field { name: '<unknown>', surname: '<unknown>' }
enhancement

All 19 comments

This feature unblock some standard library methods like Array.from with different signatures

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 new Field("John", "Smith"); which could be the second or the forth constructor. However, in the cases where they are disjoint, e.g. (assuming that the last constructor only took a string) new Field(123) could be it's own function.

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 super()

Thanks! this() is what Java and C# do, but this.constructor() is clearer. Perhaps stick with this since users that are comfortable with constructor overloading would expect it.

@dcodeIO yeah, makes sense.

@willemneal Great! Just one suggestion

class Field {
  ...
}

Which will be more consistent with super()

Wouldn't

constructor(name: string, surname?: string) { this(name); // or this.constructor ? if (surname) this.surname = surname; }
be

constructor(name: string, surname: string) {
    this(name); // or this.constructor ?
    this.surname = surname;
}

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 @method(x) decorator it could be @link, @overload, @redirect, @backward

also not sure about @method(Color.fromNum) / @method(this.setColorFromNum) probably better use @method("fromNum") / @method("setColorFromNum")

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 T, I'd most likely favor defining a method multiple times with different signatures. Requires our custom language server for proper highlighting first, though, but would be fine for me considering that one can still write code that is compatible with tsc by not using the feature.

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 constructor<string,u32,u32,u32> and a constructor<u32,u32,u32,u32> depending on arguments.

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

this(...);

That's only compatible with extending Function in JS:

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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

evgenykuzyakov picture evgenykuzyakov  路  3Comments

jerrywdlee picture jerrywdlee  路  4Comments

MaxGraey picture MaxGraey  路  4Comments

vladimir-tikhonov picture vladimir-tikhonov  路  4Comments

Iainmon picture Iainmon  路  3Comments