ceylon / ceylon-spec

DEPRECATED
Apache License 2.0
108 stars 34 forks source link

extensions #682

Open gavinking opened 11 years ago

gavinking commented 11 years ago

We've often discussed supported extension methods / attributes, but this was never particularly a priority. But now #680 essentially turns every member into a toplevel function with a compound name, letting you call an attribute or method like this:

Person.name(person)
Message.send(msg)(topic)

it feels a little incomplete to not also allow the converse, and let you call a toplevel function as if it were a member, for example:

{Float+} numbers = ... ;
value max = numbers.max1;

Where max1() is defined like this:

T max1<T>({T+} elements) given T satisfies Comparable<T> { .... }

You could define an extension method like this:

T max2<T>({T+} elements)(Comparison compare(T x, Ty)) { .... }

Letting you write:

{Float+} numbers = ... ;
value max = numbers.max2((Float x, Float y) => x<=>y);

There is, however, one major objection to this idea: it introduces the possibility of an ambiguity that we in other circumstances work very hard to eliminate. In the example above, if we were to add a member called max1 to Iterable, the code would break!

It's hard to know quite how much significance to place on that. The exact same sort of breakage can already happen to a subtype of Iterable in the exact same circumstance. However, most of us are much more used to and comfortable with the idea that subclasses and superclasses are quite coupled together in this sense.

A compromise solution would be to require a special kind of declaration for extensions, to capture the notion of "coupled together" that you already have with subtypes.

That is, we would need to declare the max1() function like this:

T max<T>({T+} this) given T satisfies Comparable<T> { .... }

and max2()() like this:

T max2<T>({T+} this)(Comparison compare(T x, Ty)) { .... }

where the keyword this indicates an extension.

WDYT?

RossTate commented 11 years ago

Another thing to take into consideration is dynamic. C#'s dynamic is known to have rather crappy compatibility with extension methods.

So here's a suggestion. How about have some separate syntax, say .., for extension-method invocation. All it does is look for visible global methods with that name and at least one parameter. If there are multiple whose parameter types' are not disjoint, then it fails to type check. Otherwise, it binds to the global method that matches the type of the object being invoked upon. Note that this would be compatible with dynamic typing, and there wouldn't be any ambiguity in the case where the object turns out to have a method of that name.

We may want to do a slight variation of this requiring extension methods to be declared as extension methods. The reason is that this will reduce the chance of accidental overlapping of extension-method names, and it may help IDEs identify/track matching extension methods more efficiently.

Thoughts?

gavinking commented 11 years ago

So here's a suggestion. How about have some separate syntax

If it's a separate syntax, it's completely useless. The only reason to have such a thing is to make it look just like a normal method call.

RossTate commented 11 years ago

I think you mean to say that a reason to have the same syntax is so that people writing code (without the help of an IDE) do not have to remember whether an operation is an actual method on the object or an extension method. Note that this makes it harder to both read and compile the code since now there are two places a method can come from. Furthermore it can lead to ambiguities and misunderstandings when the method is actually defined in both ways.

Another reason for extension methods that still applies to having different syntax is that the order is still consistent with typical OO programs (i.e. primary object followed by operation followed by operands). Another is that the IDE can use the type of the context object to offer suggestions. In fact, this suggests that the .. might be a good syntax since after the programmer types one . the IDE can make extension-method suggestions without having to change what has already been typed (though I imagine that really isn't that big a problem). From my experience with C#'s extension methods, these are the main advantages of extension methods.

FroMage commented 11 years ago

So what exactly is the problem with just treating every toplevel with a compatible first parameter as an extension method? Resolution order would be first member, then imported toplevels.

Personally I never found the need for extension methods, outside of languages where they intend to retrofit an existing SDK with methods they couldn't add otherwise because they don't "own" the SDK in question.

gavinking commented 11 years ago

Resolution order would be first member, then imported toplevels.

Right, so your code would break when the type you're calling adds a new method.

Personally I never found the need for extension methods

I have needed them. Though the main reason I needed them was to add concrete methods to interfaces. Since interfaces in Ceylon can have concrete members, this isn't really something I feel I will really need in Ceylon.

quintesse commented 11 years ago

Well I imagine it can be useful if you want to add functionality to an existing library outside your control, adding features that seemingly are part of the original design. It's a bit like the classes Arrays and Collections in the Java SDK, they really should be part of the types themselves... but on the other hand it might bloat the API with a large number of seldom used methods. Extension methods might help out here by giving the ease-of-use of a member method without needing to bloat the API with every conceivable method.

gavinking commented 11 years ago

@quintesse Except that you still need to import the thing explicitly, so it's not really as easy to use as a real member of the type. (And if the IDE is going to add the import automatically, it's still going to appear as "bloat" in the proposals list.)

