microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.33k stars 12.54k forks source link

Suggestion: Extension methods #9

Closed RyanCavanaugh closed 8 years ago

RyanCavanaugh commented 10 years ago

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
RyanCavanaugh commented 8 years ago

@alonbardavid see https://github.com/Microsoft/TypeScript/issues/9#issuecomment-74302592 for why call site rewriting really, truly, really just does not work.

Naharie commented 8 years ago

@RyanCavanaugh To take your example code from https://github.com/Microsoft/TypeScript/issues/9#issuecomment-74302592 I don't entirely see why some amount of call site rewriting can't work at all. For instance could we do something like:

// Setup
class Rectangle {
  constructor(public width: number, public height: number) {}
}
// Do not be distracted by exact syntax here
extension function getArea(this s: Rectangle ) {
  return s.width * s.height;
}
// end setup

// Challenge course begins
interface HasArea {
  getArea(): number;
}

function fn1(x: HasArea) {
  console.log(x.getArea());
}
fn1(new Rectangle(10, 10));

var a: any = Math.random() > 0.5 ? new Rectangle(3, 3) : { getArea: function() { return 3; } };
console.log(a.getArea());

var s = new Rectangle(5, 5);
var f = s.getArea.bind(s);
console.log(f);

class Square {
  constructor(public size: number) { }
  getArea() { return this.size * this.size; }
}
var box: Square|Rectangle = /* ... anything ...*/
console.log(box.getArea());

box.getArea.call(box);

var boxes: HasArea[] = [new Square(5), new Rectangle(10, 10)];
boxes.forEach(b => {
  console.log(b.getArea());
});

First gets compiled down to typescript like:

// Setup
class Rectangle {
  constructor(public width: number, public height: number) {}
}

class __ExtendRectangleBacking
{
    constructor (public Instance: Rectangle)
    {
        this.getArea = __ExtendRectangleBacking.prototype.getArea.bind (this);
    }

    getArea ()
    {
        debugger;
        if (this.Instance["getArea"] !== undefined)
        {
            return (this.Instance["getArea"].apply (this.Instance, Array.prototype.slice.call (arguments)));
        }

        var s = this.Instance;
        return (s.width * s.height);
    }
}

var __ExtendRectangle = function (Target: Rectangle)
{
    return (new __ExtendRectangleBacking (Target));
};
// end setup

// Challenge course begins
interface HasArea {
  getArea(): number;
}

function fn1(x: HasArea)
{
  console.log(x.getArea());
}

fn1(__ExtendRectangle (new Rectangle(10, 10)));

var a: any = Math.random() > 0.5 ? __ExtendRectangle (new Rectangle(3, 3)) : { getArea: function() { return 3; } };
console.log(a.getArea());

var s = new Rectangle(5, 5);
var f = __ExtendRectangle (s).getArea.bind(s);
console.log(f);

class Square {
  constructor(public size: number) { }
  getArea() { return this.size * this.size; }
}

// Worst case:
var box: Square | Rectangle = new Square (137);

console.log(__ExtendRectangle (<Rectangle>/*Probably*/box).getArea());

__ExtendRectangle (<Rectangle>/*Probably*/box).getArea.call(box);

var boxes: (HasArea | Rectangle) [] = [new Square(5), new Rectangle(10, 10)];

boxes.forEach(b => {
  console.log(__ExtendRectangle (<Rectangle>/*Probably*/b).getArea());
});

And then emited as javascript like:

// Setup
var Rectangle = (function () {
    function Rectangle(width, height) {
        this.width = width;
        this.height = height;
    }
    return Rectangle;
}());
var __ExtendRectangleBacking = (function () {
    function __ExtendRectangleBacking(Instance) {
        this.Instance = Instance;
        this.getArea = __ExtendRectangleBacking.prototype.getArea.bind(this);
    }
    __ExtendRectangleBacking.prototype.getArea = function () {
        debugger;
        if (this.Instance["getArea"] !== undefined) {
            return (this.Instance["getArea"].apply(this.Instance, Array.prototype.slice.call(arguments)));
        }
        var s = this.Instance;
        return (s.width * s.height);
    };
    return __ExtendRectangleBacking;
}());
var __ExtendRectangle = function (Target) {
    return (new __ExtendRectangleBacking(Target));
};
function fn1(x) {
    console.log(x.getArea());
}
fn1(__ExtendRectangle(new Rectangle(10, 10)));
var a = Math.random() > 0.5 ? __ExtendRectangle(new Rectangle(3, 3)) : { getArea: function () { return 3; } };
console.log(a.getArea());
var s = new Rectangle(5, 5);
var f = __ExtendRectangle(s).getArea.bind(s);
console.log(f);
var Square = (function () {
    function Square(size) {
        this.size = size;
    }
    Square.prototype.getArea = function () { return this.size * this.size; };
    return Square;
}());
// Worst case:
var box = new Square(137);
console.log(__ExtendRectangle(box).getArea());
__ExtendRectangle(box).getArea.call(box);
var boxes = [new Square(5), new Rectangle(10, 10)];
boxes.forEach(function (b) {
    console.log(__ExtendRectangle(b).getArea());
});

