Next step for the Multiple Trailing Closures syntax?

NOTE: please don't miss EDIT 2, the idea is there! thx

Today's syntax:

publisher.sink(receiveCompletion: { completion in
    // handle error
}) { data in
    // handle data
}

Tomorrow's syntax:

publisher.sink { data in
    // handle data
} receiveCompletion: { completion in
    // handle error
}

Requested controversial syntax:

publisher.sink receiveValue: { data in
    // handle data
} receiveCompletion: { completion in
    // handle error
}

Proposed, more Swifty syntax:

publisher.sink receive(value data): { // data parameter name is here optional
    // handle data
} receive(completion): {
    // handle error
}

with a new declaration for sink:

func sink(
    receive receiveCompletion: @escaping ((completion: Subscribers.Completion<Self.Failure>) -> Void),
    receive receiveValue: @escaping ((value: Self.Output) -> Void)
) -> AnyCancellable

What do you think? Crazy idea?

EDIT: (you can skip it, go to EDIT 2)

func sink(
    receive: @escaping ((_ completion: Subscribers.Completion<Self.Failure>) -> Void),
    receive: @escaping ((value: Self.Output) -> Void)
) -> AnyCancellable

with mandatory value: label (but optional '_ completion:') (to disambiguate receive: label used twice) should be more correct in this example... I guess.

EDIT 2: -----------------------------------------------------------------
The cleanest declaration should be:

func sink(
    received: @escaping ((value: Self.Output) -> Void),
    received: @escaping ((completion: Subscribers.Completion<Self.Failure>) -> Void)
) -> AnyCancellable

and simplest usage:

publisher.sink received(value): { // `received` label is optional => publisher.sink (value): {
    print(value)
} received(completion): {
    handleError(from: completion)
}

but also (if desired):

publisher.sink received(value data): { // `value` acting as label name, `data` as user chosen parameter name
    print(data)
} received(completion _): { // parameter not needed
    fatalError()
}

