Function builders

As with all pyramids of doom, also in SwiftUI it's good idea to isolate and componentize frequently, it's explicitly even stated in WWDC session videos.

However, if you don't want to create entirely new struct for your sub-component, you can also in some cases just create new sibling properties to body in the struct itself, and use them inside body, which pretty much is what your examples seem to want:

struct Test: View {

  let stack = VStack {
    Text("Foo")
    //...
  }

  var body: some View {
    VStack {
      //...
      stack
        .padding()
    }
  }
}
2 Likes

Ok. If you think we can get full control flow (including guard and switch) in a reasonable time frame with the current proposal, then I trust you.

I have run into quite a few problems in playing with SwiftUI due to it's reliance on _internalTypes (especially _modified) and protocols with associated types. For example, I can't clip to a shape which has been sized. I expect these rough edges will work themselves out by the time it comes out of beta. I'm ok with static being the default as long as dynamic is still possible at launch. Right now, I have had to use ugly hacks to get dynamic behavior (e.g. allowing the user of a custom view to set either a color or gradient fill)... but I could be doing it wrong since I am still learning how SwiftUI works.

1 Like

In the WWDC session on creating custom views in SwiftUI they had a compelling usage of this type information. The example was creating a dynamic pie chart, where segments (which were represented by a custom view type) were mapped to live data. Once enough data points were added, things started to get laggy, as the overhead of so many views started to show. However, the segment custom view type was a particular type of view that basically meant it was a graphics primitive. In the demo, they changed the type of the pie chart view to be a ShapeView which enabled SwiftUI to generate a custom Metal view for the pie chart. All of the declared data mappings and animations were preserved, but all of the lag simply disappeared. To me this was a clear demonstration of the power of this approach to handling declarative APIs, and also made me think that someone on the SwiftUI team has read Leland Wilkinson's "The Grammar of Graphics". I'm very excited about this feature.

4 Likes

Is there any reason we don’t treat guard like a bigggg if-else?

Thank you, but I didn‘t asked for alternative solutions as they are trivial, and that‘s not what I want. First I mentioned that I don’t want to create a new type for every reusability case (that’s what wwdc suggests "to type creep"), nor do I want to create the reusable parts as type members which does not make them lazy/computed. Furthermore the example was extremely simplified, but it may want to capture other local state that should also be reused. Last but not least, such alternative solutions do not improve or generalize the design of the feature discussed in this thread.

1 Like

Can we get some parity between Function Builders and Property Wrappers and how the basis of these can be exposed as custom attributes?

There is an umbrella of functionality being introduced to support SwiftUI that would be really great if it was also holistically applied to user defined attributes or at least consider how these features could be bubbled up.

1 Like

Do you actually get this indentation? This is what I would like, but for me the end brace is indented an extra step.

1 Like

From my perspective, the "umbrella of functionality" that's being introduced is exactly this concept of user-defined attributes via marked types that we've been developing across several different pitches, starting with the static-attributes pitch you yourself linked. As a result, I'm not really sure what you feel isn't being "holistically applied" or where you feel there isn't "parity". Are you asking for a general proposal for attributes, divorced from any specific language feature that would use them, just to lay out the basic rules? Are you just asking for someone to work on a specific application of the concept, perhaps the static attributes pitch or a dynamically-available variant of it? Or are you concerned that somehow the concept will be less applicable to things like static attributes than it is to, say, property wrappers?

1 Like

I was thinking specifically about the idea of function decorators (like python) and how that would compose with something like function builders.

I was thinking the different static functions that are required for Function Builders and how those requirements (that are not necessarily attached to a protocol) will be defined in used defined static-attributes

No?. You are right in saying that static attributes is sort of the umbrella here. Maybe that would necessitate different static-attributes per var, func, etc. So that instead of having one way of declaring static attributes, one would need to decide what sort of things that attributes supports (var, let, func)

yeap that is it. In my mind, in a perfect world static-attributes should provide the means to specify a way to compose but after thinking a little longer I don't think that is the case.

1 Like

I think the important point here is that all of those questions are about the language design of static attributes and not somehow general to all uses of user-defined attributes.

I very like this addition to Swift, but I think, as some others already mentioned above, there should be some visible difference between a normal closure and a function builder (builder function? result builder? whatever).

Therefore I quite like the suggestion by @anandabits, to add some lightweight annotation in front of every function builder closure, like for example an @.

Especially with SwiftUI, where there is a mix between normal closures (e.g. Button actions) and the function builders it could be very helpful.

Also there could be further advantages to this system. We could have variables containing such a function builder (this would probably need a different function type as well, something like @BuilderType () -> ReturnType), which could be passed to functions expecting a function builder type.
I imagine something like this:

func div(_ makeChildren: @HTMLBuilder () -> [HTML]) -> HTMLNode { ... }
func h1(_ makeChildren: @HTMLBuilder () -> [HTML]) -> HTMLNode { ... }
func p(_ makeChildren: @HTMLBuilder () -> [HTML]) -> HTMLNode { ... }