If the extension method returns s than accessing width gets converted to:

box.SomeMethodThatReturnsS ().width

Gets converted to:

__ExtendRectangle(box).SomeMethodThatReturnsS ().Instance.width

Any thoughts?

RyanCavanaugh commented 8 years ago

You've "cheated" here

var a: any = Math.random() > 0.5 ? new Rectangle(3, 3) : { getArea: function() { return 3; } };
console.log(a.getArea());

The declared type of a is any. There's no justification to wrap the call to new Rectangle in a __ExtendRectangle call here but not wrap the same call on the line var s = new Rectangle(5, 5);. They're indistinguishable from the compiler's perspective.

The GC implications of this are terrifying - you're causing something like 5 extra allocations per function call even in places where 0 should be needed.

Naharie commented 8 years ago

Okay. On point number one or "cheating" just adjust it so that items only get wrapped when the extension method gets called. I am guessing something like this:

// Setup
class Rectangle {
  constructor(public width: number, public height: number) {}
}

class __ExtendRectangleBacking
{
    constructor (public Instance: Rectangle)
    {
        this.getArea = __ExtendRectangleBacking.prototype.getArea.bind (this);
    }

    getArea ()
    {
        if (this.Instance["getArea"] !== undefined)
        {
            return (this.Instance["getArea"].apply (this.Instance, Array.prototype.slice.call (arguments)));
        }

        var s = this.Instance;
        return (s.width * s.height);
    }
}

// Challenge course begins
interface HasArea {
  getArea(): number;
}

function fn1(x: HasArea)
{
  console.log(x.getArea());
}

fn1(new Rectangle(10, 10));

/* Okay. I admit I don't see how to get around this one: */
var a: any = Math.random() > 0.5 ? new Rectangle(3, 3) : { getArea: function() { return 3; } };
console.log(a.getArea());

var s = new Rectangle(5, 5);
var f = __ExtendRectangle (s).getArea.bind(s);
console.log(f);

class Square {
  constructor(public size: number) { }
  getArea() { return this.size * this.size; }
}

// Worst case:
var box: Square | Rectangle = new Square (137);

console.log(__ExtendRectangle (<Rectangle>/*Probably*/box).getArea());

__ExtendRectangle (<Rectangle>/*Probably*/box).getArea.call(box);

var boxes: (HasArea | Rectangle) [] = [new Square(5), new Rectangle(10, 10)];

boxes.forEach(b => {
  console.log(__ExtendRectangle (<Rectangle>/*Probably*/b).getArea());
});

Thanks for pointing that out. Ahh well. I was just hoping that extension methods might be possible in TypeScript and If possible suggest a way to do it.

shmuelie commented 8 years ago

I think for those that really want extension methods it could be done as an add-in or a preprocessor

Naharie commented 8 years ago

@SamuelEnglard

Do any such things already exist?

shmuelie commented 8 years ago

@KingDavid12 in theory #6508 could eventually allows this. In the short term the TS language service could be used to process the syntax tree and do the very transformation suggested above in a step before the compiler runs. If such a system currently exists I don't know, but I vaguely remember people talking about such things in other issues.

RyanCavanaugh commented 8 years ago

I mean, if it could be accomplished correctly through a syntactic transform, we'd do it. The problem is that it can't.

MrMatthewLayton commented 8 years ago

