[Pitch] Make autoclosure and escaping mutually exclusive

Hi everyone,

after having a discussion with a coworker about an interesting idea he had, I came to realize that a weird case can be constructed with autoclosure and escaping.

Let me first post some code and discuss it afterwards. This is straight from a Xcode 10.2 playground:

import UIKit

var str = "Hello, playground"

func printExpression(_ exp: @autoclosure () -> String ) {
    print(exp())
}

class AutoEncloser {
    let closure: () -> String

    init(expression: @escaping @autoclosure () -> String) {
        self.closure = expression
    }

    func printIt() {
        print(closure())
    }
}

print("immediate: \(str)")

let encloser = AutoEncloser(expression: "escaping: \(str)")
encloser.printIt()

str = "Hello changed String!"
encloser.printIt()

Discussion:

The method printExpression() shows the normal use of autoclosure, so nothing special to see here.

The AutoEncloser class takes an expression and holds on to it as a closure. That's where the problems start.

  1. The caller (AutoEncloser(expression: "escaping: (str)")) has no idea that he might be in for a retain cycle.
  2. The caller also has no idea that the behaviour of the expression can change after the function call, where it has been supplied to, has returned. If you run the code you can see that in the two last lines.

The obvious fix is to forbid the usage of an escaping autoclosure. IMHO the Swift compiler should at least create a warning for that, as it seems to be an obvious anti-pattern to me.

Please have a discussion, Thank You

Roddi

2 Likes

You've explicitly tagged it as @escaping, signalling intent, and there's already an error if you don't do that. How would you silence a warning if you did intend it to escape? This might be ill advised in many situations, but I don't think it's much more harmful than a regular escaping closure.

1 Like

The interesting place is the calling code:

There is no hint that evil side-effects lurk there. But as the example shows they clearly do. I consider that very bad.

I would think the @autoclosure was created under the assumption, that calculating the expression not before the call to the function is generally safe when you do it in the scope of the called function. If I did not miss something this assumption should always hold.

But said assumption breaks spectacularly, when you hold on to the closure and run it some time later potentially even on a different thread. I would argue that was probably not an intended or considered use-case of @autoclosure when it was introduced.

It was certainly a considered use case. It even had its own spelling, @autoclosure(escaping), before we had @escaping. See SE-0103.

Nimble explicitly relies on this behavior with its expect() construct:

expect(foo).toEventually(equal(bar))

The expect(…) creates an expectation, which a test like toEventually can check repeatedly, as many times as it likes, after the call to expect is completed.

It may be surprising that a callee can reevaluate a naked expression after the call is complete, but this behavior is by design as Xiaodi notes, and has existing use cases.


This is one of many things in Swift where I wish our dev environments had more elegant, not-too-intrusive visualizations of static analysis right in the editor: subtle visual indications at the point of use of which expressions are autoclosured, which closures are escaping, which identifiers inside a closure capture something, when . is doing a dynamic dispatch, which assignments happen with value semantics, etc.

4 Likes

On the other hand, I'd argue that if you have to know at the call site that an expression is being autoclosured, the designer of the function you're calling has already failed. expect(foo).toEventually { equal(bar) } would have been fine.

(I'm minorly in support of this change, but I'm not sure it's worth the massive source breakage from the camps that do use escaping autoclosures. It's not like we'd be able to remove the feature or even warn about it unconditionally, because that would break existing libraries.)

[EDIT: Paul points out I made a mistake in my code example, but the point stands.]

In context, the more explicit replacement would be:

expect({ foo }).toEventually(equal(bar))

(it’s foo that it repeatedly evaluates until true)

…and I agree that’s a gain in clarity. My point is only that it may be hard to change this since it would be source-breaking for many projects.

1 Like

So if I get you right, this is mainly used to get rid of extra {} while at the same time sacrificing readability and safety.
And if you consider how easy it would be write a migration for the change, I see no good reason to keep the escaping autoclosure behaviour. Swift is being advertised as having safety as a design goal after all.

Thus I rest my case.

I am not a fan of this pitch. I have a couple parser DSLs where I almost exclusively use @autoclosure with @escaping The whole point of the APIs are that things are escaping, so they are clear.

2 Likes
Terms of Service

Privacy Policy

Cookie Policy