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?
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/CYPc6qoaVvwAlso 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.
--
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.
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()".
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.
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 asc.b.a.fooI'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.
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 :)