Based on this article, [(http://perfectionkills.com/extending-native-builtins/)] extension methods/(properties?) should be syntactic sugar for a "static" object, with the same semantics as C#. This ensures that extensions follow the SOLID rules and don't allow existing objects to be modified

Example

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

var myString: string = "Hello World";
var result: boolean = myString.isUpper();

Compiled

var StringExtensions = {
    isUpper: function(value) {
        ...logic;
    }
}

var myString = "Hello World";
var result = StringExtensions.isUpper(myString);

Considerations

RyanCavanaugh commented 8 years ago

@series0ne your comment suggests you haven't read the thread. This has been brought up and explained why it doesn't work several times

electricessence commented 8 years ago

If you want to see the closest real thing in TS/JS of extensions, look at RxJS. They combine extending with granular importing of functions but it's obviously very controlled and wouldn't work automatically for say ... an interface.

JHawkley commented 8 years ago

So, I'm honestly really surprised that this has been talked to death and nothing has been worked out yet. I'm also surprised this was labeled 'out of scope'. That is very short-sighted, especially since extensions belong in TypeScript more than they belong in C# for all the same reasons that TypeScript decided to use a structural type-system in the first place.

This is gonna be a long one, and I apologize for that, but I think it has to be long to set up the case properly. I can see a lot of confusion as to the "what" and "why" for extensions.

TypeScript and Structural Typing

If I'm going to make a case for extensions, I need to make sure everyone knows what problem they are trying to solve. Yep, they solve a problem that exists in TypeScript today; they're not just a "nice thing to have". They facilitate a kind of programming that can be extremely powerful and useful. I think the best way to do this is to teach everyone the problem that structural typing solved in TypeScript's youth and how that leads to extensions.

When the powers that be started to work on TypeScript, they almost immediately ran into a problem: JavaScript is very promiscuous with the structure of its objects. In particular, two objects in JavaScript can have entirely different prototypes, but still have the same properties and much of the same behavior. If these were to bounce their way into TypeScript code through the same entry-point, it would need the equipment to deal with them.

Let's take a look at a very simple kind of structure, the 2D vector: { x: number, y: number } Just how many ways can you think of obtaining such a structure in TypeScript? Here are few!

// Importing one from a third-party library.
import VectorPro from 'lib/vector-pro';

// Making your own vector class.
class MyVector {
  constructor(public x: number, public y: number) { }
  /* ... other stuff ... */
}

// Coming in from your library's public API.
export function gimmieVectorsNomNom(v: { x: number, y: number }) { /* do noms */ }

// A raw JavaScript object.
let v1 = { x: 0, y: 0 };

Now, imagine writing a library where all those types can find their way into your code. TypeScript solved this problem by allowing you to wrangle them all into an interface based on their common structure.

interface VectorLike {
  x: number;
  y: number;
}

But you have a new problem. While you can handle the structure of a vector, you can't easily attach behavior to it. But you are a clever object-oriented programmer. Wrappers are a perfect solution!

class VectorWrapper<T extends VectorLike> implements VectorLike {
  get x(): number { return this.source.x; }
  set x(v: number) { this.source.x = v; }
  get y(): number { return this.source.y; }
  set y(v: number) { this.source.y = v; }

  constructor(public source: T) {}

  length(): number { /* length stuff */ }
  add(other: { x: number, y: number }): this { /* add stuff, return this */ }
  scale(scalar: number): this { /* scaling stuff, return this */ }
}

All done! Except you're not. If you're wrapping VectorLike objects into this thing, you now have a new problem. You have to begin unwrapping them whenever you want to send them out of your API, or you can start handing out VectorWrapper objects, but you have to start checking to see if incoming VectorLike objects are already instances of VectorWrapper so you don't wrap them twice! Hmm... Not so perfect, after all.

This is when you start thinking arrogantly and make the mandate, "in order to use my library, you WILL use MY library's vector class!" All this does is shift the complexity of juggling these types to your library's users. Plus, it adds yet another vector class floating around in the JavaScript run-time. No one is happy with this.

"But, wait!" Some clever programmer interjects, "you're going about this all wrong! Here, look!"

// Just make all your vector math into functions instead!  =)
function length(op: VectorLike): number { /* length stuff */ }
function add(op1: VectorLike, op2: VectorLike): VectorLike { /* add things, return op1 */ }
function scale(op: VectorLike, scalar: number): VectorLike { /* scale stuff, return op */ }

Wow, look at that! Behavior that works with VectorLike and doesn't need any additional abstraction! What this clever programmer stumbled upon is something called procedural programming. But you might be seeing another problem:

import { add, scale } from 'vector-math';

let v1 = { x: 2, y: 4 };
let v2 = { x: 6, y: 8 };
let v3 = { x: 10, y: 12 };
let acc = { x: 0, y: 0 };

// Get the average of three vectors.
let avg = scale(add(add(add(acc, v1), v2), v3), 1 / 3);

That's quite the mess! Not to mention you have to start importing these vector math procedures where ever you want to use them in your code, polluting your scopes with functions floating all over the place. Unfortunately, in this case our behavior is too far separated from the structures they're made to operate on. Its uncomfortable and unwieldy, and why we moved away from it long ago.

So, while TypeScript solves the problem of structure with interfaces, it is far from graceful when dealing with the behavior of these structures.

But wait a minute! Maybe our procedural programming friend is on to something. The first parameter of all those vector math functions all share the same structure...

Extensions are Procedural

If you have a look at the Wikipedia page on Extension Methods, you'll see a quote right at the top by the principal developer of C#'s compiler: "Extension methods certainly are not object-oriented."

And he is right, and you can see this in the way C#'s extension methods are declared. Let's do that with our vector math procedures for comparison, real quick.

public static class VectorMathExt {
  public static Double Length(this IVector op1, IVector op2) { /* length stuff */ }
  public static IVector Add(this IVector op1, IVector op2) { /* add stuff, return op1 */ }
  public static IVector Scale(this IVector op1, double scalar) { /* scale stuff, return op1 */ }
}

Now, with these extensions in scope, where ever we have an IVector object, we can call these procedural functions. As extensions, the compiler marries this behavior to any IVector making it so we can call vec.Add(...) instead of having to use VectorMathExt.Add(vec, ...). Since this is all handled for us by the compiler, we also only have to do a single import statement, and since they're namespaced into VectorMathExt, the scope pollution is minimal. But, once they are in-scope, the compiler handles the rest, replacing our nice dot-operator calls to the extension methods with the more unwieldy static class calls.

You might have noticed a while ago I keep bolding a couple of words when they're near each other: structure and behavior. That's because, deep down, this is how programs are divided up. You cannot have one without the other and still have a working program. There are many ideas on how these two concepts should inter-relate. Object-oriented programming has the structure owning the behavior: the familiar class. Functional programming has the behavior owning the structure: data lives in the stack (as much as possible).

In between, you have procedural programming, where structure and behavior do not own each other; they stay separate. Instead, structure and behavior infer each other. A procedural programmer's job is to find structures and behaviors that agree with each other and use them together, but, as we saw, this is very tedious. Now, if there were a way to map procedural functions on to compatible data structures automatically, this would be a great relief for procedural programming in JavaScript.

Basically, extensions take procedural programming and give it that object-oriented flavor we crave, and it does all this without destructively changing the underlying types. That C# IVector interface is the same as our TypeScript VectorLike interface, just an x property and a y property, with or without extension methods.

But many of you already know how extensions work. It's the fact that this is compiler magic and these extension methods cannot be laid upon the JavaScript world neatly. These extension methods vanish when they're not used in a method call! They won't act like they have these behaviors if they're passed into different scopes that lack these extensions! It would be weird!

It's Already Weird, Dude

The idea of call-site rewriting seems strange to people coming out of JavaScript, and that's largely because they're coming from an interpreted language; JavaScript doesn't have a compiler. To a JavaScript developer, there exists only 'run-time' and anything else is sorcery and witchcraft.

However, just by virtue of using TypeScript, you must deal with 'compile-time' abstractions. I would bet that everyone here stumbled on someVariable instanceof MyInterface at one point or another and thought for a moment that TypeScript's compiler had a bug in it. But you figured it out; you understand now that 'types' are a compile-time thing and 'values' are a run-time thing, and there is a division between them.

And that is what people will learn to understand with extensions. Extensions are another one of those compile-time abstractions that don't exist in the same way in run-time.

But Only if You're Ready

Keeping that in mind, it would be a terrible idea to just start piling extensions onto everything. C# made the smart choice of making extensions opt-in; in order to use them, you must bring them into scope with the using directive. Other languages which have features similar to extensions, like Rust's traits, are also opt-in and strict about it. Being opt-in, we also get the advantage that adding extensions to the compiler will not break existing code.

It should be little different for TypeScript, especially if it will take some time for programmers to adapt to the concept.

The Missing Half

TypeScript provides a means of conjuring structure to help cope with that structural type-system, the interface and type keywords. Unfortunately, it has no simple means of joining behavior to these structures. This is what extensions provide and the problem they solve in TypeScript.

Ponder the following for a moment: Interfaces describe the structure of a type. Extensions describe the behavior of a structure.

It's starting to sound a lot like extensions are the missing compliment to interfaces. Interfaces cannot have behavior; they can define space for behavior, but that isn't the same thing as having it. Where-as extensions cannot have structure, but they can define what particular structures are capable of doing. It's getting harder and harder to see how you can have one of the features but not the other.

Conclusion

As I see it, we have a feature that is only half-implemented in TypeScript. We got interfaces that poof structure into existence; where is the thing that poofs behavior into existence to go with it? It's hard to argue that extensions do not strongly compliment TypeScript's structural type-system. It would make coping with many of the challenges that exist in the domain of JavaScript so much easier.

That being said, I have been working on a complete proposal, but I think I'll wait to see if the mood changes, after everyone has digested this, before I finish it.

RyanCavanaugh commented 8 years ago

@JHawkley please don't confuse the issue - it's not that extension methods aren't awesome and desirable. Everyone who uses C# recognizes their value. It's that they cannot[1] (cannot [2]) be added to the language in a way that actually works, for any suitable definition of "works". See https://github.com/Microsoft/TypeScript/issues/9#issuecomment-74302592

[1]: SRSLY CANNOT [2]: can't even

saschanaz commented 8 years ago

@JHawkley If your proposal really fits in a JS world then maybe you can post it on esdiscuss or WICG so that the whole JS community can benefit from it :D

I will definitely be happy to see that happen.

JHawkley commented 8 years ago

@RyanCavanaugh I actually kind of found your challenge course kind of funny, at least how you began it. Thing is: C# can't run that course with its extensions, and it is C# and it does have the CLR.

In C#, having an extension mapped to a type does not actually change the underlying type. If an object didn't match an interface before it had an extension, it still won't after. The compiler would helpfully inform you of the type mismatch. Extensions (at least how C# implements them) do not change types; they only associate free-floating procedures to types, making calling those procedures more convenient.

Fundamentally, your challenge course is confusing two concepts: extensions vs monkey-patches.

The core difference is one virtually extends an object in compile-time and the other ACTUALLY extends it in run-time. Your challenge course is making the assumption that extensions would need to work the same way in both domains, and if they don't, they fail and are therefore unworkable as a feature.

But extensions are not monkey-patches. There are other proposals for this kind of feature elsewhere. Extensions solve a different problem from monkey-patches, and so must be given different treatment and have different expectations.

They do work in TypeScript, just not in the same way as a monkey-patch. Also, the extensions that TypeScript would produce would also work fine in JavaScript; the popular library UnderscoreJS is an example of what a TypeScript library built on extensions would compile into. Perfectly usable! (In fact, a prime candidate for having its definition files re-written to support extensions.)

Also, as an aside: the only language I currently know of that could actually run your challenge course is Scala, with its implicits. Those are, uhh... really wild, though. ScalaJS exists and it does manage to bring the feature to JavaScript, but I am certain that the structures it compiles into are probably unsuitable for use in bare JavaScript, though.

JHawkley commented 8 years ago

Actually, @RyanCavanaugh, I think I see what you were saying. Like a lot of people, I got caught up in the whole idea of C#-style extension methods when your suggestion clearly states "adding members to an existing type's prototype"; that's obviously a case for run-time monkey-patching. And that definitely wouldn't work, especially now that we have union and intersection types to deal with. Although compile-time abstractions were discussed thoroughly, it wasn't really what the suggestion was originally about.

But, now I'm not sure what to do. I still think compile-time abstractions for associating behavior to types does work for TypeScript and has great merit for both it and JavaScript. It feels like I should create a new issue (or find an old one) that pertains specifically to compile-time extension methods, but I am pretty certain that, by now, the issue would be immediately marked a duplicate and I'd be sent right back here. I mean, this IS issue number 9. It's an old, old discussion...

But, I've expended considerable energy on this already, and I think I laid the case for compile-time extension methods as well as I could, where it was mostly discussed. At this point, either people understand the difference and see that it works or the idea dies here. I hope it doesn't, but TypeScript will continue to be a great language all the same.

RyanCavanaugh commented 8 years ago

Again, it would be awesome to have that feature. It's super handy. If we could wave a magic wand ✨ and make extension methods happen, we most likely would. There's no question about the desirability of the feature.

The constraint of compiling to simple JS simply makes it impossible to implement. If someone somehow came up with a way to introduce that behavior (in terms of JS emit) while still meeting our other design constraints, I think it'd be something we were really excited about. But the fact that five years later no one has come up with anything, despite everyone wanting it and wishing it existed, strongly implies that it cannot be done.

danfma commented 8 years ago

I really agree with you, @RyanCavanaugh!

saschanaz commented 8 years ago

I think something like binding+pipelining will work without the typing problems because the syntax is explicit.

electricessence commented 8 years ago

@RyanCavanaugh I still don't understand why it can't be simple syntactic sugar. All extensions are simply a static method/function that takes this as the first property. Why can't TSC simply rewrite the function insanity for us?

interface IEnumerable<T> {
   getEnumerator():IEnumerator<T>
}
// Extensions:
export function where<T>(this a:IEnumerable<T>, p:Predicate<T> ):IEnumerable<T>
{ /*... */ }
export function select<T,TResult>(this a:IEnumerable<T>, s:Selector<T,TResult>):IEnumerable<TResult>
{ /*... */ }
import {* as Linq} from "Linq";

function filter<T,TResult>(source:IEnumerable<T>, p:Predicate<T>, s:Selector<T>)
{
    return source.where(p).select(s);
}

... results in:

function filter(source, p, s)
{
    return select(where(source,p),s);
}

As always, I understand that there could be collisions if the existing objects had a .where or .select member. But: 1) In this case, the intention is clear because of the restrictive interface. 2) The compiler should be able to detect the collision and warn for both the cases where the extension itself is breaking the rule, or being broken when applied to the object being extended. 3) Also, the compiler should be able to identify if there is a difference in signatures between the two and even potentially create a virtual overload because of it.

