dart-lang / sdk

The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
https://dart.dev
BSD 3-Clause "New" or "Revised" License
10.1k stars 1.56k forks source link

Kotlin style receiver Function literals with receiver #34362

Open tejainece opened 6 years ago

tejainece commented 6 years ago

The request is to add Kotlin style receiver function literals and receiver function types language features to Dart. This will enable among other things writing Kotlin style DSLs for Flutter UI, configuration, etc.

Receivers are anonymous (not necessarily) methods that enables the access on visible methods and properties of a receiver of a lambda in its body without any additional qualifiers.

class Html {
    Body body;

    Body body(void Body.receiver()) {
        var body = Body();  // create the receiver object
        body::receiver();        // Invoke receiver with receiver object
        this.body = body;
        return body;
    }
}

class Body {
    List<Children> children;

    Div div(void Div.receiver()) { ... }
    P p(void P.receiver()) { ... }
}

Html html(void HTML.receiver()) {
    var html = Html();  // create the receiver object
    html::receiver();        // Invoke receiver with receiver object
    return html;
}

html(() {       // Anonymous function with receiver begins here
    body(() {
        div(() {
             // TODO
        });
    });   // calling a method on the receiver object without additional qualifier
});

We could go a bit further and make empty () optional in the context of an argument.

With higher order function syntax, this would look like this:

html {       // Anonymous function with receiver begins here
    body {
        div {
             // TODO
        };
    };   // calling a method on the receiver object without additional qualifier
};

It allows other cool things like let, apply, with, etc from Kotlin. It looks prettier and clearer than yaml and toml for configurations.

lrhn commented 6 years ago

So void Body.receiver() is the function type (or method type?) of a function taking no arguments, returning void and needing to be run with a Body instance as its this binding.

That would be a new kind of function type, but not particularly special type-wise. It could be considered equivalent to a function with an extra required (non-nullable?) "named" parameter named this with type Body. All type checking should otherwise work normally for that. The invocation then passes the this argument along with any other arguments, and invocation proceeds normally, just with this bound to the special extra argument value. So, from an object model or type system perspective, this is hardly a controversial feature.

So, it's most likely possible, the question is whether it's desirable for Dart as a language.

It's not great for readability that you cannot infer what this points to from the surrounding text. If you are of the "Explicit is better than implicit" school, then this is not an advantage. If you like writing DSLs (the epitome of implicitness), then it's likely a great feature.

tejainece commented 6 years ago

So void Body.receiver() is the function type (or method type?) of a function taking no arguments, returning void and needing to be run with a Body instance as its this binding.

Yes, You are right. It is a function "receiver" type (or anonymous method type?).

That would be a new kind of function type, but not particularly special type-wise. It could be considered equivalent to a function with an extra required (non-nullable?) "named" parameter named this with type Body. All type checking should otherwise work normally for that. The invocation then passes the this argument along with any other arguments, and invocation proceeds normally, just with this bound to the special extra argument value. So, from an object model or type system perspective, this is hardly a controversial feature.

Yes! Just, would like to add that the this parameter is not exposed to the end user. It is implicit and hidden.

It's not great for readability that you cannot infer what this points to from the surrounding text. If you are of the "Explicit is better than implicit" school, then this is not an advantage. If you like writing DSLs (the epitome of implicitness), then it's likely a great feature.

You have listed some good concerns here. I like explicit programming (One of the reasons why I love Dart). But function receiver types and anonymous methods are completely explicit from context. I mean, you cannot have something like this:

// Not allowed!!!!! This is just a function or anonymous function. Not an anonymous method.
Function f = {
    body { ... }
};

It has to be either defined at a place where an anonymous method is expected or defined explicitly. Examples:

Example with explicit definition:

// Warning: Imaginary syntax. Declaring anonymous method named `myHtmlBuilder` on `Html`.
void Html.myHtmlBuilder() = {
    // It is very clear that this is anonymous method on `Html`
    body { ... }; // This is analyzable, easy to understand and totally explicit (As explicit as vanilla method)
};

Example with context:

// It is explicitly clear from `html` function's syntax that it is an anonymous method on `Html`.
// Also IntelliJ like parameter hint for implicit `this` parameter might be helpful.
html {
    body { ... }
};
tejainece commented 6 years ago

Adding an example for Kotlin's with, let, apply, etc.

Standard library or support library level with:

