[Closed] Pitch: Explicit marker for autoclosure parameters

Imo this might be yet another issue that is better solved in the tooling rather than in the language: If @autoclosure is really a problem, why not just set the default style for such parameters to bold red in the editor?

3 Likes

It's not so nice looking for the non-trailing closure parameters:

foo({ 5 > 2 }, 2) { 3 } // vs
foo(closure 5 > 2, 2, closure 3)

But this is indeed a possibility (see the alternatives 2 & 3).

Basically, if you allow for an alternative syntax for making closures that doesn't involve nesting, e.g. this:

    foo({ foo(42) })  <--> foo(closure foo(42))

and do a special exceptional treatment for short-circuiting bool operations then @autoclosure feature is not needed. I guess that's what other languages are doing for bool operators (I'm not so sure what do they do for things like logs, etc to conditionally evaluate parameters).

I'm 99.9% IDE user so that would definitely work for me. Do you mean this is already possible?

I don't think so — and afair, the plug-in capabilities of Xcode are not that great :face_with_peeking_eye:.

Well, we’d also have to account for ??, but operators aside @autoclosure has its place.

assert and precondition have already been brought up, but even if those were specially handled it would break XCTAssertThrows in a way that’s impossible to circumvent. And if we privilege that, how many third-party libraries do something similar?

Could be this for asserts / etc, or do you think that's too much hassle?

my_assert { condition }
my_assert { condition } message: { "some expensive message \(here)" }

@autoclosure is a feature that requires some discipline on the part of API designers. I think this has always been true and usually been understood by everyone involved. When it is used with discipline, though, it is very powerful and useful. I don’t think we should make the good uses worse to help control the rare abuses of the feature.

21 Likes

Three questions for you:

  1. if we prefer brevity over clarity when passing autoclosure parameters why don't we do the same for & and @ parameters? ("@autoinout" / "@autobinding")

  2. if we didn't have @autoclosure parameters now (just the short-circuiting behaviour for bool operations and using explicit closures for asserts, etc), would we introduce them now?

  3. why don't other languages (old, and more importantly new that were created after Swift) have autoclosure equivalents?

Not porting judgement on anything but...

D has the same concept (lazy parameters) and it's likely that it was the inspiration for Swift because it works exactly the same.

C/C++/Objective-C use macros that look like functions to achieve the same effect.

Rust also uses macros for this (see assert!), with a more hygienic macro system. I suppose the ! at the end of a macro name in Rust would satisfy your requirement of being clear at the call site that something special is happening.

There is definitely a need for this lazy evaluation feature, even though the implementation isn't always through closures.

4 Likes

Something like @autoclosure *could be a great feature, but it has always been half-baked:

I think maybe the conflation of what it does with closures was not the right way to go. All of this compiles:

func ƒ(_: () -> Void) { }
func ƒ(autoclosure: @autoclosure () -> Void) { }
func ƒƒ(_: (() -> Void) -> Void) { }
func ƒƒ(autoclosured: (@autoclosure () -> Void) -> Void) { }

ƒƒ(ƒ)
ƒƒ(ƒ(autoclosure:))
ƒƒ(autoclosured: ƒ)
ƒƒ(autoclosured: ƒ(autoclosure:))

What you are suggesting is like first adding @autoinout so that we can write

func reduce(state: inout State, _ action: Action) { }
...
var state
reduce(&state, action)

as

func reduce(state: @autoinout State, _ action: Action) { }
...
var state
reduce(state, action)

and then you pitch requiring call site notation to turn that into something like

var state
reduce(inout state, action)

I can understand if you want to get rid of autoclosure, but what is the point of keeping it and requiring a special annotation? Then what you are actually pitching is nothing more or less than a new notation for closures.

1 Like

Instead of introducing a new keyword, how about allowing user to pass a closure explicitly?

This is @tera's proposal:

baz(autoclosure foo(42))

This is mine:

baz(foo(42)) // this works
baz { foo(42) } // how about allowing this too?

This will give user a choice when they have to use the baz() API but find it's confusing. And it's unlikely to break existing code.

