[swift-evolution] Pitch: only allow capture of inout parameters in @noescape closures

Kevin Ballard kevin at sb.org
Thu Jan 28 15:46:45 CST 2016

I'm generally in favor of this. My biggest concern is when using
closures that are known not to escape the function but aren't marked as
@noescape. Sometimes this is just due to the frameworks not annotating
types properly, e.g. dispatch_sync()'s closure isn't @noescaping.
Sometimes this is due to a function taking a closure that may escape
sometimes, but not escape at other times (e.g. UIView animation methods
usually don't escape the animation block, unless a non-zero delay is
provided). And sometimes this is due to synchronization at a level the
API doesn't know about, such as using dispatch_async + dispatch_group to
wait until a block has executed.

That said, if dispatch_sync() is fixed, then I think it's reasonable to
require people to use manual workarounds for the remaining issues (e.g.
writing a local copy themselves). Especially because capturing an inout
parameter in a block is a relatively rare thing to do anyway (I don't
think I've ever actually done that myself, which is why I wasn't even
sure how Swift handled it).

The idea of using a capture list of [inout x] is interesting, but
ultimately confusing, because inout implies writeback semantics, when in
fact the closure wouldn't be doing writeback but would in fact be
closing over a shared mutable value (e.g. writeback implies there's a
specific point where the modified value becomes visible to the enclosing
scope, but that can't possibly work with non- at noescape closures because
there's no single point where the compiler knows it's appropriate to
insert the writeback). Based on that, I think the local copy version is
the most appropriate workaround.

So, given all that, +1 from me, but I really want to see dispatch_sync()
fixed as soon as possible (I want it fixed anyway, but I think
dispatch_sync() is the most likely candidate for unexpectedly hitting
this proposed limitation in Swift). I recognize that dispatch_sync()
isn't something the Swift team can fix (though Swift does have its own
copy of libdispatch, I assume that changes made to that don't actually
propagate upstream back to Apple's version), but since Swift core team
members are Apple employees, perhaps they can put some pressure on the
appropriate team to add the noescaping attribute to the appropriate
libdispatch APIs.

-Kevin Ballard

On Thu, Jan 28, 2016, at 11:58 AM, Joe Groff via swift-evolution wrote:
> I think the time has come for us to limit the implicit capture of
> 'inout' parameters to @noescape closures. In the early days before
> @noescape, we designed a semantics for capturing "inout" parameters
> that attempted to balance the constraints on inout with the usability
> of higher-order functions. Inout parameters don't pass a first-class
> reference as in other languages, but instead provide a limited lease
> to mutate a property for the duration of a call; this means closures
> can't extend the lifetime of that lease, so closures instead capture
> the local mutable copy of the inout parameter. This gives the expected
> behavior as long as closures over an inout parameter never escape
> their original scope, but leads to surprises as soon as those closures
> escape. This unintuitive behavior has made several "Swift gotchas"
> lists. Now that we can explicitly mark closures as not escaping, I
> think we should prevent 'inout' parameters from being implicitly
> captured by escapable closures. There are several ways we can still
> support today's behavior:
> - Encourage people to write the local copy themselves, if that's what
>   they want:
>> func foo(inout x: Int) {  var localX = x; defer { x = localX }
>> // Asynchronously mutate localX, then wait for it to finish
>> dispatch_async { mutate(&localX) }  dispatch_sync {} }
> - You can explicitly capture an immutable copy:
>> func foo(inout x: Int) {  // We don't need to mutate or observe
>> mutations on x in the async job  dispatch_async {[x] in
>> doStuffWithoutMutating(x) } }
> and/or we could introduce a new explicit capture kind for the current
> behavior:
>> func foo(inout x: Int) {  // Explicitly capture the 'inout' mutable
>> shadow copy of x  dispatch_async {[inout x] in mutate(&x) }
>> dispatch_sync { } }
> Making it explicit should make the behavior less surprising when it
> occurs. We should able to provide fixits to migrate code that relies
> on the current behavior as well. What do you all think?
> -Joe
> _________________________________________________
> swift-evolution mailing list swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160128/ec98ea96/attachment.html>

More information about the swift-evolution mailing list