In the recent discussion about @ArrayBuilder, it came up that trailing closure syntax is not currently supported in the most intuitive way for Array initializers like init(@ArrayBuilder build: () -> [Element]):
I investigated this on the parsing side, and have a prototype implementation: Support `[Element] { ... }` trailing closure init syntax by calda · Pull Request #86244 · swiftlang/swift · GitHub
We can pretty easily allow trailing closure syntax to be used for Array and Dictionary initializers following the sugared type name. For example:
extension Array {
init(@ArrayBuilder_ build: () -> [Element]) {
self = build()
}
}
var value = [String] {
"a"
"b"
}
Code like this is currently rejected with various errors, and would become supported:
// error: 'let' declarations cannot be computed properties
let a = [String] {
"a"
}
// error: variable with getter/setter cannot have an initial value
var b = [String] {
"b"
}
The more impactful change is that this also affects array value literals that could be confused for types with a closure on the following line. For example, this closure is currently interpreted as a separate, unused closure (producing an error):
let string = "foo"
let array = [string]
{ print("bar") } // error: closure expression is unused
but would instead become interpreted as a trailing closure (which also produces an error);
let string = "foo"
let array = [string]
{ print("bar") } // error: cannot call value of non-function type '[String]'
Since these examples are all invalid today, would be fine to change the meaning of them. The only example I can find where this is valid today is a result builder that takes closure values:
@resultBuilder
enum FunctionArrayBuilder {
static func buildBlock(_ components: (() -> Void)...) -> [() -> Void] {
components
}
}
@FunctionArrayBuilder
var buildFunctions: [() -> Void] {
let foo = "foo"
let array = [foo]
{ print(array) } // Currently compiles, but if parsed as trailing closure would error with "cannot call value of non-function type '[String]'"
}
To avoid this source break, we could include heuristics like:
- The trailing closure open brace
{must be on the same line as the closing brace]of the array type. - The array literal must parse successfully as a type (excludes arrays like
["foo"], but still includes arrays like[foo]). - The array literal must only include a single element (excludes arrays like
[foo, bar]).
There is existing precedent in the language for the the presence of a new line changing whether code is interpreted as a call or two separate expressions:
func foo(
a: String,
b: String
) {
print("Called", a)
}
// Calls `foo`
let first = foo(
a: "1",
b: "1"
)
// Does not call `foo`
let second = foo
(
a: "2",
b: "2"
)
// Prints:
// Called 1
// ()
// (String, String) -> ()
print(type(of: first))
print(type(of: second))
Given all known potential source breaks currently include the newline, and there is existing precedent for that sort of heuristic, including that one heuristic to avoid the source break seems reasonable. Including the other heuristics probably doesn’t hurt, but I can’t think of an example where it would matter.
What do folks think about the pitch overall? Can anyone think of any other source compatibility concerns? I worked through some examples with if statements/expressions and everything checked out.