I had this idea in an earlier post, but it was an unintelligible brain dump. I hope this is easier to follow.
The idea is very similar to the alternative proposal, but approaches it from another angle. I came to the realization that the alternative proposal felt like a function builder pattern on the function call. Function calls would conceptually only have normal argument types. The trailing closures are instead thought of as a metaprogramming equivalent to function builders that operate on the function call. I think this may appeal to more people since it does not need to be thought of as a function call with multiple kinds of arguments. Like the alternative proposal, this also solves the ambiguity.
Think of the following example as calling two SwiftUI modifier style metafunctions (called ".#animations:" and ".#completion:") that add the "animations" and "completion" arguments to the end of the "animate(..)" function call. The metafunctions use a different label-like syntax and metafunction composition operator ".#" to differentiate them from normal functions. These builder metafunctions (or label-like metaproperties if you prefer) are formatted with a macro-style syntax similar to literal expressions or conditional compilation blocks since that implies metaprogramming:
UIView.animate(withDuration: 0.7, delay: 1.0)
.#animations: {
self.view.layoutIfNeeded()
}
.#completion: { finished in
print("Basket doors opened!")
}
Button()
.#action: {
doSomething()
}
.#label: {
Text("")
}
.padding(20)
This composes well and the flow is easy to read with or without indentation. The view modifier continues to look visually connected to the Button. I assume the metaproperties act like normal properties where the order doesn't matter.
Alternatively, the macro syntax could be dropped since the syntax is already unambiguous:
Button()
.action: {
doSomething()
}
.label: {
Text("")
}
.padding(20)
This may be taking the concept too far, but there could be varients of these metafunctions for special cases. Think of .foo: as a metaproperty and .#bar(...) as a metafunction in this example.
// Set arguments by index.
RandomAction(action2: { doSomething2() })
.#action3: {
doSomething3()
}
.#argument(atIndex: 0) {
doSomething1()
}
// Variadic function
foobar(action1: { doSomething1() })
.#append() {
doSomething2()
}
.#append() {
doSomething3()
}
This syntax could be expanded to non-closure arguments to help pull large arrays, dictionaries, or verbose function calls out of the arguments list. Ordinarily I would put these awkward arguments in a constant then place the constant in the argument list. This would pollute the local namespace with a lot of constants that are only used once. By expanding this to other argument types, the awkward functions could be specified inline without losing track of parentheses and braces. This could benefit DSLs that use something other than a nested function builder for content. For example, a DSL built as a list of enum values could benefit.
// Note: A good example would be much more complex as this would inline the traditional way reasonably well.
// A constant that is used one time to help reduce nesting
let configuration = [
.name("My Name"),
.file("myfile", inBundle: "mybundle")
]
let service = MyService(x: 1, y: 1, z: 1, configuration: configuration)
// Now we can't use the `configuration` constant for anything else in this scope.
// Inline version
let service = MyService(x: 1, y: 1, z: 1)
.#configuration: [
.name("My Name"),
.file("myfile", inBundle: "mybundle")
]
This is almost the same as the alternative proposal, but adds a metafunction composition operator ".#" and may be easier for someone to wrap their head around two representations for arguments. Does anyone else feel this is Swifty way to fix the ambiguity and add multiple trailing closures/arguments?
EDIT: Fixed typos– many times.