func chapter(_ number: Int) -> @HTMLBuilder () -> [HTML] {
    return @{
        "Chapter \(number)."
    }
}

div @{
    h1(chapter(1))
    p @{
        "Call me Ishmael. Some years ago"
    }
    p @{
        "There is now your insular city"
    }
    
    h1(chapter(2))
    p @{
        "I stuffed a shirt or two"
    }
}

Of course you could also type annotate the builder like this:

let builder = @HTMLBuilder {
    "Foo"
    "Bar"
}
// builder is of type `@HTMLBuilder () -> [HTML]`
2 Likes

Having had some days to get familiar with SwiftUI, I don’t see the benefits of the magic part in DSL , i.e. Implicitly collecting types instead of explicit return on the ”closure”.

Now we have ”if” statements that only work sometimes, have to use forEach instead of normal for, no guard, issues with let/var, workarounds with AnyView()... And still sometimes you end up having to use explicit return anyway.

Instead of all that non-standard stuff, it would be much nicer to stick with the normal statements and use always explicit return (a single value or tuple or array). Maybe then the typechecker would also be sane again instead of complaining about errors in unrelated areas of valid code...

I do love what SwiftUI does, but there’s no need for the inscrutable magic in it. And ideally would love to see function builders to have less magic in them.

12 Likes

Kinda curious, what kind of design are you thinking about?

If you look up-thread, examples of what would be required to make SwiftUI manually have been posted. It's not pretty. You're trading a far more complex usage site (a Swift antipattern) for more flexibility (fixable with builders in the future) and the possibility (but not guarantee) of better error messages. I agree that much more work should be put into diagnostics here ASAP, but you'd make the pattern generally harder to use without the builders at all.

2 Likes

This is an example of the AnyView / explicit return "mess" I was inferring to by Matt (not me) from this gist:
https://gist.github.com/mattgallagher/eaa5d3242d83360a52c45c9706479e34

struct ContentView: View {
	@ObjectBinding var step: PassthroughBindable<UInt64>
	var body: some View {
		GeometryReader { proxy in
			ZStack(alignment: .topLeading) { 
				Spacer()
				ForEach(0..<circleCount) { index -> AnyView in
...
					let rect = randomRect(in: CGRect(origin: .zero, size: proxy.size))
					return AnyView(StrokedShape(shape: Circle(), style: StrokeStyle())
...
					)
				}
			}...
		}
	}
}

What would be nicer is to have the DSL "closure" be just regular closure with regular behaviour, and then return the "builder" stuff as the return value, so:

var body: some View {
       guard let value = someOptional else { return nil }
   
       // do some regular stuff
  
       let headerView = value.headerTitle ? HeaderView(value.headerTitle) : nil

      return ( // tuple or array
          headerView,
          ContentView(value)
     )
}

Don't know how implementable that would be, but can't be harder that current thing, right?

1 Like

Unfortunately it'd still suffer from the same problem that current function builder has (minus the magic part).

With the (hard?) requirement that the design must allow for type propagation, most of the elements inside the closure can't go through branching (if/else/for) without being awkward.

var body some View {
    let branchingResult: Either<Expression1, Expression2>
    // Note on the `Either` type

    if condition {
        branchingResult = .first(expression1)
    } else {
        branchingResult = .second(expression2)
    }

    return (branchingResult) // Now we have `SomeView<Either<Expression1, Expression2>>`
}

The problem arises when expression1 and expression2 doesn't have the same type. Swift is strongly typed, so that's not gonna fly. What we can do is to introduce the Either type. Things get ugly really quick when you want to include nested function, esp. now that you need to specify the type yourself.

The problem with branching doesn't lie within the magic of gathering (see what I did there? :-) expressions, but rather the requirement of type propagation. Though it doesn't seem to be long-term problem; we could potentially add support for looping variable assignment over time.

7 Likes

Wow, was that reference to MtG? Never thought to see such in swift forums :joy: Anyways, good point about branching + strong types being the real issue here...

2 Likes

Just thinking out loud here, but the issue with branching and strong types also seems to be present with opaque result types:

protocol P { }
extension Int : P { }
extension String : P { }

func f2(i: Int) -> some P { // ok: both returns produce Int
  if i > 10 { return i }
  return 0
}

func f2(flip: Bool) -> some P {
  if flip { return 17 }
  return "a string" // error: different return types Int and String
}

As with the builder/gatherer DSL function syntax proposed here, opaque result types have difficulty handling situations where branching paths have different return types.

1 Like

If anything, both features do respond to the same underlying need; to easily propagate type information.

You know, it just occurred to me that opaque types could be used to simplify the signatures of the ViewBuilder.buildBlock methods. i.e. instead of this:

static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(
  _ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4,
  _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8, _ c9: C9
) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)>
where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View,
      C5 : View, C6 : View, C7 : View, C8 : View, C9 : View

you could have this:

static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(
  _ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4,
  _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8, _ c9: C9
) -> some TupleView
where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View,
      C5 : View, C6 : View, C7 : View, C8 : View, C9 : View

Hmm…maybe that's not quite as much of an improvement as I thought it would be.