electricessence commented 8 years ago

@RyanCavanaugh And again, I understand that implementing extensions is probably going to break things in JS but it could be done in a restrictive way first pass where everything has to be in order for it to work right.

RyanCavanaugh commented 8 years ago

Here is a non-exhaustive list of problems with this approach, as already described at https://github.com/Microsoft/TypeScript/issues/9#issuecomment-74302592

  1. What happens when an extension method is invoked on an expression of type any ? We'll have no idea which method to rewrite to, let alone whether we should at all. A slight change to a .d.ts file that you don't even own might cause an expression to become any and break your program - super bad. How would you even diagnose this? It just looks like a compiler bug. You pulled some new definition files from DefinitelyTyped and now your program stops working at runtime? No.
  2. What happens if you simply reference an extension method, rather than invoke it? The best-case scenario would be a bind invocation, but even this isn't the same (reference identity stops working, for example). Compound this with the previous bullet point.
  3. What happens if you have a union of two objects and two extension methods extending both types in the same way? Or if you have a union where one type has the method built-in and the other has the method from an extension. By all accounts you would expect to be able to invoke the method, but we'd have to somehow generate code that at runtime would distinguish the two objects and pick which method to invoke. In the general case, this is simply impossible. Compound this with the previous two bullet points.
  4. What happens if you e.g. add foo as an extension method to bar and pass a bar to a function that expects { foo(): string } ? We'd have to disallow this because we can't just go rewrite the inside of the function. But now it's completely opaque why you can invoke a method, but you can't pass it to a function that wants to invoke the method on your behalf. It just looks like a compiler bug.
