[Pitch] Functional - Clearer Design for Functions with Receivers

Hey,

Because functions are first-class types in Swift you can assign a variable a function, like sample in the sample.

struct Foo {
    func printing(_ i: Int) {
        print("\(Self.Type.self):\(i)")
    }
}
let sample = Foo.printing // sample is `(Foo) -> () -> Void`
sample(Foo())(0) //calling sample with receiver of type `Foo` and `0` as parameter

But if you want to use this syntax in a callback as a function parameter, you have to use:

struct Bar {
    func printing(_ i: Int) {
        print("\(Self.Type.self):\(i)")
    }
    
    let answer = 42
    
    private func currentDesign(action: (Foo) -> () -> Void) {
        let foo = Foo()
        action(foo)()
    }
    
    func usingCurrentDesign() {
        currentDesign(action: { (foo: Foo) -> () -> Void in
            return { // this second closure is needed because it is returning () -> Void 
                        //With parameters, there would be eg. `(i: Int) in`
                
                self.printing(self.answer) // although the receiver of `action` is Foo, self is Bar
                foo.printing(self.answer) // 1. thats why this correct call is necessary
            }
        })
    }
}

Bar().usingCurrentDesign() // prints: "Bar.Type:42\nFoo.Type:42"

//this sample compiles

Ideally at 1.: a call like self.printing(self:Bar.answer) would be perfect, with self being implicit Foo and the option to switch to the outer self of type Bar by adding a label.

So I would suggest this new design:

extension Bar {
    func newDesign(action: Foo.() -> Void) { //clear syntax, Foo is the receiver, () is the empty parameter list and returns Void
        let foo = Foo()
        foo.action() //instead writing `action(foo)()`, this syntax is clearer
    }
    
    func usingNewDesign() {
        newDesign {     // () has no parameter, so no '() in' needed
            print(Self.Type.self) // self is implicite Foo
            self.printing(14) // => "Foo.Type:14"
            printing(14) // like before, self can be ommited => "Foo.Type:14"
            
            let bar = self:Bar // switch explicite self to Bar (using label syntax from loops. Kotlin uses this@Bar)
            bar.printing(1) // => "Bar.Type:1"
            self:Bar.printing(1) // => "Bar.Type:1"
            
            printing(self:Bar.answer) // switch explicite self to Bar (using label syntax from loops. Kotlin uses this@Bar) and calls Foo.something(42) => "Foo.Type:42"
        }
    }
    
    func newDesignWithReturning(action: Foo.() -> Int) -> Int { //clear syntax, Foo is the receiver, () is the empty parameter list and returns Int
        let foo = Foo()
        return foo.action()
    }
    
    func usingWithReturning() {
        let foo = Foo()
        let i = newDesignWithReturning(action: {
            print(Self.Type.self) // like before, self is implicite Foo
            return 5 // needs to return Int
        })
        printing(i)
    }
    
    func newDesignWithArgumentAndReturning(action: Foo.(Int) -> Int) -> Int {
        let foo = Foo()
        return foo.action(10)
    }
    
    func usingWithArgumentAndReturning() {
        let i = newDesignWithArgumentAndReturning(action: { (i: Int) in
            print(Self.Type.self) // like before, self is implicite Foo
            print(i) // prints 10
            return i / 5 // needs to return Int
        })
        printing(i)
    }
}
Bar().usingWithReturning()
Bar().usingWithArgumentAndReturning()

// this new design does not compile

Because the current design is already implemented, the new design would be syntactic sugar for the developer.
Under the hood, the compiler could use the old design => so no breaking code (I think :)?

This pitch was highly motivated by Kotlin functions with receivers, see https://kotlinlang.org/docs/reference/lambdas.html#function-literals-with-receiver

1 Like

Proposal