If anything, given that the argument name holds no significant to the closure type (and I don't think any of this will change that), you'd probably just have

publisher.sink receive(data): {
    // handle data
} receive(completion): {
    // handle error
}

Consequently, closure syntax could naturally be revised (for coherence and to get rid of the not so convincing 'in' keyword) into:

_ = [1, 2, 3, -1, -2].count(where: { value in value > 0 })
_ = [1, 2, 3, -1, -2].count(where(value): { value > 0 })

_ = [1, 2, 3, -1, -2].count { value in value > 0 }
_ = [1, 2, 3, -1, -2].count (value): { value > 0 }

_ = [1, 2, 3, -1, -2].count where: { value in value > 0 }
_ = [1, 2, 3, -1, -2].count where(value): { value > 0 }

_ = [1, 2, 3, -1, -2].count(where: {(value: Int) -> Bool in value > 0 })
_ = [1, 2, 3, -1, -2].count(where(value: Int) -> Bool: { value > 0 })
    
...

I'm not sure, just thinking... but always shorter syntax! ;)
@Joe_Groff, maybe could you comment?

value in receive(value data) was the Argument Label to match parameter signature.
data the chosen parameter name...

The problem is that there is no receive(value:) as an argument, only receive. The (value:) part is only privilege to functions and methods.

If we're to add this, it should be (and has been) a separated piece:

1 Like

That aside, I'd prefer if the design in this area is more orthogonal to the existence of the label. If the existence of receive label could warrant an entirely new syntax, that'd ramp up language complexity quite quickly.

value is in closure declaration:
... receive receiveValue: @escaping ((value: Self.Output) -> Void) ...

receiveValue is here only to allow the double usage of receive:

func foo(a: Int, a: Int) // ambiguous, forbidden
func foo(a a1: Int, a a2: Int) // non ambiguous, allowed

Yes, and that has not been done since SE-0111. There is no type (value: Self.Output) -> Void in the type system, only (Self.Output) -> Void. So the next best place to put (value:) would be in the argument name–receiveValue(value:). Then again, that'd easily be a separated pitch/proposal.

Your design is an interesting one, and I may end up using it should it be implemented. I'm just pointing out that it takes multiple steps to reach there.

I want to clarify this a bit. Currently closures have many syntaxes:

{ $0 < $1 }
{ (a, b) in a < b }
{ (a: Int, b: Int) -> Bool in a < b }

It'd be nice if the new syntax can just work with any of the syntax above, rather than creating a new set of argument closure syntax.

1 Like
{ $0 < $1 }
(a, b): { a < b }
(a: Int, b: Int) -> Bool: { a < b }

was the consequence on closures syntax I suggested earlier in this discussion...

They're slightly different. If we take:

func foo(ext int: Int) { ... }

a and b would be akin to int, which we allow. But if we're to put (completion: Subscribers.Completion<Self.Failure>) -> Void, completion would be akin to ext, which is generally not allowed there. Heck, I'm not even sure if completion fits either bill, it could be a completely different component to the closure name/type.

I've edited initial post to change sink function definition into:

func sink(
    receive: @escaping ((_ completion: Subscribers.Completion<Self.Failure>) -> Void),
    receive: @escaping ((value: Self.Output) -> Void)
) -> AnyCancellable

with mandatory value: label (but optional '_ completion:' ) (to disambiguate receive: label used twice) to allow following usage:

publisher.sink receive(value anyName1): {
    // handle data by using var anyName1
} receive(anyName2): {
    // handle error by using var anyName2
}

but also

publisher.sink receive(value): {
    // handle data using value var
} receive(anyName): {
    // handle error using anyName var
}

but of course, the best proposition could be:

func sink(
    receive: @escaping ((completion: Subscribers.Completion<Self.Failure>) -> Void),
    receive: @escaping ((value: Self.Output) -> Void)
) -> AnyCancellable

with usage1:

publisher.sink receive(value): {
    // handle data using value var
} receive(completion): {
    // handle error using completion var
}

and eventually usage2:

publisher.sink receive(value anyName1): {
    // handle data using anyName1 var
} receive(completion anyName2): {
    // handle error using anyName2 var
}

This is a nice syntax, but... to put it plainly, I don't think we need to add one more trailing closure syntax.

That aside, how do you include a capture list with this new syntax. What do you do if you need to capture a weak self for instance?

1 Like

Instinctive response :slight_smile: :

URLSession.shared.dataTask(
    with: urlPath,
    completionHandler(data, response, error): [weak self] { ... }
)

URLSession.shared.dataTask(with: urlPath) (data, response, error): [weak self] { 
    ...
}

Why would you add one more closure syntax when you could deprecate the actual one :wink:
Personally, I was never convinced by the 'incoherently inner'-'in keyword' closure syntax, did you?

Can we really deprecate the current one? Somehow I doubt it. I was never totally satisfied with the current syntax, but that hardly seem a good reason to overhaul the syntax one of Swift's fundamental concept people have been using everywhere for 5 years.


If the goal is really to deprecate the current closure syntax, I think the syntax also need to work outside of the trailing closure context. For instance, what would this look like with the new syntax:

let closure = { [unwoned self] (x: Int) in x + self.value }

or this:

let closure: (Int) -> Int = { [unwoned self] in $0 + self.value }
let closure = (x: Int): [unowned self] { x + self.value }

let closure: (Int) -> Int = [unowned self] { $0 + self.value }

What do you think?

One more time: when I wrote this post, I was only considering my revision of the multiple trailing closures syntax because it looks cool to me (initial post, EDIT 2). It's only after sharing that closure syntax rethinking appears to me as a consequence. Everything here is a fresh idea (not even half a day), maybe all of this is just a big big fail. But pleasant to eyes, don't you think?

Only Crusty (or @dabrahams to advocate him?) could tell :slight_smile:

Note: the two syntaxes can coexist during transition phase...

Also, don't forget that closures can specify their return type:

let closure = { (x: Int) -> Int in x + 1 }

This is sometime necessary when the closure body is complex.

Personally, I'm not convinced it's a good thing to have two syntaxes for the same thing. People will have to learn two syntaxes (since we all read other people's code at some point).

And in a way we already have two: one for closures and one for regular functions. They're both pretty much the same thing since you can nest a function inside another function and capture the local variables.

1 Like

Return type: already mentioned earliest in my posts.

let closure = (x: Int) -> Int: { x + 1 }

My goal is not to deprecate or mess closure syntax. It's just to share what I feel like a more sexy, more Swifty syntax:

Current (5.3)

publisher.sink { value in
    ...
} receiveCompletion: { completion in
    ...
}

vs

publisher.sink (value): { // or publisher.sink received(value): {
    ...
} received(completion): {
    ...
}

or if you want a slightly different and uselessly more verbose example:

publisher.sink received(value data: String) -> Void: {
    print(data)
} received(completion _: Subscribers.Completion<DummyError>) -> Void: {
    fatalError()
}

First of all I don’t quite understand the purpose of ‘value’ in the following call:

Secondly, I feel that the proposed syntax isn’t as clear as the current one. In my opinion, the current way of specifying values to be used in a closure is quite good.

value is a label which participate in closure signature, it serves to disambiguate the double usage of receive label. If you prefer:

publisher.sink received(value data: String): { ... }

which means for the closure caller side:

receiveValueHandler(value: "foo")

But this is NOT the interesting part of the proposal. It's a specific case of sink example.

The interesting part is the embedding of closure signature in label/parameter and its consequence: a new coherent and outer-block syntax for closures.

Another example:

URLSession.shared.dataTask(
    with: urlPath,
    completionHandler(data, response, error): [weak self] { ... }
)
Terms of Service

Privacy Policy

Cookie Policy