Uniform Function Call Syntax or Pipeline operator

124 views
Skip to first unread message

Kasper Peulen

unread,
Nov 15, 2017, 10:43:21 AM11/15/17
to Dart Core Development
What do you guys think of the Uniform Function Call Syntax?

If you don't know what it is, here is a short article:
https://en.wikipedia.org/wiki/Uniform_Function_Call_Syntax

TLDR:

The Uniform Function Call Syntax allows functions to be written using a syntax of methods. So you can write f(x)as x.f() or more generally you can write f(x,y,z,...) as x.f(y,z,...).
It has a couple of benefits:
  • It allows free functions to be chained similarly as methods. It kind of fulfills the role of the pipeline operator, that some languages have. Say that we have the free functions join, absolute, basename, capitalize and camelcase, you can write:
    x.absolute(...).join(...).basename(...).capitalize(...).camelcase(...)
  • You don't need method extensions, with this syntax, you can write any function as a method extension.
  • It can give better "dot-autocomplete" in IDEs, which use type information to show a list of available functions, dependent on the context. When the programmer starts with an argument, the set of potentially applicable functions is greatly narrowed down, aiding discoverability of functions.
I think the pipeline operator, could have similar benefits. I'm not sure what would be a better fit for Dart.

If you don't see which problems this actually solve, I think the path package is a good example. 

In some sense you would want to wrap the path string in a Path class, because of chainability, and it makes it easier to expose your api in that way. On the other hand, pure function actually makes more sense. This is what the path readme says:

Why doesn't this make paths first-class objects?
When you have path objects, then every API that takes a path has to decide if it accepts strings, path objects, or both.
  • Accepting strings is the most convenient, but then it seems weird to have these path objects that aren't actually accepted by anything that needs a path. Once you've created a path, you have to always call .toString() on it before you can do anything useful with it.
  • Requiring objects forces users to wrap path strings in these objects, which is tedious. It also means coupling that API to whatever library defines this path class. If there are multiple "path" libraries that each define their own path types, then any library that works with paths has to pick which one it uses.
  • Taking both means you can't type your API. That defeats the purpose of having a path type: why have a type if your APIs can't annotate that they expect it?

Here is a poll on Dartisan about the pipeline operator: https://plus.google.com/117557057118952472631/posts/CYPc6qoaVvw
Also here is some gist, with an example where the pipeline operator (or UFCS) would make sense: https://gist.github.com/kasperpeulen/fa6811063bd8c8eefab7840c0785250e

Erik Ernst

unread,
Nov 15, 2017, 11:18:17 AM11/15/17
to Kasper Peulen, Dart Core Development
On Wed, Nov 15, 2017 at 4:43 PM, Kasper Peulen <kasper...@gmail.com> wrote:
What do you guys think of the Uniform Function Call Syntax?

If you don't know what it is, here is a short article:
https://en.wikipedia.org/wiki/Uniform_Function_Call_Syntax

TLDR:

The Uniform Function Call Syntax allows functions to be written using a syntax of methods. So you can write f(x)as x.f() or more generally you can write f(x,y,z,...) as x.f(y,z,...).
It has a couple of benefits:
  • It allows free functions to be chained similarly as methods. It kind of fulfills the role of the pipeline operator, that some languages have. Say that we have the free functions join, absolute, basename, capitalize and camelcase, you can write:
    x.absolute(...).join(...).basename(...).capitalize(...).camelcase(...)
  • You don't need method extensions, with this syntax, you can write any function as a method extension.
  • It can give better "dot-autocomplete" in IDEs, which use type information to show a list of available functions, dependent on the context. When the programmer starts with an argument, the set of potentially applicable functions is greatly narrowed down, aiding discoverability of functions.
I think the pipeline operator, could have similar benefits. I'm not sure what would be a better fit for Dart.

The pipeline operator has a property which is helpful in Dart: `a |> foo(b, c)` is statically known to be exactly the same as `foo(a, b, c)`, and `(a, b) |> foo(c)` could also mean `foo(a, b, c)`, etc. (no, they never mention BETA, but they should).
 
Given that Dart allows invocations of methods on receivers of type `dynamic` with no static evidence for the existence of the invoked method, the semantics of the uniform function call syntax could be considered somewhat "magic": If `a` has type `dynamic` and the run time value of `a` turns out to have a `foo` method, then `a.foo(b, c)` would be a regular instance method invocation on `a` with selector `foo` and arguments `b, c`. If `a` does not have a `foo` it would (presumably) be a plain function call, written as `foo(a, b, c)` today, where `foo` is looked up in the enclosing lexical scopes.