Future<void> with<T>(T value, void T.receiver()) async {
    await value?::receiver();
}

Usage of with:

class Something {
  void something() { ... }
  void somethingElse() { ... }
  void newThing() { ... }
}

main() {
  ...
  with(Somethng()) {
    something();
    ...
    somethingElse();
    ...
    newThing();
    ...
  };
  ...
}
lrhn commented 6 years ago

Declaring a method on Html, like:

void Html.myHtmlBuilder() {
    // It is very clear that this is a method on `Html`
    body { ... }; // This is analyzable, easy to understand and totally explicit (As explicit as vanilla method)
}

sounds remarkably like extension methods. The thing being asked for here is effectively to make function expressions that acts like extension methods, and which can be passed around like first-class objects before being bound to their receiver. That's more complicated than just scoped extension methods, because they bind the receiver late.

It's an interesting notion (I just read up on Kotlin's run/let/with/apply/also for a different reason). The underlying functionality would have something in common with extension methods, interface default methods and mixins - in all cases, a method is applied to another receiver than the one it was written for (if any). The one concept we do not have is a free first-class unbound method. That's a larger change to the function type system, but probably not a complex one.

If we want to add functions like the Kotlin apply or also, then we probably need self-types to express them. Perhaps through generic extension methods where the receiver is a type parameter.

The Dart versions of the Kotlin methods could be something like:

R T::let<T, R>(R Function (T) action) => action(this);  // Extension method on T, `this` has type T.
R T::run<T, R>(R Function (T) action) => this::action();
T T::apply<T, R>(R Function (T) action) { action(this); return this; }
T T::also<T, R>(R Function (T) action) { this::action(); return this; }
R with<T, R>(T object, R T::Function() action) => object::action();

(Except Dart wouldn't have apply and also, we'd just use cascades instead).

All in all, I like the idea. Not sure if we can make it a priority, though. It also matters whether we can do an efficient implementation of it, without inducing an overhead on code that doesn't use the feature.

@munificent (in case this might be useful for constructing UI).

tejainece commented 6 years ago

sounds remarkably like extension methods

Yes. Kotlin's let, run, apply and also require extension methods. It would definitely be cool to have extension methods as proposed here, which seems to be a non-intrusive addition to Dart's simple type system. However, this enhancement request is strictly limited to "receiver functions" without the need for extension methods. Only with and similar patterns are within the scope of this proposal. That excludes let, run, apply and also but also opens door to have them in the future.

Receivers could be implement as an anonymous function with a hidden implicit required parameter called this of receiver's type. So there wont be any necessity to add extension methods or changing Dart's type system. This should work with existing type system.

For example:

This

// Warning: Imaginary syntax. Declaring anonymous method named `myHtmlBuilder` on `Html`.
void Html.myHtmlBuilder() = {
    // It is very clear that this is anonymous method on `Html`
    body { ... }; // This is analyzable, easy to understand and totally explicit (As explicit as vanilla method)
};

would translate into:

typedef Function = void HtmlBuilder(Html html);
HtmlBuilder myhtmlBuilder = (/* implicit Html this */) {
    // It is very clear that this is anonymous method on `Html`
    /*implicit this.*/body { ... }; // This is analyzable, easy to understand and totally explicit (As explicit as vanilla method)
};

and this

Html html(void HTML.receiver()) {
    var html = Html();  // create the receiver object
    html::receiver();        // Invoke receiver with receiver object
    return html;
}

translate into:

Html html(HtmlBuilder receiver) {
    var html = Html();  // create the receiver object
    receiver(html);        // Invoke receiver with receiver object
    return html;
}

So with a little syntactic sugar and no change to Dart's type system, we can implement receiver methods and hence DSL.

Either implemented like this proposal or using extension methods like Kotlin does, I feel "receiver functions" would be a great addition to Dart (especially Flutter and maybe even Angular).

lrhn commented 6 years ago

Instead of introducing a whole new kind of function parameter, and answering the tricky questions around those (like, is a static function, which cannot have any uses of this, usable as a receiver-function for any receiver?), we could just allow functions to declare any normal parameter as a this parameter.

Then you can write (Html this) { somethingOnHtml(); } and the name will be looked up in the parameter this. If we combine this with something like #16900, then you can bind an object as a receiver as o -> (this) { .... use methods on this ... }.

(We still have to handle some edge cases with that approach, like cases where an instance method in the lexical scope is not on this).