quintesse commented 11 years ago

Sure, but that seems like a really small price to pay, because you decided to import it, so I guess you knew what you were doing. And as long as you don't do an ... import the list would not be that bloated, right?

quintesse commented 11 years ago

So personally I think we won't be using them much in the SDK, at least for now, because if it was interesting enough to put it in the SDK it was probably interesting enough to make it a member or toplevel. But for the typical extension library where somebody adds a bunch of methods dedicated to a very specific goal I could see extension methods to be somewhat useful. Would it add anything you can't easily do in other way? No. The only thing you would gain is a syntax that is slightly more pleasant to look at. So if it's worth it I don't know, but I like it.

gavinking commented 11 years ago

I've concluded that we probably won't need this feature, unless users beg for it.

RossTate commented 11 years ago

Here are some things that don't fit into your reasoning that you'd just stick it in the interface if it were really that useful:

Element[] sort<Element>({Element*} elements) given Element satisfies Comparable<Element> {...}
Element sum<Element>({Element+} numbers) given Element satisfies Numeric<Element> {...}
Integer sum({Element*} integers) {...}

Actually, I just realized these and many others are already present in ceylon.language. With extension methods, I would just type . and then the IDE would let me know there's already a .sort method, in case I didn't realize it had already been written - the IDE could even automatically import the method for me. Without extension methods, I would notice its absence and then be left looking through libraries to see if someone had already written it.

gavinking commented 11 years ago

@RossTate, sure, which is why I used the example of max() in the issue description.

gavinking commented 11 years ago

P.S. There's no real reason why the IDE can't propose toplevel functions with one parameter in the proposals list. If I type numbers.ma, autocompletion could always fill it in as max(numbers).

RossTate commented 11 years ago

True. So if y'all are willing to do that, then I guess the issue comes down to syntax. I personally like the primary-object-then-operation-then-operands syntax, but I'll let y'all be the judges on that.

drochetti commented 11 years ago

I think extension methods contribute to the expressiveness of the language, helping frameworks to express themselves better. For example:

Size fontSize = 14.px; // over px(14)
value count = users.where((User user) => user.age > 18).groupBy(`User.lastName`).count()

But I agree that in most cases concrete Interface members or toplevel functions can do the job just fine, but the gains in syntax and possibility to easily develop some DSL is a nice plus IMHO. In a static language as Ceylon it's easier not to abuse them, since you got to explicit import them and the IDE will help you, right?

gavinking commented 11 years ago

@drochetti Sure, and the truth is that this feature was always a favorite of mine until fairly recently. But in pursuit of parsimony, I think it's very reasonable to leave it out of 1.0 and add it if and when we discover a burning need.

drochetti commented 11 years ago

@gavinking couldn't agree more

oehme commented 11 years ago

Just a simple example to whet your appetite: With extension methods I can have a testing framework add an assertion method called shouldBe that takes any object. Together with the infix syntax it reads like this:

max(1,2,3) shouldBe 3;

Much clearer than

assertEquals(3, max(1,2,3));
oplohmann commented 10 years ago

I hope that extension methods make it into Ceylon one or the other way. Reason being is that all the static utility methods in Apache Commons are a bit of a nuisance. Instead of StringUtils.isBlank or something it would be more object-oriented and cleaner to say aString.isBlank().

gavinking commented 10 years ago

The point is that in Ceylon you would type:

blank(aString)

Like I tried to say above, the problem with this in Java is that if I start typing "isBl", the stupid IDE doesn't automatically discover and propose StringUtils.isBlank() and automatically import it for me.

I used to never understand why this was, but now, after dealing with performance in Ceylon IDE I think perhaps I do understand it: I speculate that it's a performance-related limitation.

On Mon, Dec 9, 2013 at 1:05 PM, Oliver Plohmann notifications@github.comwrote:

I hope that extension methods make it into Ceylon one or the other way. Reason being is that all the static utility methods in Apache Commons are a bit of a nuisance. Instead of StringUtils.isBlank or something it would be more object-oriented and cleaner to say aString.isBlank().

— Reply to this email directly or view it on GitHubhttps://github.com/ceylon/ceylon-spec/issues/682#issuecomment-30126483 .

Gavin King gavin@ceylon-lang.org http://profiles.google.com/gavin.king http://ceylon-lang.org http://hibernate.org http://seamframework.org