RyanCavanaugh commented 8 years ago
  1. What about intersection types? What should happen for a type A & B that has an extension method foo for both A and B with the same name, but different implementations for A.foo and B.foo? I guess you just can't invoke it, but that breaks the contract for intersection types -- now A & B is no longer a substitute for A nor B. Technically we would just pick the first method we found and invoke it, but now your program's behavior would be dependent on the compiler's internal ordering of the constituents of intersection types, which is not at all specified (would the type B & A now need to be distinct from A & B ?)
  2. We would be strictly unable to ever change anything in the type system again without fear of breaking someone's program's behavior. Overload resolution, for example -- we really want to tweak the rules here, but now returning an A instead of a B due to resolution changes might result in a different method being invoked at runtime and cause a previously-working program to fail (or worse, corrupt data)
electricessence commented 8 years ago

@RyanCavanaugh all valid points that I'm aware of. Hence why I'm suggesting it may require a heavy amount of restrictions to work. Compile time collision detection would be essential, and not optional.

RyanCavanaugh commented 8 years ago
  1. Basic JS premises like bar["foo"] !== undefined implying "foo" in bar are now broken, unless we would want to somehow rewrite the in operator in certain cases (how??). It's not clear how you could do this in a way that doesn't fundamentally change the runtime behavior of JS in many cases.
  2. Basic JS premises like const x = "foo"; bar[x](); being identical to bar.foo(); are now broken, unless we want to start doing aggressive constant folding, but even that wouldn't be sufficient. It's hard to imagine keeping a straight face on the "all JS is TS" promise here.