That approach would not give as nice a syntax because you have to introduce a parameter and write this, not just write the Type:: in front, but instead it allows you to infer the parameter type in the normal way.

tejainece commented 6 years ago

I like your proposal. It does not introduce syntactic sugar and new syntax. Ofcourse one has to write (Html this) { ... }, but it looks more straight forward and intuitive.

class Html {
    Body body;

    Body body(void receiver(Body body)) {
        var body = Body();  // create the receiver object
        receiver(body);        // Invoke receiver with receiver object
        this.body = body;
        return body;
    }
}

class Body {
    List<Children> children;

    Div div(void receiver(Div div)) { ... }
    P p(void receiver(P p)) { ... }
}

DSL would look something like this:

html((this) {       // Anonymous function with receiver begins here
    body((this) {
        div((this) {
             // TODO
        });
    });   // calling a method on the receiver object without additional qualifier
});

With higher order functions and optional semicolons:

html (this) {       // Anonymous function with receiver begins here
    body (this) {
        div (this) {
             // TODO
        }
    }   // calling a method on the receiver object without additional qualifier
}
tejainece commented 6 years ago

Step 2. Extend the syntax of cascade to allow cascading blocks, like foo..{ code...}

"Cascading blocks" with implicit this context is a brilliant idea. With that we need neither InvocationContext (which feels very odd to me) nor receiver methods.

Example: This is basically with pattern baked into the language.

Html().{
  // Here "this" is "Html" instance on which cascading block was invoked.
  /* implicit this. */body(width: "100%").{ ... } // body is just a method on Html
}

Full example:

class Html {
    Body body;
    Body body({String width}) => this.body = Body()..width = width; 
}

class Body {
    List<Children> children;
    Div div(/*{... params ...} */) { ... }
    P p(/*{... params ...} */) { ... }
}

Usage in DSL with optional semicolons:

Html().{
    body().{
        div(class: "a-box").{
             // TODO
        }
    }
}
tejainece commented 6 years ago

the body can see too much inside the object

Only what is public. Nothing to loose our sleeps over. Also "cascaded blocks" have same problem.

Every change in the class (e.g. addition a new method) has the potential to conflict with some of the anonymous blocks already written by someone.

This same problem is already present inside a method. Adding new methods will eclipse the functions from global scope. This does not prevent people from adding methods though.

void hello() => print("hello");
class User {
  User() => hello();
  void hello() => "Hello from user"; // <= Newly added! Kaboom!
}

Kotlin has receiver methods and extension methods. Have to say, this has never been a problem. Again "cascaded blocks" have same problem.

which is aggravated by the fact that object's properties eclipse the names from the lexical scope, It's easy to inadvertently hit object's method instead of local one

Very careless programming. Never a real problem. This is already the case inside usual methods as mentioned above. "cascaded blocks" have same problem.

And then, there's a number of purely practical considerations. In your design, you have to define lots of auxiliary functions: e.g. "Body" has to define a function to insert "div", to insert "p", and every other type of element, True, these functions can be inherited from common base class, but the common base class cannot know anything about custom components. In flutter, custom components can nest, there can be Foo with Bar as a child, but Foo doesn't know anything about Bar, so it cannot contain "bar" method.

