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: Extension methods #9

Closed
RyanCavanaugh opened this Issue Jul 15, 2014 · 164 comments

Comments

Projects
None yet
@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Jul 15, 2014

Allow a declarative way of adding members to an existing type's prototype

Example:

class Shape {
 // ...
}

/* ...elsewhere...*/
extension class Shape { // Syntax??
    getArea() { return /* ... */; }
}

var x = new Shape();
console.log(x.getArea()); // OK
@knazeri

This comment has been minimized.

Copy link

knazeri commented Jul 29, 2014

Highly suggested 👍
This would add methods to existing types without creating a new derived type or modifying the original type and it would definitely make sense in linq to typescript query operators

The original issue at codeplex had pretty much lots of votes:
https://typescript.codeplex.com/workitem/100

@basarat

This comment has been minimized.

Copy link
Contributor

basarat commented Jul 29, 2014

👍

1 similar comment
@mpawelski

This comment has been minimized.

Copy link

mpawelski commented Aug 13, 2014

👍

@basarat

This comment has been minimized.

Copy link
Contributor

basarat commented Aug 14, 2014

Perhaps this can be used in an ambient context as well :

declare module foo{
    class A{}
    extension class A{} 
}
@saschanaz

This comment has been minimized.

Copy link
Contributor

saschanaz commented Aug 14, 2014

extending A { } for short? I think we don't need class there.

@knazeri

This comment has been minimized.

Copy link

knazeri commented Aug 14, 2014

A couple of questions:
What would be the compiled js like? And how would be the runtime interoperability with the existing class?
Extension methods in C# are static methods with syntactic sugar for instance method call syntax with the first parameter specifying the type the method operates on. Which brings some benefits beside the instance call syntax:

  • Extension methods can be called directly as static method call
  • Extension classes are not newable
  • There could be different extension methods on the same type in the same namespace as long as each class containing extension methods has its own identifier

Having considered extension class syntax, is extension class actually a class or a module at runtime? How do you translate this in a body of an extension method at runtime? (Function.apply, Function.call maybe?) And how would extension methods be like on an interface which I believe is the most commonly use case for extension methods?

@zlumer

This comment has been minimized.

Copy link

zlumer commented Aug 15, 2014

While the naive implementation of extension methods is probably like this:

extension class Shape { // Syntax??
    getArea() { return /* ... */; }
}
console.log(x.getArea()); // OK

Compiling to:

// js
var _Shape = {
    getArea:function() { return /* ... */; }
}
console.log(_Shape.getArea.call(x));

I think due to first-class functions in JS there is a problem with this approach:

extension class Shape { // Syntax??
    getArea() { return /* ... */; }
}
var x = new Shape();
var y = new Shape();
console.log(x.getArea); // Function or undefined?
x.getArea.foo = "bar";
console.log(y.getArea.foo); // "bar" or undefined?

If extension methods are to be implemented, there would probably have to be restirictions on accessing/changing their properties.

Considering "class" syntax

extension class Shape { // Syntax??
    getArea() { return /* ... */; }
}

In my opinion, one of the common uses of extension methods is to make them work on interfaces, not just classes, so you could do something along the lines of

interface IPoint {
    x:number;
    y:number;
}
class Point implements IPoint {
    constructor(public x,public y) {
        // ...
    }
}
var p1:Point = new Point(0, 50);
var p2:IPoint = { x:32, y:32 };

console.log(p1.getLength()); // extension method
console.log(p2.getLength()); // same extension method

If possible, I think C#-like extension methods would work best:

extension function getLength(this p:IPoint):number {
    return Math.sqrt(p.x*p.x + p.y*p.y);
}
// another extension method with the same name but for different class
extension function getLength(this s:string):number {
    return s.length;
}

console.log(getLength); // compiler error
console.log(p1.getLength); // Function or undefined or compiler error?
console.log(p1.getLength.foo); // compiler error
p1.getLength.foo = "bar"; // compiler error

Compiling to:

// js
function _IPoint$getLength() {
    return Math.sqrt(this.x*this.x + this.y*this.y);
}
function _string$getLength() {
    return this.length;
}
console.log(_IPoint$getLength.call(p1)); // OK
console.log(_IPoint$getLength.call(p2)); // OK
@saschanaz

This comment has been minimized.

Copy link
Contributor

saschanaz commented Aug 15, 2014

Hmm, I thought that this proposal was just to extend prototypes.

