SE-0286: Forward Scan for Trailing Closures

The updates to the proposal seem sufficiently targeted toward (what seems to be) the only major concerns in this thread. I personally would not be bothered by acceptance of the proposal as-amended if the Core Team believes that's what's best given the time constraints involved.

I agree, this revision looks good to me.

2 Likes

This proposal needs to put us at a good "resting place" both for Swift 5.3+ and Swift 6. The heuristic makes the forward scan work for enough existing APIs that I don't feel like we can simply drop it from Swift 6 without a suitable replacement. For example, I call out this SwiftUI example in the proposal:

func sheet(
  isPresented: Binding<Bool>,
  onDismiss: (() -> Void)? = nil,
  content: @escaping () -> Content
) -> some View

The intent here is clear: the unlabeled trailing closure is supposed to be content, because the SwiftUI DSL is formulated that way. If we take away the heuristic but don't have some way to indicate SwiftUI's intent, we have a source break without a good path forward for developers. Of course, a proposal along the lines of what's mentioned in "Future directions" of the proposal could absolutely revoke the heuristic from some future Swift version (be it Swift 6 or later), but it needs to do so with full consideration of the source compatibility impact and the need to support API developers. We can't assume that such a proposal will come along and be accepted in time; SE-0286 has to leave the language in a good place for all of the language versions it touches (Swift 5.3+ and Swift 6+).

SwiftUI isn't an outlier here; I saw a number of examples when I did a source compatibility run with the forward+backward scan but without the heuristic (I had the same thought as you).

Doug

5 Likes

Thanks for elaborating on that Doug. I'm very satisfied with this iteration of the proposal. Excellent work done on meeting such a tight schedule!

Doug, this is fantastic; thank you for taking my suggestions and experimenting with them. I really appreciate how you've tried out the design with and without the currently proposed heuristic, and that you've targeted the backward scan legacy affordances narrowly to single trailing closures.

I take it this means that the heuristic you propose would remain part of Swift for the foreseeable future; I think this is fine. In fact, I'd say that it's not an unprincipled compatibility measure, but perhaps fits into a bigger desire to align the behavior of arguments with default values with the behavior of overloads that omit the argument entirely; to the extent that the heuristic brings these two closer, it seems defensible on the basis of that principle, if we see fit to articulate such a principle as something desirable.

2 Likes

I dropped the "unprincipled" adjective from the revision. It's a reasonable heuristic that's fairly easy to describe and that seems to work quite well in practice, so yes, I'm proposing to keep the heuristic for the foreseeable future.

Doug

This change makes tactical sense, because it deals with one of the grating inconsistencies between function calls with and without trailing closures.

Strategically, I worry that patching over these inconsistencies in a piecemeal fashion is going to lead to the language being left in a local suboptimum, where the design is considered ā€œgood enoughā€ and further progress stalls because of source compatibility and other considerations. In the first pitch, where you were staging this change in using attributes, I was going to suggest that you instead just fix this all at once using an attribute (enabling forward scan, callee control of labels, etc.) and either live with that forever or find the resolve to flip the default in future. Fortunately/unfortunately you found a clever way to avoid the attributes here, so that's a less compelling alternative.

So I agree that this is an uphill step, but I worry about where the language is going to end up as a result.

The (revised) proposal makes so much sense that I just want to +1.

Though I'm a little confused as to why we need to define "structurally resembles" function types. Is it different from "anything that can use closure"?

A parameter of type Any can accept a closure, but does not "structurally resemble" a function type. I wanted the narrower definition.

Doug

2 Likes

Mmm, this is interesting, and Iā€™m not sure Iā€™d appreciated that. Currently, Any works with trailing closure syntax:

func f(_ x: Any) -> String { "Hello, \(x)!" }
print(f { "World" })
// ā€œHello, (Function)!ā€

Unless thereā€™s a compelling reason, I donā€™t know that we should break that, and Iā€™m not sure itā€™s touched on in the proposal.

1 Like

SE-0286 has been accepted with the modifications mentioned above. Thank you all for your reviews.

10 Likes

From the proposal:

The unlabeled trailing closure will be matched to the next parameter that is either unlabeled or has a declared type that structurally resembles a function type

so this is still well-formed.

Doug