With InvocationContext, you have to implement multiple overloaded methods with different parent types. Dart doesnt support methods overloading yet. Besides, it is hard to know what a child can be added to (usually you wouldn't even have control over that package). It is easier to know what will be added into something. Kinda like add should be in List, not String, int and every other class.

but the common base class cannot know anything about custom components

There should be a Widget base class for widgets and generic addWidget receiver method in Body for custom widgets.

tejainece commented 6 years ago

Question 1: What gets passed as InvocationContext?

class Body {
  void operator+(InvocationContext<HtmlWidget> ic) { ... }
}

Html().{
    +Body().{ // <= What do you pass here?
      // ...
    }
}

If I understand right, this gets passed as invocation context? That is pretty cool! So when there is a no this (outside method or in cascade blocks), a method/operator with InvocationContext is disallowed.

Question 2: Abusing unary operators Abusing unary operators for this is not a good idea. But it does result in a clean syntax.

Question 3: It is an intrusive and inconsistent change because of the statement below:

allow every function/method/operator/constructor in dart to have a magic argument of type InvocationContext.

Having a method like this:

void something(InvocationContext<Html> ic, var1, var2) { ... }

and calling it like this:

something(var1, var2);

is not consistent.

However, using constructor of child or any other function that returns child to add children is a huge bonus!

tejainece commented 6 years ago

If we get just the "cascaded blocks", without any of receiver functions, InvocationContext or higher order functions, we can achieve the DSL like shown below:

class Html {
    Body body;
    Body body({String width}) => this.body = Body()..width = width; 
}

class Body {
    List<Children> children;
    Div div(/*{... params ...} */) { ... }
    P p(/*{... params ...} */) { ... }
}

Html().{
    body().{
        div(class: "a-box").{
             // TODO
        }
    }
}
lrhn commented 6 years ago

(Nitpick: There is no prefix + operator in Dart. The only overridable prefix operators are ~ and -).

MichaelRFairhurst commented 6 years ago

I like this, and I think if done right it can solve this case:

List.map((x) => x.foo);

It could also make for nicer tablification:

List.tableify((TableData td) => use(td.isEven, td.isOdd, td.isFirst, td.isLast, td.item, td.rowNumber));

assuming strawman syntax .{}/.=>, those examples become:

List.map(.=> foo);
List.tableify(.=> use(isEven, isOdd, isFirst, isLast, item, rowNumber));
Guang1234567 commented 4 years ago

Another example:

BEFORE:

return new Container(
  width: _kDotSpacing,
  child: new Center(
    child: new Material(
      color: color,
      type: MaterialType.circle,
      child: new Container(
        width: _kDotSize * zoom,
        height: _kDotSize * zoom,
        child: new InkWell(
          onTap: () => onPageSelected(index),
        ),
      ),
    ),
  ),
);

AFTER:

return Container{
    .width = _kDotSpacing;
    +Center{
        +Material{
            .color = color;
            .type = MaterialType.circle;
            +Container{
                .width = _kDotSize * zoom;
                .height = _kDotSize * zoom;
                +InkWell(
                    onTap: () => onPageSelected(index)
                )
            }
        }
    }
};

If dart ever comes up with an idea of how to make semicolons optional, this will remove all the remaining noise. (With the old format, optional semicolons won't help at all b/c commas)

Excellent kotlin like example! @tatumizer I like this DSL more than JSX in the JS world.

I also prefer swift to implememt this feature (with receiver) too, haha.

But dart extension most like swift extension that without where limit than kotlin at 2020-05-08.

Could add this feature (where limit) to dart to make it more powerful than like a Semi-finished Product?

The below swift extension example that reimplement the QuickCheck library in haskell to show where limit :

public struct Array<Element> {
}

// like extension name in dart
public protocol Arbitrary {
    static func arbitrary() -> Self
}

// like extension name in dart
public protocol Smaller {
    func smaller() -> Self?
}

extension Array: Arbitrary where Element: Arbitrary {
    public static func arbitrary() -> [Element] {
        let randomLength = Int.random(in: 0...50)
        return tabulate(times: randomLength) { _ in
            return Element.arbitrary()
        }
    }
}

extension Array: Smaller where Element: Arbitrary {
    public func smaller() -> [Element]? {
        if !self.isEmpty {
            return Array(self.dropFirst())
        }
        return nil
    }
}

public func check<A: Arbitrary & Smaller>(message: String, size: Int = 100, prop: (A) -> Bool) -> () {
    for _ in 0..<size {
        let value = A.arbitrary()
        if !prop(value) {
            let smallerValue = iterateWhile(condition: { !prop($0) }, initialValue: value) {
                $0.smaller()
            }
            print("\"\(message)\" doesn't hold: \(smallerValue)")
            return
        }
    }
    print("\"\(message)\" passed \(size) tests.")
}

Now use below code example to quicktest your qsort (QuickSort) function :

import Swift_QuickCheck

public func qsort(_ array: [Int]) -> [Int] {
    if array.isEmpty {
        return []
    }
    var arr = array
    let pivot = arr.removeFirst()
    let lesser = arr.filter {
        $0 < pivot
    }
    let greater = arr.filter {
        $0 >= pivot
    }
    return qsort(lesser) + [pivot] + qsort(greater)
}

check(message: "qsort should behave like sort") { (x: Array<Int>) in
      return qsort(x) == x.sorted(by: <)
}

Output:

"qsort should behave like sort" passed 100 tests.

Now using dart to reimplement maybe like that :

extension Arbitrary<Element: Arbitrary> on Array<Element>  {
   //  ...
}