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?
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 .
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.
Three questions for you:
-
if we prefer brevity over clarity when passing autoclosure parameters why don't we do the same for
&
and@
parameters? ("@autoinout" / "@autobinding") -
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?
-
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.
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.
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.
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?
Interestingly they used "lazy"
even if "lazy argument can be executed 0 or more times".
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)…" }
@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.
@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?
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.