If you don't see which problems this actually solve, I think the path package is a good example. 

In some sense you would want to wrap the path string in a Path class, because of chainability, and it makes it easier to expose your api in that way. On the other hand, pure function actually makes more sense. This is what the path readme says:

Why doesn't this make paths first-class objects?
When you have path objects, then every API that takes a path has to decide if it accepts strings, path objects, or both.
  • Accepting strings is the most convenient, but then it seems weird to have these path objects that aren't actually accepted by anything that needs a path. Once you've created a path, you have to always call .toString() on it before you can do anything useful with it.
  • Requiring objects forces users to wrap path strings in these objects, which is tedious. It also means coupling that API to whatever library defines this path class. If there are multiple "path" libraries that each define their own path types, then any library that works with paths has to pick which one it uses.
  • Taking both means you can't type your API. That defeats the purpose of having a path type: why have a type if your APIs can't annotate that they expect it?

Here is a poll on Dartisan about the pipeline operator: https://plus.google.com/117557057118952472631/posts/CYPc6qoaVvw
Also here is some gist, with an example where the pipeline operator (or UFCS) would make sense: https://gist.github.com/kasperpeulen/fa6811063bd8c8eefab7840c0785250e

--
You received this message because you are subscribed to the Google Groups "Dart Core Development" group.
To unsubscribe from this group and stop receiving emails from it, send an email to core-dev+unsubscribe@dartlang.org.



--
Erik Ernst  -  Google Danmark ApS
Skt Petri Passage 5, 2 sal, 1165 København K, Denmark
CVR no. 28866984

Kevin Moore

unread,
Nov 15, 2017, 3:19:49 PM11/15/17
to Erik Ernst, Kasper Peulen, Dart Core Development

Kevin Moore

unread,
Nov 15, 2017, 3:20:21 PM11/15/17
to Erik Ernst, Kasper Peulen, Dart Core Development

Lasse R.H. Nielsen

unread,
Nov 15, 2017, 3:37:32 PM11/15/17
to Kasper Peulen, Dart Core Development
I have some issues with the proposed syntax because it overlaps with existing method call syntax.

If I write `foo.bar()`, the `bar` would now either refer to the `bar` method on the class of `foo`, or to a name in the static scope.
I guess that if there *is* a `bar` method on the class, use that, otherwise do local lexical lookup and see if there is a `bar` method. That is, *resolution* of `bar` depends on the *value* of `foo` (or at least on its static type).
Should resolution also take signatures into account? Like, if `foo` has a `bar(int)` method, and a static `bar(Foo)` function is in scope, will `foo.bar()` then call the static function or always try to call the method with the wrong number of arguments?

As Erik says, for a dynamic call, the resolution needs to be done dynamically (which basically means compiling both and switching at run time depending on the actual run-time type of `foo`). 

Another issue is that I have no way to override the behavior if `foo` has a `bar` method and I want to call a different `bar` function instead. Then I have to go back to `bar(foo)` instead of `foo.bar()` (well, or `var tmp = bar; foo.tmp()`, which is not more readable). This shows that the lookup is fragile - if someone adds a method to `foo`, an existing static call will now change behavior.

It also makes some errors harder to catch. You can do `foo.print()` on any object because `print` accepts Object. If think you have a method named `print`, then you won't get any warnings if it isn't actually there. Globally available methods that accept Object will accept anything.