class A {
    foo() {

    }
}
extending A {
    bar() {

    }
}

Compiling to:

var A = (function () {
    function A() {
    }
    A.prototype.foo = function () {
    };
    return A;
})();
A.prototype.bar = function () {
};
@knazeri

This comment has been minimized.

Copy link

knazeri commented Aug 15, 2014

I don't think extension methods should be implemented by adding a function to the prototype! For one thing extending the prototype of built in types, is considered dangerous, 3rd party libraries may also add a method with the same name to the prototype, causing the behavior to depend on which JS file was loaded last, or worse you might overwrite the existing built-in methods! And for another, there's no way to have different method overloads for the extension methods defined in different modules!
More importantly, prototype extending works only for concrete classes, in a sense, no interface or declared class extension methods!

@basarat

This comment has been minimized.

Copy link
Contributor

basarat commented Aug 15, 2014

I still agree with @saschanaz . There are issues surely but for :

3rd party libraries may also add a method with the same name to the prototype, causing the behavior to depend on which JS file was loaded last, or worse you might overwrite the existing built-in methods

this would be caught by name collision in the lib's .d.ts file and your custom.d.ts file.

More importantly, prototype extending works only for concrete classes, in a sense, no interface or declared class extension methods

I don't expect it there to be any codegen for ambient declarations. PS: If you ambiently say something is a class TypeScript already assumes it has a prototype. e.g.

declare class Foo{}
console.log(Foo.prototype); // Compiles fine

So adding to the ambient prototype is consistent with what already exists.

Perhaps examples of your concerns (if still there) would help.

@knazeri

This comment has been minimized.

Copy link

knazeri commented Aug 15, 2014

this would be caught by name collision in the lib's .d.ts file and your custom.d.ts file.

This is only true if there is .d.ts metadata for every 3rd party library (which is not) and only when .ts files are being compiled together (ie. in the same project). Besides there are lots of .js libraries as part of server-side environment which are being injected into dynamic pages at runtime (take asp.net server controls for example)

If you ambiently say something is a class TypeScript already assumes it has a prototype

