Case 4 should work. Since it's implicitly ignoring Void, the block in for doesn't emit anything, and so should also be ignored. If for design is laid out, that will most likely be the case, since it is for if-else block.
Joe, in example (4) here, in that âfullness of timeâ future, do you imagine the for loop mapping to a ForEach-like construct with an empty body? Or being elided completely, because the body emits nothing? (I realize Iâm asking you to speculate about a distant hypothetical not yet even proposed, so any response with a grain of salt obvs.)
I like this feature as a whole. What I am really hoping for is that this prioritizes the completion of key features missing in generics right now like variadic generics as I have been waiting for that for quite a while. And if the introduction of this helps motivate and speed up progress in other areas across the language, it's a win win.
I'm really happy to see the general feature. I can see many applications beyond SwiftUI which is itself really exciting.
However, I'm very concerned about the lack of support for fundamental language constructs like switch, for, while, etc. The fact that SwiftUI has a View type called ForEach is indicative of the problem.
I don't know what it would take to fully support other flow-control and loop constructs, but I think that the inclusion of support for ifâwhile intuitive and expressiveâonly puts the lack of support for the other constructs into sharp relief. I know I will frequently have the impulse to reach for these flow-control language constructs in SwiftUI, and will be repeatedly frustrated by their absence...
And this is dangerous, even in the short-term, because the result will be implicitly training me and every other developer to avoid switch and for when they're really the right tool for the job... except in a DSL.
Can you elaborate on this further? The mapping I can imagine would require the body of the for loop to be capturable by the DSL for lazy evaluation the way ForEach captures a closure. That seems to contradict the stated goal of keeping the evaluation semantics roughly the same as normal Swift functions. Maybe I'm missing something here.
I've been considering this proposal all week as I am learning SwiftUI. There is a lot to like here. This design offers support for very sophisticated eDSLs in a relatively straightforward manner. The type safety and inference it makes possible are wonderful! I have written many DSLs and appreciate very much how much power this design puts in the hands of a library author.
That said, I do share some of the concerns others have mentioned. This shouldn't be interpreted as opposition in any way. I am generally in favor, but I do think we should pay close attention to these concerns and at least consider revising the proposal in response to them.
"function builders" vs "builder functions"
Starting with the trivial, I find the name "function builder" confusing. It indicates that a function is being built. That is not the case. The function is doing the building. So I agree with commenters in favor of calling this "builder functions" instead.
do support seems unnecessary
I agree with previous commenters that a domain-specific grouping operator would be better than piggybacking on do. Without a meaningful motivating use case I think it should be removed from the proposal.
Void and @discardableResult
I agree with others that the inability to insert assertions or print statements is surprising. We should consider very carefully how Void and @discardableResult statements are handled.
I think it is safe to just ignore Void returning statements. They provide only a single bit of information to a builder. If a DSL really needs to record a single bit in some locations there are other was to accomplish that. A Void return is not necessary and does not uniquely enable any interesting DSL designs (as far as I can tell). I believe similar logic applies to Never and empty-enum-returning expressions.
@discardableResut is more interesting, especially in the case of a DSL which might match some of these expressions. It would seem wrong to ignore them in this case, so they must be provided to the DSL. We could discard them if they are not recognized, but I'm concerned that this could lead to confusion and surprising behavior when they are discarded and the programmer didn't expect that. So they should probably be required to participate in the building process or be rejected by the compiler.
State
I agree that it may be useful for a DSL to be able to have a stateful builder even if the same basic design is followed. Maybe we should allow the builder to have a default initializer and use mutating instance methods instead of static methods. This would be a very minor change to the proposal - just initialize an instance of the builder and call instance methods on the instance instead of static methods on the type. If a builder didn't need state we could also support the static variant described by this proposal.
Variadic generics
I agree with previous commenters that in order for this feature to properly support eDSLs variadic generics are necessary. We need to make sure they become a near-term priority if we move forward with this approach. Novice users are not going to understand why they can't add more than 10 children in SwiftUI. Worse, the error message in this case is currently very bad and can be quite misleading.
Scoping
I agree with others that the ability for a DSL to make operators available is extremely important. DSLs aim for concision and may want to rely on names that have contextual meaning in the domain that would conflict with meaning in other domains. The inability to scope them is going to be problematic.
Control flow
I agree with others that lack of support for many control flow constructs is less than ideal. I think what I would like to see in this area is a sketch for what a fully fleshed out design would look like. Something along the lines of the Improving the UI of Generics document that was written during the opaque return type discussion. Having a more clear vision of the eventual goal will provide more confidence that this proposal is the right first step to take.
One control flow topic that has not been discussed yet is that builder function do not have a way to early exit. This seems like something that could be very useful, and indeed necessary in order to support guard (unless we force people to throw or trap).
Syntax
In the receiver closures thread I was in favor of usage site syntax. With the release of SwiftUI, I gave fresh consideration to this question. Early in the week I thought I might decide it wasn't really necessary after all, especially if it was going to be featured in the UI DSL we'll all be using for a decade or more.
Several concerns about omitting usage site syntax still remain on my mind. The semantics of the body is significantly different than ordinary Swift code. This includes basic things like what control flow constructs are available all the way to the compiler magic used to accumulate expressions into build a result. I feel that it is crucial that a reader be able to recognize the idiom even when they are not already familiar with the DSL in use (we need to remember that this will not only be used by SwiftUI).
Having spent a lot of time reading and writing SwiftUI code this week, some of my concerns have been eased. The idiom seems relatively immediately recognizable simply based on the (usually) large number of (what appear to be) unassigned results, as the proposal authors suggest. This clue does not rely on knowing the specific DSL.
However, I remain concerned that in real world code bases we will see nontrivial callback handlers and other closures intermingled with the DSL code. If this happens I think juxtaposing closure semantics without a syntactic hint could make code harder to read in general and sometimes even cause confusion, especially for less experienced developers.
I decided to look over the Landmarks example to imagine how the code would change if we adopted a usage site annotation. I was really surprised by how little weight this adds to the code. Most SwiftUI code seems to have plenty of view modifiers and often views without children. These dominate the syntactic weight.
The following snippet from the Landmarks example has been rewritten to use the @{} syntax for builder functions. Notice how the @ signals almost recede into the background, but are just noticeable enough to highlight where a new group of children starts.
var body: some View {
VStack @{
HStack @{
HikeGraph(hike: hike, path: \.elevation)
.frame(width: 50, height: 30)
.animation(nil)
VStack(alignment: .leading) @{
Text(verbatim: hike.name)
.font(.headline)
Text(verbatim: hike.distanceText)
}
Spacer()
Button(action: {
withAnimation {
self.showDetail.toggle()
}
}) @{
Image(systemName: "chevron.right.circle")
.imageScale(.large)
.rotationEffect(.degrees(showDetail ? 90 : 0))
.scaleEffect(showDetail ? 1.5 : 1)
.padding()
}
}
if showDetail {
HikeDetail(hike: hike)
.transition(transition)
}
}
}
Note: in this approach, the builder attribute would still be necessary on the parameter of the DSL operator, while the @ at the usage site indicates the change in semantics without being burdened by the syntactic weight of stating the builder type. This is very important because the user of the DSL should not need to even know what the builder type even is. That said, if we go in this direction we could additionally support @MyBuilder {} syntax for standalone usage when assigning to a value of function type, etc.
One advantage in going with this syntax is that it allows us to repurpose return for early exit from the builder. When the user writes return the compiler would add a call to buildFunction to return the result built up to that point in the body.
Another advantage to this syntax is that it would enable use of implicit return in closures that just forward along the call something else that provides a value of the DSLs return type. If a user wishes to write a normal closure with an explicit result they would just omit the @ and write a normal function, possibly with implicit return. Forwarding calls are often single-liners and are perfect candidates for this.
I'm still trying to evaluate the advantages and disadvantages of usage site annotation, but on balance the advantages still seem to outweigh the disadvantages IMO. The fact that this approach is consistent with the rest of Swift's design is a strong indicator to me that we should think more carefully before we proceed without it.
Just ran into this issue on StackOverflow, I think it's a case we should address in the final design:
var body: some View {
switch shape {
case .oneCircle:
return ZStack { // type: ZStack<ShapeView<Circle, Color>>
Circle().fill(Color.red)
}
case .twoCircles:
return ZStack { // type: ZStack<TupleView<(ShapeView<Circle, Color>, ShapeView<Circle, Color>)>>
Circle().fill(Color.green)
Circle().fill(Color.blue)
}
}
}
error: Function declares an opaque return type, but the return statements in its body do not have matching underlying types
The solution is to erase the types with AnyView
var body: some View {
switch shape {
case .oneCircle:
return AnyView(ZStack {
Circle().fill(Color.red)
})
case .twoCircles:
return AnyView(ZStack {
Circle().fill(Color.green)
Circle().fill(Color.blue)
})
}
}
I think SwiftUI establishes really good goal syntax, which lays out the requirements for how property wrappers, function builders and opaque return types should work.
...but issues Issues like this are making it increasingly evident to me that these features need refinement. Don't get me wrong, I don't want to make the perfect the enemy of the good, but we don't want to paint ourselves into a corner by rushing these out the door.
I think that these features need to be fleshed out as prerequisites:
- Generalized existentials, to handle cases like the one above without the nuisance of manual type erasure.
- A fleshed out attribute story, to turn Swift from turning into
@Adjective("enterprise") @Noun("Java"). We can't just bolt on every new feature with an attribute. - (Not immediately necessary, but be addressed pretty soon) A way of dealing with heterogeneous types without erasing their types, such as varadiac generics
I mostly agree with @anandabits with some points to raise.
This should work well
I feel like the reason we're doing stateless is for compile time checking at type-system level.
Though it hasn't been made clear what benefit that would bring.
I'm not entirely agree with this. If anything, it can be avoid by using different eDSL in different files (and that's probably a good file structure anyway). While it is a problem, I'm not sure it's as big as it's made out to be.
No strong opinion on either side here.
All of the typing capabilities of the static method approach would also work with the stateful approach I described. The approach that wonât work is the style that uses a âreceiver closureâ. In that approach the builder has to accumulate results internally, therefore preventing the capture of expression-level type information in the accumulated result. The approach I described doesnât require internal accumulation, but still allows the builder to track state during the building process if necessary.
It may not always be possible to avoid conflicts. An eDSL may not necessarily reside in a standalone module. It is easily conceivable that somebody needs to import a module that defines an eDSL (even if they donât use that eDSL) in the same file that uses a different eDSL declared in a different module. If there is overlap in the DSL operators there could be confusing error messages and the user may have to write fully-qualified names, thus defeating part of the purpose of an eDSL (conciseness).
I donât think this is a showstopper but it is something that we really should find a way to solve. An eDSL should ideally be self-contained and be able to define operators that are not in scope outside of its usage without having to be qualified.
Allowing static methods on the function builder type (or instance methods if we support stateful builders) to play this role seems like the obvious solution. I very much hope the implementation is feasible.
I see. In that case if we end up with instance methods, we probably donât need static methods. Compiler should be able to reason about non-mutating methods equally as well.
One thing I'm missing already while working with SwiftUI is support for if let.
Something like this doesn't work:
if let image = image {
Image(uiImage: image)
} else {
Image(systemName: "person.crop.circle")
}
Closure containing control flow statement cannot be used with function builder 'ViewBuilder'
You can obviously do a != nil check and force unwrap the value, but then we're not using Swift at its fullest.
Wow, I wasnât aware that if let didnât work. I assumed all forms of if were supported. Itâs way too subtle to expect users to understand limitations like this. Hopefully itâs just an issue with the current implementation and is intended to be fixed before release.
You could also solve this one with a .map { Image ($0) } ?? Image(default). That having been said, I agree with you, I would like to see if let and switch working in this.
We really need a new name for this feature. Both âfunction buildersâ and âbuilder functionsâ are confusing. When I look at a piece of SwiftUI code, I see a computed property (body) and a lot of closures. From what I understand, this is about closures progressively building their return values.
As for control flow, I think we should either allow no control flow or allow all control flow. If no control flow is allowed, SwiftUI just needs its own âIfâ type and done. But this is an important design question. Do we want to allow Swiftâs control flow or do we want DSLs to provide only what is appropriate? Either way, we need a complete story.
Lastly, clarity at the point of use should be improved. It looks like nobody knows how though. Trying to save keystrokes is a dangerous design territory. (Looking at you, âVStackâ.)
I haven't really tried the feature but will it possible to create Swift package manifest APIs that look like this?
package {
"MyPackage"
dependencies {
remote {
"https://github.com/blah"
upToNextMajor(from: "1.0.0")
}
local {
path("../bar")
}
}
products {
library("foo")
executable("exec")
}
targets {
target {
"TargetA"
}
target {
"TargetB"
dependencies {
"TargetA"
"Blah"
}
path("my/custom/path")
}
}
}
It ought to be, though a few small tweaks (like making the package name, which can only appear once, a parameter to package(âŚ)) might make it a lot easier.
Hmm
Iâm not sure what code-completion and such would look like. Iâm not entirely convinced that it would be an improvement over the current design, which clearly shows you all the options at your disposal.
For the other stuff people have mentioned (especially about control-flow), it seems to me that coroutines/generator functions would be a simple solution. SwiftUIâs special ForEach will likely remain, as I would hope it doesnât eagerly iterate your entire data-source. Perhaps it could be given a better, less eager-evaluationy-sounding name.
I wonder if coroutines were considered by the team as an alternative. Iâd be interested to hear why it couldnât be used in this case.
Even for optimisations - perhaps the receiver that iterates the generator function could check the specific type and dispatch to an optimised layout function. If that small part was inline able, it should be possible to achieve the same performance (in theory).
+1 to this line of thought. I find it especially helpful in the Button initializer, distinguishing the action anonymous function from the subview definition.
Overall, I like the proposal and understand the need for DSLs support inside Swift.
However, I think there's a need for clear syntax boundary separation in the code marking the scope where writing DSL is possible. Take for instance Swift UI: you have your struct which looks exactly like any other struct in Swift. And in fact you can write any legit Swift there. But then there's body var, which looks exactly like any other computable property, only it contains SwiftUI DSL. We know it is DSL only because we learned that from tutorials / videos.
Now consider some future DSL in a project you just cloned from GitHub. There's no obvious way to tell where Swift ends and DSL starts.
The solution would be to introduce a clear marker around DSL blocks. That will allow the reader to clearly understand that different syntactic rules apply here. What do you think?
I don't think we need explicit marker for when we enter the transforming block, but if it's needed, +1 for @{ ... }
+1 for DSLs. I still give my vote to the concern that we might overcomplicate Swift for too little return.
Is there a way at all to achieve the same thing with existing language features but uglier syntax?
As a Swift user without knowledge of building compilers, I would expect SwiftUI to look like this:
VStack { builder in
builder.add(Text("Label 1"))
builder.add(Text("Label 2"))
if weAreInAGoodMood {
builder.add(Text("Label 3"))
}
}
... or a bit more streamlined:
VStack { builder in
builder.text("Label 1")
builder.text("Label 2")
if weAreInAGoodMood {
builder.text("Label 3")
}
}
Edit: And if I understand correctly: Of the SwiftIUI features we've seen, mainly (only) the stacks build views from multiple View expressions. Lists and possible future Grids aren't even touched by function builders. So an "ugly" syntax like pictured above wouldn't even be that pervasive in practical DSL scenarios ... (?)