1 Like

The question is still, “Why?” @autoclosure exists to give API designers the power to defer evaluation of argument expressions. What is gained by giving clients the ability to spell these expressions as closures, and why is that gain worth introducing the ambiguity of whether the client-provided closure itself is then wrapped in an automatic closure?

2 Likes

Interestingly they used "lazy" even if "lazy argument can be executed 0 or more times". :thinking:

It's about consistency and established precedents. I'm applying the same line of reasoning used to defend @autoclosure feature existence in its current form ("brevity matters more than clarity at the point of use", "it requires discipline, but it is powerful", "let's not punish good users because of a few abusers", "if we didn't have this feature by now we'd introduce it now" to the similar features @autoinout and @autobinding. If the line of reasoning good for the former it should be as good for the latter, right?

Not autoclosures - that's what closures do by themselves. @autoclosures merely allow to disguise the call site by not having the { } symbols that we'd have to put otherwise:

precondition(denominator != 0, "Can't divide \(numerator) by zero")
logger.info("Starting division of \(numerator) by \(denominator)…")

vs

precondition { denominator != 0 } message: { "Can't divide \(numerator) by zero" }
logger.info { "Starting division of \(numerator) by \(denominator)…" }
1 Like

@autoclosures merely allow to disguise the call site by not having the { } symbols that we'd have to put otherwise

Precisely. That's why it makes no sense to keep autoclosure if you require the call site to be annotated.

I am not disagreeing with that, and suggesting that in alternatives #2 and #3.

It shouldn't be that surprising: lazy has an existing meaning in Swift where the initializer is run once on demand the first time the value is requested while D is a different language unencumbered by this precedent. Given lazy in D is older than Swift itself, it's likely Swift designers knew about it but found having two different meanings for lazy would be confusing and went for @autoclosure instead.

4 Likes

@autoclosure should be a parameter wrapper. It's just a representation of a get accessor (with additional complexity from rethrows that I'm not going to address here). If it were used like this…

@propertyWrapper public struct Get<Value> {
  public var wrappedValue: Value { projectedValue() }
  public var projectedValue: () -> Value

  public init(wrappedValue: @autoclosure @escaping () -> Value) {
    projectedValue = wrappedValue
  }

  public init(projectedValue: @escaping () -> Value) {
    self.projectedValue = projectedValue
  }
}

func and(_ bool0: Bool, @Get _ bool1: Bool) -> Bool {
  bool0 && bool1
}

…then we would have the ability to choose how to utilize the get/value duality, which @autoclosure does not allow:

func expensive() -> Bool { .init() }
_ = and(true, expensive())
_ = and(true, $_: expensive)

However "projected parameter syntax" (or whatever that's called) cannot be used with operators (yet), and init(wrappedValue:) still requires the @autoclosure we have.

Considering the reasoning for explicit marker for @autoclosure parameters, in @tera's opinion, @Wrapper parameters should also be explicitly marked, right?

1 Like

The more I think about it the more alternative #2 is appealing ("Prohibit autoclosure parameters altogether"). Operators aside (which, btw, do not need @autoclosure feature as well, which I can expand later on), consider these examples:

precondition(denom != 0)
precondition{denom != 0}

precondition(denom != 0, "Can't divide \(num) by zero")
precondition { denom != 0 } message: { "Can't divide \(num) by zero" }

logger.info("Starting division of \(num) by \(denom)…")
logger.info{"Starting division of \(num) by \(denom)…"}

c.horizontalSizeClassValue(regular: foo(), compact: bar(), otherwise: baz())
c.horizontalSizeClassValue { foo() } compact: { bar() } otherwise: { baz() }

All we are gaining by having auto closures here is that we write the first lines instead of the second, and numerous of drawbacks mentioned above (lack of clarity at the point of use, potential for abuse, extra complexity in the compiler, etc). Doesn't look a win overall.

The declaration for precondition() is

public func precondition(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = String(), file: StaticString = #file, line: UInt = #line)

Note that the last two parameters are not autoclosures, nor is there any reason for them to be, since no one should ever be passing them.