Ambient class declaration does not have implementation, which is not the case for extension methods, unless extension methods for ambient class declarations are being erased in the compiled .js (which I believe is counterproductive to it's purpose), the program might break at runtime!

And how about interfaces? To extend an interface new methods has to be added to the interface, but changing the interface by adding new methods would break compatibility with existing consumers! Take IEnumerable<T> in C# for example, I cannot imagine how many classes are already implementing this interface! Without having extension methods on interfaces, there could be no LINQ as we know it!

I believe the real advantage of extension methods is being loosely coupled with the existing type, e.g.

Array.prototype.join = function (inner, outerKeySelector, innerKeySelector, resultSelector) {
};

The above example is supposed to add linq-like join operator for arrays, however having altered the prototype, we have lost the built-in join method. Having extension methods separated in a concrete different method actually benefits better method overloading.

There's also a big benefit with generic extension methods which is reducing the need for a common base class.
The following is an example of using generic extension method in C#:

public static bool In<T>(this T obj, params T[] source)
{
    return new HashSet<T>(source).Contains(obj);
}

Which you can simply use:

var o = 1;
o.In(1, 2, 3, 4, 5); /// true
@RyanCavanaugh

This comment has been minimized.

Copy link
Member

RyanCavanaugh commented Aug 15, 2014

Great discussion so far.

There's a key decision point here on this feature, which is how the emitted code would work. I see two options based on what's been discussed so far

Example code I'll use

class Square { /* ... */ }
extension function getArea(this: Square) { // First param must be 'this'
    return this.width * this.height;
}
var x = new Square(10, 10), y: any = x;
console.log(x.getArea());
console.log(y.getArea());

Add to .prototype

/* Regular emit for 'Square' here */
Square.prototype.getArea = function() {
    return this.width * this.height;
}
var x = new Square(10, 10), y = x;
console.log(x.getArea());
console.log(y.getArea()); // Succeeds

Pros

  • Works even if type information is missing (y)
  • Accurately models many existing libraries' behavior

Cons

  • Doesn't work for interfaces, which is probably a primary use case
  • Adding to prototype is often a bad idea, especially for built-in objects

Rewrite call sites

/* Regular emit for 'Square' here */
__Square_getArea = function() {
    return this.width * this.height;
}
var x = new Square(10, 10), y = x;
console.log(__Square_getArea.call(x));
console.log(y.getArea()); // Fails

Pros

  • Works on interfaces as well as classes
  • Don't mess around with prototype, so it's safe for built-in types

Cons

  • Unclear what it means to use this method in a non-call position
  • Fails unexpectedly when strong type information is absent
  • Doesn't model existing JS library behavior; we'd still then need an alternate syntax for those
@knazeri

This comment has been minimized.

Copy link

knazeri commented Aug 15, 2014

@RyanCavanaugh I would alter the example code, to use the type parameter for the extension method instead of dynamic binding of this :

class Square { /* ... */ }
extension function getArea(this square: Square) { // First param must be 'this'
    return square.width * square.height;
}
var x = new Square(10, 10);
console.log(x.getArea());

Which would translate to this:

function getArea(square) {
    return square.width * square.height;
}
var x = new Square(10, 10);
console.log(getArea(x));

So that the function behave like a static helper method and it's valid to call it non-instance way. However still fails when strong type information is not present. You can call the extension function explicitly:

var x = new Square(10, 10), y:any = x;
console.log(x.getArea()); // Succeeds
console.log(getArea(y));  // Succeeds
@ivogabe

This comment has been minimized.

Copy link
Contributor

ivogabe commented Aug 15, 2014

I like the suggestion of @KamyarNazeri, this can be used on an enum (or an union in the feature) too. This code is also faster than a prototype call or a .call().

Would the extension keyword be necessary? Since you already add this to the first argument.

@knazeri

This comment has been minimized.

Copy link

knazeri commented Aug 15, 2014

@ivogabe +1 for mentioning enum, I would like to also mention that using static functions, you can call methods on objects that are null, e.g. isNullOrEmpty for string type. small things yes, but darn useful

@saschanaz

This comment has been minimized.

Copy link
Contributor

saschanaz commented Aug 15, 2014

How about this?

class Square { /* ... */ }
function getArea() joins Square {
    return this.width * this.height;
}
var x = new Square(10, 10);
console.log(x.getArea());

Translates to:

/* Regular emit for 'Square' here */
__Square_getArea = function(_this) {
    return _this.width * _this.height;
}
var x = new Square(10, 10);
console.log(__Square_getArea(x));

PS: Or without renaming, as @KamyarNazeri did. I like the non-instance way.

function getArea(_this) {
    return _this.width * _this.height;
}
var x = new Square(10, 10);
console.log(getArea(x));
@RyanCavanaugh

This comment has been minimized.

Copy link
Member

RyanCavanaugh commented Aug 15, 2014

I want to emphasize the danger of the "rewrite call sites" approach:

class Square { /*... */ }
/* Extension method getArea, syntax irrelevant */
function process(callback: (n: Square) => void) {
    /* invoke callback on a variety of Squares */
}

process((n) => {
    n.getArea(); // Succeeds
});
process(((n) => {
    n.getArea(); // No compile error, fails at runtime
}));

var x = new Square(), y: any;
var arr1 = [x];
arr1[0].getArea(); // Succeeds
var arr2 = [x, y];
arr2[0].getArea(); // No compile error, fails at runtime

There are very strong reasons TypeScript avoids rewriting code based on type information (see https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals). No one wants to get in a situation where adding a non-observed element to an array or adding extra parentheses around a function could causes program to start to fail at runtime.

@knazeri

This comment has been minimized.

Copy link

knazeri commented Aug 15, 2014

@RyanCavanaugh Seriously I don't understand why by adding extra parenthesis around a lambda function, the runtime infers variable n as any?
It's like to evaluate variable b's type argument in the following code as any (which is not)

var a = function (n: number): void { }
var b = (a);  // here b accepts number!

As for the latter example, I believe no-one expects an extension method to work when boxing variable to another type (here any due to the type mismatch in the array). At least this is how C# / VB work, with boxing to another type, there's no extension method! Only they don't compile in such situations.

@danquirk

This comment has been minimized.

Copy link
Member

danquirk commented Aug 15, 2014

We explicitly decided parenthesizing an expression should be the way to opt out of contextual typing or else you were always at the mercy of the inference algorithm even if it was wrong/incomplete.

@saschanaz

This comment has been minimized.

Copy link
Contributor

saschanaz commented Aug 16, 2014

Maybe we can choose separate syntax to call those functions.

class Square {
  foo() {
  }
}
function getArea() joins Square {
  return this.width * this.height;
}
var x = new Square(10, 10);
var y: any = x;
console.log(x calls getArea()); // Succeeds
console.log(y calls getArea()); // Succeeds both in compiling and in runtime.
console.log(x.getArea()); // Fails, this is only for functions in prototype
console.log(x calls foo()); // Fails, this is only for extension functions

PS: Fixed to allow y calls getArea().

@knazeri

This comment has been minimized.

Copy link

knazeri commented Aug 16, 2014

This turned to be more complicated that it looked like! Having rewrite call sites comes handy when coding, however I agree that the code might break at runtime! I've seen lots of javascript developers debugging their code in the browser developing tools which also fails using rewrite call sites.
But the ability to extend interfaces is so good that you can't simply ignore it, so probably we are addressing two different problems here:

  • Extending a class (by using extension methods)
  • Extending interfaces (including built-in types since built-in types are represented as interfaces in the typescript runtime)

Maybe we could consider Java 8 default methods (or as they are often called defender methods) to extend an existing interface. Of course default methods in an interface could translate back to prototype in the emitted js code:

interface ISquare {
    width: number;
    height: number;
    default getArea(): number {
        return this.width * this.height;
    }
}

class Square implements ISquare {
    constructor(public width: number, public height: number) {
    }
    // no need to implement getArea here
}

var x = new Square(10, 10), y: any = x;
console.log(x.getArea());  // Succeeds
console.log(y.getArea());  // Succeeds

which would compile to:

var Square = (function () {
    function Square(width, height) {
        this.width = width;
        this.height = height;
    }
    Square.prototype.getArea = function () {
        return this.width * this.height;
    };
    return Square;
})();
@basarat

This comment has been minimized.

Copy link
Contributor

basarat commented Aug 17, 2014

default methods in an interface could translate back to prototype in the emitted js code

This interface looks more like an abstract class. #6

@knazeri

This comment has been minimized.

Copy link

knazeri commented Aug 17, 2014

This interface looks more like an abstract class.

Only a class might already inherit another base class, I think the real benefit with interface extension comes with multiple inheritance simulation.

@ivogabe

This comment has been minimized.

Copy link
Contributor

ivogabe commented Aug 17, 2014

Maybe we could consider Java 8 default methods

The difference is that interfaces in TypeScript can be used without classes. How would the following code compile?

interface ISquare {
    width: number;
    height: number;
    default getArea(): number {
        return this.width * this.height;
    }
}
var square: ISquare = {
    width: 100,
    height: 50
};
console.log(square.getArea);

Maybe we can choose separate syntax to call those functions.

Maybe call the function like this: (since this syntax already exists)

getArea(square);
// instead of
square calls getArea();
@saschanaz

This comment has been minimized.

Copy link
Contributor

saschanaz commented Aug 17, 2014

Maybe call the function like this: (since this syntax already exists)

Chaining would become uncomfortable in that case. Example:

var num = new Number(0);
function plus(input: number) joins Number {
  return this.valueOf() + input;
}
num.plus(3).plus(3).plus(3);
num calls plus(3) calls plus(3) calls plus(3); // Well... not very good to see. Hmm

versus

plus(plus(plus(num, 3), 3), 3);

...while the rewritten JavaScript will be anyway plus(plus(plus ... form.

Edited:

num->plus(3); // Syntax that looks like the C++ one but for extensions?

or

num<-plus(3); // function 'plus' joins to 'num'.
@knazeri

This comment has been minimized.

Copy link

knazeri commented Aug 17, 2014

Yea well, object literals are not the only problem:

interface A {
    default void foo() {
        console.log("A");
    }
}

interface B {
    default void foo() {
        console.log("B");
    }
}

class Bar implements A, B {
    default void foo() {    // required for conflict resolution
        console.log("Bar"); 
    }
}

var o = new Bar();
A o1 = o;
B o2 = o;
o.foo(); // Bar
o1.foo; // probably want A
o2.foo; // probably want B

From the client code perspective, default methods are just ordinary methods, so in case of the simple example with one class that implements an interface with a default method, prototype seems to be fine, however most of the time client code that invokes the default method will have to invoke the method at the call site. same old story. 💤

@zlumer

This comment has been minimized.

Copy link

zlumer commented Aug 17, 2014

For the record, here's an example of runtime exceptions in C#:
http://stackoverflow.com/questions/5311465/extension-method-and-dynamic-object-in-c-sharp
While TypeScript is certainly far more likely to have untyped variables than C# (I myself have about 60% variables untyped in my TS code and only about 10% untyped in C# code), this can be considered a known limitation. Maybe extension methods should only work with a "--noImplicitAny" compiler flag to decrease the possibility of a runtime error.

Personally I'm strongly against any silent prototype modifications - changes to any existing prototype (everything except initial class-defining code generated by TS compiler) should always be explicit, having programmer manually type "class.prototype" to prevent at least some part of the errors.
Having said that, there's absolutely no reason to introduce new syntax for prototype extending: you can always do Square.prototype.getArea = /*...*/ and get all the syntactical benefits of such approach. If "rewriting call sites" is considered inappropriate, I think it's best to not introduce extension methods at all, than confuse programmers with a very dangerous silent prototype-changing feature.

Extension methods are nothing more than syntax sugar for method calls (at least that's how I see it and how they are implemented in C#). While certainly useful, they are not necessary.

@series0ne

This comment has been minimized.

Copy link

series0ne commented Nov 30, 2016

😯 @RyanCavanaugh you would not believe the post I'm writing right now...which, without seeing the bind operator proposal, is virtually identical (great minds...?) here goes...

Essentially the only way I can see this working is to introduce an operator which instructs the TypeScript compiler, this is an extension method call! This borrows from C++

Example

class StringExtensions {
    public static isUpper(this string value): boolean {
        return value.toUpperCase() === value;
    }
}

var x: string = "Hello World";

x->isUpper();

//or

x::isUpper();

Even if isUpper was already a member of String.prototype, TypeScript would know to use the extension method, not the prototype bound method, unless the operator changed.

In terms of dealing with things like union or intersection types, I think the only solution would be to make the extension method invariant; for example

var x: string | number = 0;

x::isUpper(); // Error, cannot call this because the extension method expects type "string"

Thus, you would require an overload to allow this behavior.

Again, I did NOT read about the bind operator before writing this, and it's just escaped my noodle, so there are likely some gaping holes!

P.S. probably want to go with ::, and not -> because TS already has => and =>/-> look too similar.

@JHawkley

This comment has been minimized.

Copy link

JHawkley commented Dec 1, 2016

Yeah, the bind operator would go a long way toward giving us extension-like behavior. It still isn't monkey-patching, won't fix types like you might want, and has its own challenges for integrating into TypeScript, but not impossible challenges.

TypeScript would have to start tracking the type of this in a function more aggressively, for instance. We already have that ability to explicitly specify its type with the fake this parameter, but it would have to be expanded to automatically infer the type of this for functions pulled from class prototypes and the like.

Anyways, I still think TypeScript can benefit from a compile-time abstraction for associating behavior to types, especially since it has one for structure. Many current JavaScript libraries are created in a manner that would work well with it and they wouldn't be helped by the bind operator. Also, it would reduce the compulsion to duplicate function implementations in separate classes or use unintuitive forms of inheritance, all just to get the nicer feeling method-call syntax. I've done that before...

I think I may submit my current proposal under the terminology the programming language D uses, 'Uniform Function Call Syntax', and try to keep the use of the words 'extension' or 'extends' to a minimum.

@danfma

This comment has been minimized.

Copy link

danfma commented Dec 1, 2016

Then maybe the Scala extensions would be help this kind of problem. In Scala, I can define a class which will provide methods and properties to an instance. So, in resume:

extension class MyExtension(target: any)
{
    printHello() {
        console.log("DO MY CODE");
    }
}

var something: any = ...;

something.printHello();

could be transformed to this equivalent:

class MyExtension(target: any)
{
    printHello() {
        console.log("DO MY CODE");
    }
}

var something: any = ...;
var something$MyExtension = new MyExtension(something);

something$MyExtension.printHello();

and then to the equivalent javascript code. Thus, In the end:

  • We won't have any problem with name collision because to use an extension I need to import it;
  • All the transformation is done by the compiler;
  • Yes, we have problems with additional allocation of objects when creating a new extension, but that is a feature that can or can't be used, so we just need to used it wisely;
@RyanCavanaugh

This comment has been minimized.

Copy link
Member

RyanCavanaugh commented Dec 5, 2016

@danfma I don't see how that's meaningfully different from the dozens of upthread suggestions for similar rewriting of call sites, nor how it addresses the serious problems with that approach.

@danfma

This comment has been minimized.

Copy link

danfma commented Dec 5, 2016

@RyanCavanaugh, that is just another suggestion like many others above :D

But I still don't get what are the problems with the previous ideas. For example:

let x = 10;

extension function plusOne(this self: number): number {
 if (typeof self !== "number") throw new TypeError(); // or anything else 
 return self + 1;
}

let y = 10.plusOne();

Generated javascript:

var x = 10;

function plusOne(self) {
  if (typeof self !== "number") throw new TypeError();
  return self + 1;
}

var y = plusOne(x);
  • We don't pollute the global variable space;
  • If we had a name colliding, with another extension or with a member of the object, when importing we just need to import with another name import { plusOne as plus1 } from "some/place";
  • The only problem that I see is when used in a global scope, but that is a problem that you will have with any library used in global scope;

Then, please, can you point me what you see that I can't see as a problem?

@danfma

This comment has been minimized.

Copy link

danfma commented Dec 5, 2016

About the extension classes, it has the advantage to allow you to extend with more than one method, but it will make the compiler life hard when having two extension classes with the same function name. One solution for that, is to not resolve automatically when that occurs, so if a name collides, the compiler can raise an error, and let the user resolve, by manually creating the extension by hand, for example.

@RyanCavanaugh

This comment has been minimized.

Copy link
Member

RyanCavanaugh commented Dec 5, 2016

All of the problems raised at #9 (comment) and #9 (comment) apply

@danfma

This comment has been minimized.

Copy link

danfma commented Dec 5, 2016

ok, I will check!

@danfma

This comment has been minimized.

Copy link

danfma commented Dec 5, 2016

@RyanCavanaugh, I got your points, but I still disagree in some points. Javascript itself is a dangerous language because you can do a lot of things to bug your own code and like the TypeScript team always talks 'TypeScript is JavaScript', so it inherits all the JS problems.

So when you say It's also possible to wire your house with uninsulated 36 gauge wire and bubble gum as long as you're willing to accept it sometimes catching fire, you are right, but you already accepted that too! :D

What I want to say is that even in C# you can have problems by using dynamic or Reflection, so I don't see the difference here.

Anyway, I will stop here and thank you for the long list provided before. 👍

@AndrewLang

This comment has been minimized.

Copy link

AndrewLang commented Dec 7, 2016

👍

@co3carbonate

This comment has been minimized.

Copy link

co3carbonate commented Dec 14, 2016

An alternative is to include a forward declaration of the methods you are extending in the class, then modifying the prototype to extend them. For example:

class Greeter {
    greeting: string;
    say: Function;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

Greeter.prototype.say = function(msg:string) {
    alert(msg);
}

let greeter = new Greeter("world");
greeter.say(greeter.greet()); // alerts "Hello, world"

Hope it helps

@mikeaustin

This comment has been minimized.

Copy link

mikeaustin commented Mar 22, 2017

Honestly, I'd love to see a move away from internal methods at all... some day? Monkey-patching and static helpers only go so far. CLOS and Dylan are good examples of adding methods to classes at any time -- without monkey-patching, and they're lexically scoped. The problem with C# extension methods is that they are statically dispatched, just some syntactic sugar.

If every method was external, you could do something like this:

var _capitalize = capitalize || new Map();

String.addMethod(_capitalize, function() {
return this.slice(0, 1).toUpperCase() + this.slice(1);
});

"foo".invoke(_capitalize);

// Implementation

Object.prototype.addMethod = function(method, func) {
method.set(this, func);
}

Object.prototype.invoke = function(method, args) {
return method.get(this.constructor).apply(this);
};

// Syntax thoughts, where extend creates a locally scoped external function.

extend String {
capitalize() { ... }
}

"foo".capitalize();

Just some thoughts :)
Mike

@profound7

This comment has been minimized.

Copy link

profound7 commented Apr 5, 2017

Some of the examples @RyanCavanaugh wrote on why rewriting call-sites wouldn't work for JS targets, actually works in Haxe (similar to TypeScript and can transpile to JS and others).

See here for one of the examples, in Haxe. Click the build button to see the trace outputs and the JS transpiled output.

Some of his examples don't work, but the Haxe compiler actually manages to catch some of those errors ("x has no method getArea"). However, using the any type (Dynamic in Haxe), will definitely not work, but haxe coders know why they don't work, because in haxe docs, it was stated that we should avoid using Dynamic as it turns off certain type checking features.

To make most of Ryan's examples work in haxe (haven't tried all of them), simply type cast or add type hinting so the compiler knows what to do. Of course, if someone convinced the compiler that a chicken is a duck, but it doesn't quack at runtime, then the problem isn't that the feature is impossible. It is the person who convinced the compiler that the chicken is a duck.

For union types (Square and Rectangle; where one has instance method getArea and the other a static extension getArea), the compiler should show an error because getArea does not really exist for one of them. It should be made that once the type is checked (through an if guard or something), then getArea should work (since within the if statement, the compiler can tell if its a square or rectangle).

Static extension methods can only work if the compiler is able to know the type as it is a compile-time feature. Programmers shouldn't expect something untyped/not properly typed/unverified to take advantage of a compile-time feature that requires type to be known. I think it is possible to implement and that it is also reasonable to expect it not to work if the compiler is unable to infer the type.

@RyanCavanaugh

This comment has been minimized.

Copy link
Member

RyanCavanaugh commented Apr 5, 2017

@profound7 Consider this code (Haxe):

class BetterSquare extends Square {
    public function getArea() {
        return this.x * this.y;
    }
}


class SquareExtension
{
    public static function getArea(square:Square)
    {
        trace("getArea!");
    	return square.x * square.y;
    }
}

class AThing {
    public static function doSomething() {
        var qq: Square = new BetterSquare(10, 10);
        qq.getArea();
    }
}

This results in calling the extension method, rather than the BetterSquare method. This breaks JS runtime semantics preservation -- qq.getArea() must call the instance method on the class because that's what JS does. The TypeScript type system never changes the runtime behavior of code; that is a core principle and a primary guarantee of our language.

@RyanCavanaugh

This comment has been minimized.

Copy link
Member

RyanCavanaugh commented Apr 5, 2017

(Be aware that any for code you can possibly think about emitting, I can show an example of how to use that behavior to break JS semantics)

@mikeaustin

This comment has been minimized.

Copy link

mikeaustin commented Apr 5, 2017

Ok, I get it. It mimics C# in a sense that it prioritizes class methods over extension methods. It seems possible to check if the method exists first, but I assume that would incur some overhead.

Personally, I like the idea of being able to shadow class methods. It opens the door to adding functionality, applying bug fixes, logging, security -- AOP cut-points, etc. If they are lexically scoped, there's no risk to other code (other than trojan horses, I guess). Hmm, now I've got to think :)

I've been toying with the idea lately and came up with this form for extension methods. Thoughts?

https://structura.quora.com/Extension-Methods-in-JavaScript

TL;DR:

var _capitalize = Impulse.extend(String, _capitalize, function() {
  return this[0].toUpperCase() + this.slice(1);
});

console.log(_capitalize.apply("foo"));

It takes advantage of hoisting and passing the previous method when defining a new one. Applying the method works just like regular functions, but it looks at the type of 'this' and dispatches accordingly. I know there are libraries for multi-methods and stuff, but I wanted to keep it simple.

I use an underscore to not pollute the namespace. lisp-1 vs lisp-2 namespacing debate :)

@RyanCavanaugh

This comment has been minimized.

Copy link
Member

RyanCavanaugh commented Apr 5, 2017

@mikeaustin it's fine for primitives and classes, but with an erased type system there's no prototype to key off of. It's also not really an "extension method" in the sense of something that appears as a new method of a given type.

@series0ne

This comment has been minimized.

Copy link

series0ne commented Apr 5, 2017

I may have mentioned this before on another thread but I think this problem can only be solved with the introduction of a new operator

Calling an existing method on a type

obj.foo();

Calling an extension method on a type

obj::foo();

The latter would translate to...

Extensions.foo(obj);

This allows a distinct differentiation between prototype bound method and extension method calls, without any ambiguity over which is being called in a particular context.

I know there is an ESNext proposal for a bind operator :: so not sure how this would impact this suggestion...would always go with C++'s -> operator if they conflict

@mikeaustin

This comment has been minimized.

Copy link

mikeaustin commented Apr 6, 2017

Hmm, the universal access principle comes to mind when I see two different ways to call a method. Isn't the purpose of extension methods to hide the implementation?

As to Ryan's message, "extension method" isn't really a formal term outside of C# (I think?) I guess I could call what I'm describing as "method shadowing", which is exactly what it's doing.

@aluanhaddad

This comment has been minimized.

Copy link
Contributor

aluanhaddad commented Apr 6, 2017

@mikeaustin Scala implicits are sometimes colloquy referred to as extension methods.
I think the term is fairly well understood.

I agree that using a different token breaks the abstraction.

In spite of the semantics being static in C# the syntactic attraction is precisely the point.

One of the problems I have with the :: is that it doesn't offer any syntactic abstraction over any existing notation.

I feel like it fails at its purpose, which I think is at least in part the very worthy goal of making own versus ambient desirably indistinct, before it even starts.

It would be great to have extension methods but I can't think of a reasonable way.

@mikeaustin

This comment has been minimized.

Copy link

mikeaustin commented Apr 6, 2017

@aluanhaddad Ahh yes, Scala implicits. Ok, I'll refer to the other way round as "lexically scoped methods". I've been playing a bit more with seamless extension methods in a language I'm prototyping. I've found this works for calling the object method before any extension methods:

(10).toString ? (10).toString() : _toString.apply(10)

And the other way around, lexically scoped methods with fallback to object methods:

typeof _toString === "undefined" ? (10).toString() : _toString.apply(10)

The language does not do static type-checking, so a TypeScript version would probably need a different approach. The above can be / is slow since it has to do that at each call site. But like I said, I'm just prototyping and getting a sense of how it could work.

@aluanhaddad

This comment has been minimized.

Copy link
Contributor

aluanhaddad commented Apr 6, 2017

In both C# and Scala an instance method will always win out in terms of overload resolution so I think that's a good approach.

The problem with applying that approach in TypeScript is not static checking but rather that it is impossible to do it without resorting to type directed emit, which is not an option.

@mikeaustin

This comment has been minimized.

Copy link

mikeaustin commented Apr 7, 2017

I see the type-directed-emit topic comes up with async/awat, but other than that is it not used anywhere else? Sorry, I don't mean to keep this topic alive forever :)

Side note -- taking your extension methods comments to heart, I've come up with this for my language:

(foo.toString || _toString).apply(foo, []); // Syntax emmited for: foo.toString()

Again, it's not going to be fast, but at least it's short.

@aluanhaddad

This comment has been minimized.

Copy link
Contributor

aluanhaddad commented Apr 7, 2017

@mikeaustin I like it, that's a very clean emit, I'm curious to see how your language progresses.

With respect to type directed in async methods, IIRC it only rears its ugly head when you don't use type inference for return types and don't install a global Promise polyfill, as it only considers the type when provided, defaulting back to the global Promise.

I couldn't care less about the extra methods available on Bluebird Promises and I never specify the return type of async methods.

So I believe the issue is easily dodged, but I may be wrong and it may be affecting me in some other way that I'm not aware of.

@mikeaustin

This comment has been minimized.

Copy link

mikeaustin commented Apr 19, 2017

Sorry for spamming the TypeScript list, but I made a slight change in my extension methods to support traits/mixins that might be interesting. When extension methods were in local scope, there was no good way (aka not using eval()) to dynamically add methods.

So, I encapsulated them in a _methods lexically scoped object:

var Iterable = new Trait({
  // iterator() can be an extension method or a plain old function
  map: (iterator) => (func) => { var iter = iterator.apply(this); ... }
});

var _methods = Extension.extend(_methods, String, {
  iterator: () => { return new Iterator(this); }
});

// Bind all the trait methods and pass them to extend String
var _methods = Extension.extend(_methods, String, Iterable.bind(_methods.iterator));

_methods.map.apply("abc", [c => c.charCodeAt(0)]); // [97, 98, 99]

I can probably simplify extend() a bit more. The language this is targetting: Impulse-JS.

@atrauzzi

This comment has been minimized.

Copy link

atrauzzi commented May 10, 2017

With the amount of library authoring going on and what seems like increased adoption of TS, now seems like a great time to dust this idea off and see if it's worth dedicating resources to!

Again, biggest thing is for libraries. I already find myself very badly wishing for some kind of idiomatic way to sprinkle some extensions around.

✏️

@RyanCavanaugh

This comment has been minimized.

Copy link
Member

RyanCavanaugh commented May 10, 2017

Seems people aren't reading the thread.

@Microsoft Microsoft locked and limited conversation to collaborators May 10, 2017

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.