(And finally, I really don't like that in `foo.bar()`, the `bar` name is actually looked up lexically. That's just too far from how the "." operator usually works. It's too confusing).

Not saying it can't work, but there are some pitfalls to address and avoid.

I would prefer to have something better, perhaps more like Rust's traits, that allows you to effectively extend the object, rather than a lexical-scope based function invocation with a confusing syntax. Or just a separate syntax for doing the invocation, I'm partial to "foo->bar()".

/L

On Wed, Nov 15, 2017 at 4:43 PM, Kasper Peulen <kasper...@gmail.com> wrote:

--
You received this message because you are subscribed to the Google Groups "Dart Core Development" group.
To unsubscribe from this group and stop receiving emails from it, send an email to core-dev+unsubscribe@dartlang.org.



--
Lasse R.H. Nielsen - l...@google.com  
'Faith without judgement merely degrades the spirit divine'
Google Denmark ApS - Frederiksborggade 20B, 1 sal - 1360 København K - Denmark - CVR nr. 28 86 69 84

Bob Nystrom

unread,
Nov 15, 2017, 6:41:17 PM11/15/17
to Lasse R.H. Nielsen, Kasper Peulen, Dart Core Development
On Wed, Nov 15, 2017 at 12:37 PM, 'Lasse R.H. Nielsen' via Dart Core Development <core...@dartlang.org> wrote:
Another issue is that I have no way to override the behavior if `foo` has a `bar` method and I want to call a different `bar` function instead. Then I have to go back to `bar(foo)` instead of `foo.bar()` (well, or `var tmp = bar; foo.tmp()`, which is not more readable). This shows that the lookup is fragile - if someone adds a method to `foo`, an existing static call will now change behavior.

It also makes some errors harder to catch. You can do `foo.print()` on any object because `print` accepts Object. If think you have a method named `print`, then you won't get any warnings if it isn't actually there. Globally available methods that accept Object will accept anything.

Right, I agree with this. I think allowing method-style syntax for any function is a little too loose and magical for my taste.

I'm also hesitant to support multiple distinct syntaxes that do the same thing (i.e. "foo(123)" and "123.foo()"). One of the things users tell us they really appreciate about Dart is that there's generally one best way to do things.

At the same time, I very much would like a way give users the ability to syntactically hang new operations on existing types without needing to modify the type itself. For me, the most appealing way to do that is through something like C# and Kotlin's extension methods.
 
(And finally, I really don't like that in `foo.bar()`, the `bar` name is actually looked up lexically. That's just too far from how the "." operator usually works. It's too confusing).

I would have felt that way too, but my experience with C# is that it ends up not really being an issue.

Consider that already in Dart, "a.b()" could mean:
  • Invoke the top-level function b from the library imported using prefix a.
  • Access the top-level getter b from the library imported using prefix a, which returns a function, and then invoke that function.
  • Invoke the static method b on class a.
  • Invoke the static getter b on class a, which returns a function, and then invoke that function.
  • Invoke the instance method b on the object a.
  • Invoke the instance getter b on class a, which returns a function, and then invoke that function.
Extension methods are pretty different in that the member is no longer scoped to the thing on the left (whatever it is). But in practice, I didn't find it hard to get used to. Once I was used to it, it was a really powerful, expressive tool.

Keep in mind that extension methods don't just let you add new methods to classes, they let you add new methods to types. So you can do things like:
  • Add a method to an enum type.
  • Add a sum() method to Iterable<num>, but not other iterable types.
  • Add a method to an interface.
And, of course, there's the general usefulness of being able to add new domain-specific operations to primitive types. If you're trying to define a DSL, it's important to be able to work with numbers and strings while still having them do things meaningful to your domain.
 
Not saying it can't work, but there are some pitfalls to address and avoid.

Yes, figuring out how it would interact with shadowing, library privacy, polymorphism, dynamic etc. Lots of interesting things to work through.


I would prefer to have something better, perhaps more like Rust's traits, that allows you to effectively extend the object, rather than a lexical-scope based function invocation with a confusing syntax.

I could be wrong, but my understanding is that traits in Rust are more or less lexically scoped similar to extension methods. If the implementation of a trait is in scope, then the methods it defines are now in scope on the type(s) it implements.

But, at a high level, I agree that Rust traits are also a good source of inspiration for us to learn from. The best solution for Dart may take ideas from C#, Kotlin, and Rust and synthesize them in some interesting way.

Or just a separate syntax for doing the invocation, I'm partial to "foo->bar()".

That does side-step a lot of problems, but it's hard to get over the unfamiliarity. It's really hard to beat "." when it comes to palatability.

Cheers!

– bob

Lasse R.H. Nielsen

unread,
Nov 16, 2017, 1:47:34 AM11/16/17
to Bob Nystrom, Kasper Peulen, Dart Core Development
On Thu, Nov 16, 2017 at 12:40 AM, Bob Nystrom <rnys...@google.com> wrote:


On Wed, Nov 15, 2017 at 12:37 PM, 'Lasse R.H. Nielsen' via Dart Core Development <core...@dartlang.org> wrote:
Another issue is that I have no way to override the behavior if `foo` has a `bar` method and I want to call a different `bar` function instead. Then I have to go back to `bar(foo)` instead of `foo.bar()` (well, or `var tmp = bar; foo.tmp()`, which is not more readable). This shows that the lookup is fragile - if someone adds a method to `foo`, an existing static call will now change behavior.

It also makes some errors harder to catch. You can do `foo.print()` on any object because `print` accepts Object. If think you have a method named `print`, then you won't get any warnings if it isn't actually there. Globally available methods that accept Object will accept anything.

Right, I agree with this. I think allowing method-style syntax for any function is a little too loose and magical for my taste.

I'm also hesitant to support multiple distinct syntaxes that do the same thing (i.e. "foo(123)" and "123.foo()"). One of the things users tell us they really appreciate about Dart is that there's generally one best way to do things.

That's why I prefer C# extension methods to this idea - they may be lexically scoped and non-virtual, but they still act only as methods. It's not "any static method in scope will work", it has to be deliberate.
 

At the same time, I very much would like a way give users the ability to syntactically hang new operations on existing types without needing to modify the type itself. For me, the most appealing way to do that is through something like C# and Kotlin's extension methods.

I think Dart severely needs something like that. The Strong Mode type system doesn't allow you to just add a feature to an interface - it's a compile-time error if every implementation of that interface doesn't get an implementation at the same time. That makes it a breaking change to add a new method. Libraries can increment their major version (but few would want to for such a change, that really feels like a minor-version addition), and the SDK is blocked until the next major version.
 
 
(And finally, I really don't like that in `foo.bar()`, the `bar` name is actually looked up lexically. That's just too far from how the "." operator usually works. It's too confusing).

I would have felt that way too, but my experience with C# is that it ends up not really being an issue.

Consider that already in Dart, "a.b()" could mean:
  • Invoke the top-level function b from the library imported using prefix a.
  • Access the top-level getter b from the library imported using prefix a, which returns a function, and then invoke that function.
  • Invoke the static method b on class a.
  • Invoke the static getter b on class a, which returns a function, and then invoke that function.
  • Invoke the instance method b on the object a.
  • Invoke the instance getter b on class a, which returns a function, and then invoke that function.
In every case it is "figure out what a is, then look for b in a". People call the operation "dotting into", it's clearly understood to be a hierarchical operation.
The "unform FCS" breaks that by looking up "a" and then completely ignoring "a" when looking up "b". That's what I don't like - I cannot see on the syntax that the local lexical scope affects "b", so I could decide to add a local variable with the same name and break things. Sure, I'd figure out, but it's still annoying:

   var bar = foo.bar();  // Looks good, won't work.

It also breaks the tear-off syntax, unless "foo.bar" is the same as the curried "(...) => bar(foo, ...)". Which is doable, but then we get into silly cases like:
   () => foo(a, b, c)
being writable as
   c.b.a.foo
I'd rather not enable that kind of behavior. :)

 
Extension methods are pretty different in that the member is no longer scoped to the thing on the left (whatever it is). But in practice, I didn't find it hard to get used to. Once I was used to it, it was a really powerful, expressive tool.

It's declaration isn't scoped to the type, but it's use is. It really is what it says: a scope extension of the class. If I understand traits, they are somewhat similar in that they are type-based extensions, there you just introduce a new type to an existing one (like interface injection) and then extend it in the same declaration.
That works well too.
 

Keep in mind that extension methods don't just let you add new methods to classes, they let you add new methods to types. So you can do things like:
  • Add a method to an enum type.
  • Add a sum() method to Iterable<num>, but not other iterable types.
  • Add a method to an interface.
And, of course, there's the general usefulness of being able to add new domain-specific operations to primitive types. If you're trying to define a DSL, it's important to be able to work with numbers and strings while still having them do things meaningful to your domain.

It's definitely useful. It's all based on (effectively) static methods, so you don't get virtual overrides, but for a DSL that's usually sufficient.

 
 
Not saying it can't work, but there are some pitfalls to address and avoid.

Yes, figuring out how it would interact with shadowing, library privacy, polymorphism, dynamic etc. Lots of interesting things to work through.

My initial impression is that there are other options I'd investigate before the UFCS approach, other options that use "." as well, but only works with delibrately introduced functions, not any static function in scope. Breaking the "a.b looks up b in a" rule is, IMO, too bad for readability. 
Traits or scoped extension methods look much more promising.

I think UFCS works better in a language that is designed around it, and Dart most certainly isn't. We use `.` for too many things already.
 

I would prefer to have something better, perhaps more like Rust's traits, that allows you to effectively extend the object, rather than a lexical-scope based function invocation with a confusing syntax.

I could be wrong, but my understanding is that traits in Rust are more or less lexically scoped similar to extension methods. If the implementation of a trait is in scope, then the methods it defines are now in scope on the type(s) it implements.

That's how I understand it too (from a somewhat cursory reading). 
 
But, at a high level, I agree that Rust traits are also a good source of inspiration for us to learn from. The best solution for Dart may take ideas from C#, Kotlin, and Rust and synthesize them in some interesting way.

Or just a separate syntax for doing the invocation, I'm partial to "foo->bar()".

That does side-step a lot of problems, but it's hard to get over the unfamiliarity. It's really hard to beat "." when it comes to palatability.

True, but by using a different syntax, I hope it's clear that `bar` is looked up differently here than in `foo.bar`. It's harder to mis-read. It addresses my main issue with the syntax while retaining all the functionality. 
Example:
   foo.bar().print();
vs.
   foo.bar()->print();
In the former, the .bar() invokes a method, .print() invokes a static function - or a method, I don't actually know.
In the latter, it's clear that there is a difference, "print" is further away from the object (I actually like that it's a two-character operator). It really says "look over there!" to me. 
Might just be habitual thinking by now, but I think it works :)

/L
 
/L

Bob Nystrom

unread,
Nov 16, 2017, 7:45:11 PM11/16/17
to Lasse R.H. Nielsen, Kasper Peulen, Dart Core Development
On Wed, Nov 15, 2017 at 10:47 PM, 'Lasse R.H. Nielsen' via Dart Core Development <core...@dartlang.org> wrote:


On Thu, Nov 16, 2017 at 12:40 AM, Bob Nystrom <rnys...@google.com> wrote:


On Wed, Nov 15, 2017 at 12:37 PM, 'Lasse R.H. Nielsen' via Dart Core Development <core...@dartlang.org> wrote:
Another issue is that I have no way to override the behavior if `foo` has a `bar` method and I want to call a different `bar` function instead. Then I have to go back to `bar(foo)` instead of `foo.bar()` (well, or `var tmp = bar; foo.tmp()`, which is not more readable). This shows that the lookup is fragile - if someone adds a method to `foo`, an existing static call will now change behavior.

It also makes some errors harder to catch. You can do `foo.print()` on any object because `print` accepts Object. If think you have a method named `print`, then you won't get any warnings if it isn't actually there. Globally available methods that accept Object will accept anything.

Right, I agree with this. I think allowing method-style syntax for any function is a little too loose and magical for my taste.

I'm also hesitant to support multiple distinct syntaxes that do the same thing (i.e. "foo(123)" and "123.foo()"). One of the things users tell us they really appreciate about Dart is that there's generally one best way to do things.

That's why I prefer C# extension methods to this idea - they may be lexically scoped and non-virtual, but they still act only as methods. It's not "any static method in scope will work", it has to be deliberate.

+1 me too.

It also means names in the user's local lexical scope won't shadow methods, nor do extension methods shadow lexically scoped names. There is instead a separate "extension method" scope that only gets involved after the ".".
 
 

At the same time, I very much would like a way give users the ability to syntactically hang new operations on existing types without needing to modify the type itself. For me, the most appealing way to do that is through something like C# and Kotlin's extension methods.

I think Dart severely needs something like that. The Strong Mode type system doesn't allow you to just add a feature to an interface - it's a compile-time error if every implementation of that interface doesn't get an implementation at the same time. That makes it a breaking change to add a new method. Libraries can increment their major version (but few would want to for such a change, that really feels like a minor-version addition), and the SDK is blocked until the next major version.

This is less of an issue in third party packages where pub's versioning system helps out. (You are one of a small number of people on Earth who feel the pain of changing "dart:" libraries directly.) It is an issue though. And, certainly, not being able to add new capabilities to our core libraries easily hurts all users because it slows down the new features they would like to use.
 
 
 
(And finally, I really don't like that in `foo.bar()`, the `bar` name is actually looked up lexically. That's just too far from how the "." operator usually works. It's too confusing).

I would have felt that way too, but my experience with C# is that it ends up not really being an issue.

Consider that already in Dart, "a.b()" could mean:
  • Invoke the top-level function b from the library imported using prefix a.
  • Access the top-level getter b from the library imported using prefix a, which returns a function, and then invoke that function.
  • Invoke the static method b on class a.
  • Invoke the static getter b on class a, which returns a function, and then invoke that function.
  • Invoke the instance method b on the object a.
  • Invoke the instance getter b on class a, which returns a function, and then invoke that function.
In every case it is "figure out what a is, then look for b in a". People call the operation "dotting into", it's clearly understood to be a hierarchical operation.
The "unform FCS" breaks that by looking up "a" and then completely ignoring "a" when looking up "b". That's what I don't like - I cannot see on the syntax that the local lexical scope affects "b", so I could decide to add a local variable with the same name and break things. Sure, I'd figure out, but it's still annoying:

   var bar = foo.bar();  // Looks good, won't work.

It also breaks the tear-off syntax, unless "foo.bar" is the same as the curried "(...) => bar(foo, ...)". Which is doable, but then we get into silly cases like:
   () => foo(a, b, c)
being writable as
   c.b.a.foo
I'd rather not enable that kind of behavior. :)

Totally agreed on all of that.
 

 
Extension methods are pretty different in that the member is no longer scoped to the thing on the left (whatever it is). But in practice, I didn't find it hard to get used to. Once I was used to it, it was a really powerful, expressive tool.

It's declaration isn't scoped to the type, but it's use is. It really is what it says: a scope extension of the class. If I understand traits, they are somewhat similar in that they are type-based extensions, there you just introduce a new type to an existing one (like interface injection) and then extend it in the same declaration.
That works well too.

+1.
 
 

Keep in mind that extension methods don't just let you add new methods to classes, they let you add new methods to types. So you can do things like:
  • Add a method to an enum type.
  • Add a sum() method to Iterable<num>, but not other iterable types.
  • Add a method to an interface.
And, of course, there's the general usefulness of being able to add new domain-specific operations to primitive types. If you're trying to define a DSL, it's important to be able to work with numbers and strings while still having them do things meaningful to your domain.

It's definitely useful. It's all based on (effectively) static methods, so you don't get virtual overrides, but for a DSL that's usually sufficient.

Yes. One of the things I've been surprised to learn over my years of OOP programming is how rare polymorphism actually is. It's important for some stuff, obviously, but you can get surprisingly far without it. What is really useful is the readable, auto-completable syntax you get from method chains.

Interesting trivia: The first version of C++ ("Cfront" at the time) didn't have virtual methods at all.


 
 
Not saying it can't work, but there are some pitfalls to address and avoid.

Yes, figuring out how it would interact with shadowing, library privacy, polymorphism, dynamic etc. Lots of interesting things to work through.

My initial impression is that there are other options I'd investigate before the UFCS approach, other options that use "." as well, but only works with delibrately introduced functions, not any static function in scope. Breaking the "a.b looks up b in a" rule is, IMO, too bad for readability. 
Traits or scoped extension methods look much more promising.

+100.

 
But, at a high level, I agree that Rust traits are also a good source of inspiration for us to learn from. The best solution for Dart may take ideas from C#, Kotlin, and Rust and synthesize them in some interesting way.

Or just a separate syntax for doing the invocation, I'm partial to "foo->bar()".

That does side-step a lot of problems, but it's hard to get over the unfamiliarity. It's really hard to beat "." when it comes to palatability.

True, but by using a different syntax, I hope it's clear that `bar` is looked up differently here than in `foo.bar`. It's harder to mis-read. It addresses my main issue with the syntax while retaining all the functionality. 
Example:
   foo.bar().print();
vs.
   foo.bar()->print();
In the former, the .bar() invokes a method, .print() invokes a static function - or a method, I don't actually know.
In the latter, it's clear that there is a difference, "print" is further away from the object (I actually like that it's a two-character operator). It really says "look over there!" to me. 
Might just be habitual thinking by now, but I think it works :)

Maybe. I'm open to the possibility, but I'd be surprised if it ended up being a more usable end result than just "overloading" "." to also support extension methods. If nothing else, C#, Rust, and Kotlin (maybe protocols in Swift?) have shown that users seem to handle it OK.

When it comes to stuff like DSLs, I think there may be a real downside to an alternate syntax like "->" that doesn't feel as first-class or natural as Ye Olde Full Stoppe.

Cheers!

– bob

Alex Tatumizer

unread,
Nov 26, 2017, 12:13:58 AM11/26/17
to Bob Nystrom, Lasse R.H. Nielsen, Kasper Peulen, Dart Core Development
Extension methods is very powerful feature indeed. Not only when you want to add methods to the class beyond your reach.

Just for the sake of example, suppose you want to add method to Iterable<int> - e.g. say for Iterable<int>, we define operator *int ("multiply by int")
With extension methods, you can do that. Without them, you can't, there's no mechanism for that..



Reply all
Reply to author
Forward
0 new messages