RyanCavanaugh commented 8 years ago

Applying an extension to a value that is of any could be warned against or simply ignored.

This is circular logic. How do you know that an invocation of a method of an any expression is an extension method call rather than a normal call? You don't, because it's any.

electricessence commented 8 years ago

Yeah, like # 8 is great example. But I'm suggesting that those are not cases where extensions be valid. It would have to be a function, and it would have to be dot notation.

electricessence commented 8 years ago

You don't, because it's any.

any does throw a wrench in the gears. Maybe the use of any is somehow disallowed in modules that use extensions.

electricessence commented 8 years ago

I guess ultimately, I don't see it as impossible. It's just problematic or difficult to get right.

RyanCavanaugh commented 8 years ago

I mean, it's possible as long as you're willing to accept a feature that is outright dangerous to use, has non-working scenarios that look like compiler bugs, restricts the use of many common JS idioms, requires type-directed emit, doesn't interop with plain JS at all, and prevents the TS type system from ever changing again.

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. :wink:

MrMatthewLayton commented 8 years ago

@RyanCavanaugh could you provide a sort of similarity in terms of how dangerous extension methods are in C# vs TS? - I mean we're all talking about extension methods, essentially because they're in C#, and we'd like to see them in TS...

...brb, need to go rewire my house quickly...I can smell burning gum!

