Receiver Closures

This could also have some very interesting synergy with the Static Callable Pitch which is currently in review and seems pretty likely to be implementd

I hope i can explain it well enough.

// static callable allows to call an class/struct as if it was a method
struct Adder {
    var base: Int
    call(_ x: Int) -> Int {
        return base + x
    }
}

let add = Adder(base: 2)

add(1) // -> 3

// by using this it would be possible to define a protocol
protocol Buildable {
    mutating call(_ build: @receiver (inout Self) -> Void)
}

extension Buildable {
    mutating call(_ build: @receiver (inout Self) -> Void) {
        build(&self)
    }
}

Using this protocol it would be possible to eliminate even more boilerplate code when defining a DSL.

struct HTML: Buildable {
    
    var head = Head()
    var body = Body()

    struct Head: Buildable { }
    struct Body: Buildable {}
}

This could be used like with whatever the calling syntax it turns out to be.

// Using "no call site indication" syntax

var html = HTML()

html { // Self: Head
   head { // Self: Head
       ...
   }
   body { // Self: Body
       ...
   }
}
2 Likes

After some initial concerns, I'm now quite interested in this feature.

However, I don't quite understand why we need a new type.

class A {
   func a() -> Int { return 1 }
}
type(of: A.a) // (A) -> () -> Int

Why don't we reuse the same type for receiver closures as we do for methods.
After all, normal closures also share the same type with normal functions.

let closure: (Person) -> () -> () = {
   print("name = \(name)") // references Person.name
}
var bruce = Person(name: "bruce")
closure(bruce)() // prints: name = bruce

@jrose: you are right that mutation is important and that receiver closures should support it. For classes that is not a problem, for structs we could use (inout Self) -> (Args) -> Result. Currently this does not work (error: escaping closure can only capture inout parameters explicitly by value), but maybe this restriction can be lifted?

The question of course is, how the compiler would parse the body of such a closure. It would have to distinguish a receiver closure with an implicit self from a normal closure which returns a function.

The restriction on mutation isn't arbitrary; it's a consequence of inout being "copy-in, copy-out". When you have the value fn: (inout Foo) -> () -> Void, for example, calling let boundFn = fn(&foo) is only able to mutate foo for the duration of that call—i.e. before you even get to call boundFn().

3 Likes

Thanks for the explanation!

There was an accepted proposal to change unnapplied mutating method references from (inout Type) -> (...) -> Void to (inout Type, ...) -> Void, but if I remember correctly it missed its chance to be implemented.