electricessence commented 8 years ago

@RyanCavanaugh at your last comment: I have to respect consistency. So I agree, adding a feature that creates confusion is bad. Thank you for the in-depth reply.

electricessence commented 8 years ago

@series0ne if I may help out here and summarize... Just the notion of any is enough to create a serious issue. There's just too much opportunity for collision. And as I chatted with Ryan about, you could solve these headaches, but it would create different headaches and inconsistencies that make it not worth it.

RyanCavanaugh commented 8 years ago

There's no danger in C# because C# a) resolves all call sites at compile-time, b) uses a nominal type system, c) uses an explicit opt-in mechanism to provide dynamic method calls (in which extension methods simply don't work), and d) doesn't allow method references in typeless contexts. Consider this C# code:

            IEnumerable<int> enumer = null;
            List<int> list = null;
            Func<int> x = enumer.Count;
            int y = list.Count;

The IL for the last two statements there is very different!

We also have to consider code like this

            var z1 = enumer.Count; // Error
            var z2 = list.Select; // Error

C# rightly errors on these, but TS really can't - that's valid JS and we have to emit something.

electricessence commented 8 years ago

@RyanCavanaugh, maybe you've already done so, but I think this topic is worthy of a blog post. :P "Why JS/TS cannot have extensions"

MrMatthewLayton commented 8 years ago

Essentially it seems then, that we will simply have to stick to statically bound method calls in TypeScript, the same way we would have done it with C# before extension methods :(

RyanCavanaugh commented 8 years ago

The bind operator https://github.com/tc39/proposal-bind-operator is very promising and would accomplish nearly all the extension method scenarios quite cleanly. So keep some hope :wink:

MrMatthewLayton commented 8 years ago

😯 @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 isUpperwas 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 commented 8 years ago

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 commented 8 years ago

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:

RyanCavanaugh commented 8 years ago

@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 commented 8 years ago

@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);

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

danfma commented 8 years ago

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 commented 8 years ago

All of the problems raised at https://github.com/Microsoft/TypeScript/issues/9#issuecomment-74302592 and https://github.com/Microsoft/TypeScript/issues/9#issuecomment-263978824 apply

danfma commented 8 years ago

ok, I will check!

danfma commented 8 years ago

@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 commented 7 years ago

👍

co3carbonate commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

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.