Function builders

Right. To be clear, I didn't mean to focus on receiver closures or any other alternative in particular—I just found it odd that you have claimed the technique used in this proposal is absolutely necessary because it allows for type propagation, but then dismissed variadic generics because most people won't need the very same type propagation that's put forward as the reason for adopting this proposal.

1 Like

It's not clear to me what this means. There's nothing really unusual about the syntax here, just that seemingly-ignored values in a closure are being collected in a particular way, which seems like semantics. There are plenty of places in Swift where things that are different semantically have indistinguishable syntax (e.g. calling pure versus impure functions, variables with value versus reference semantics), which can have downsides (confusion) and upsides (uniformity, concision).

2 Likes

Has the scenario of a function builder building a switch not into an Either but into that same enum (or something isomorphic)? I think this might enable monad-like things (eg. reading or writing some implicit state) in a clean way, but I have not thought through all the details. So it also might affect other parts of the feature (buildOptional) in undesirable ways. Anyway, I would be thrilled to hear someone else had done the hard thinking here already :slight_smile:

2 Likes

It means that I believe that stripping away the naming and looking at foo()and bar() etc. is perfectly valid for making an argument about syntax and syntax readability. Also I believe that the syntax should ideally be clear without understanding the names that some programmer (maybe me a long time ago) chose. Fair enough there is sometimes a trade-off and so a balance to strike. But in contrast to function overloading and value versus reference types, this here is a rather modern/unusual/special feature for a language that is at its core object oriented and functional. Therefor Builder Functions (or however they're gonna be called) deserve to be treated as the special case and the behaviour of these "special" closures should be very explicit in my humble opinion.

In the interest of avoiding any misunderstanding, I wanted to notify the thread that we've merged the initial implementation of this feature into the Swift repository. This does not substantively affect this discussion; it was done for technical reasons and should certainly not be interpreted as an endorsement of just releasing the status quo in Swift 5.1. I believe I've already made it clear that we expect changes to the design before release. Nonetheless, since we do expect some form of this feature to make the cut, and since keeping a large PR branch in working condition has real costs (which grow as the release grows closer), we decided (with core-team approval) that it was okay to immediately integrate it in its current working state.

21 Likes

If this is meant to be a real (open) discussion (rather than an announcement that function builders will be added to Swift), imho those advantages should be listed explicitly.
As it has been mentioned many times before, this is no democracy, and it's not required to convince "the community" - but still it wouldn't hurt to get as many people on board as possible.

8 Likes

How is that information used by ConditionalContent? Does it help in diffing Views?

The builder doesn't directly produce an Either type, it invokes buildEither overloads on the builder type, so your implementation has some control over how that gets reassembled into a result. It might nonetheless be inconvenient to reassemble it into your original enum, though, given John's scheme of producing a balanced binary tree of buildEithers to join the branches.

3 Likes

Right, I think we are following each other now, so let me go out on a limb and spell out in more detail what my line of thinking was last time:

If one is using a function builder, either to carry some data "over" the expressions and statements, such as a Reader type, or "alongside" as in the case of a Stream type like Combine, so adding complete and error cases, won't one want that Reader struct that holds the state, or that Stream enum, to be what this function builder is overloaded on, and not Either? (Or some function type involving them, anyway. Hmm.)

Anyway, thanks for your reply, maybe mine is "food for thought" for a future proposal.

3 Likes

I can't seem to get buildIf working in Xcode 11. It gives a segmentation fault (11) for me. Is something wrong in the code here?

@_functionBuilder
public struct StringBuilder {
    public static func buildBlock(_ children: String...) -> String {
        return children.joined()
    }

    public static func buildIf(_ child: String?) -> String {
        return child ?? ""
    }
}

@StringBuilder
public func combinedString(spaced: Bool) -> String {
    "test"
    if spaced {
        " "
    }
    "other"
}
1 Like

No, that looks like it should work, and it should definitely not be crashing the compiler. Mind filing a bug over at our open-source tracker?

1 Like

Thanks, opened an issue (SR-10923)

1 Like

Now we have a limit of max 10 items inside View body. The function declaration leaves something to be desired https://developer.apple.com/documentation/swiftui/viewbuilder/3278693-buildblock . Lack of variadic generics added complexity and API workaround to Combine and Function builders. I do believe all of that is going to be changed by the release.

2 Likes

When it was requested to allow extensions for structural types, such as tuples, one of the arguments for not doing it right now was that it would require variadic generics to do properly and Swift doesn't want to implement a half-baked mechanism that will need to be replaced later. Even though in practice, the length of tuples that people are going to use very probably has some reasonable upper bound (who's going to use 50-element tuples?).

Now we're talking about a feature where, without variadic generics, you can only put up to 10 items inside a view body—something that is a very real limitation in practice—but the feature is merged anyway. Yes, maybe variadic generics will happen until the release. It's more likely that they are not though, and then nobody is going to rip the feature back out again.

I find this to be weirdly inconsistent and it's one of the reasons why I wrote in the other thread that it doesn't look as if the Swift Core team cares about the fundamental issues with Swift as much as about flashy new features that can be marketed well.

8 Likes

Let's not get haughty, variadic generic is a big endeavor, which I don't see it getting done soon, even as a priority.

With that said, Swift doesn't seem to shy away from doing this kind of thing (tuple equality, hashable, comparable, etc).

2 Likes

@John_McCall I finally could catch up the proposal and this thread. So far I'm overwhelmed by the complexity of this feature. Don't get me wrong, I like it a lot, but it was and still is hard to wrap your mind around it.

@gregtitus mentioned on Slack that the compiler executes the builder functions in a depth-first-traversal way, which is not mentioned anywhere in the proposal or in this thread, but this tiny information was crucial in my learning process. Knowing this makes this feature less magical and more predictable.

Here a good example:

@_functionBuilder
struct TestBuilder {
  typealias Component = [Int]
  
  static func buildBlock(_ children: Component...) -> Component {
    let result = children.flatMap { $0 }
    print("buildBlock \(children) -> \(result)")
    return result
  }
  
  static func buildIf(_ children: Component?) -> Component {
    let result = children ?? []
    print("buildIf \(children as Any) -> \(result)")
    return result
  }
}

func testBlock(@TestBuilder makeChildren: () -> [Int]) -> [Int] {
  return makeChildren()
}

testBlock {
  if true {
    [1]
    if true {
      [2]
    }
  }
  [3]
  if true {
    [4]
    if false {
      [42]
    }
  }
  [5, 6]
}

This prints:

buildBlock [[2]] -> [2]
buildIf Optional([2]) -> [2]
buildBlock [[1], [2]] -> [1, 2]
buildIf Optional([1, 2]) -> [1, 2]
buildIf nil -> []
buildBlock [[4], []] -> [4]
buildIf Optional([4]) -> [4]
buildBlock [[1, 2], [3], [4], [5, 6]] -> [1, 2, 3, 4, 5, 6]

I'm worried about one particular thing with the current design. What if you release a module with a builder that has not specified every possible builder functions? Theoretically I as the module user can retroactively extend your builder type with custom builder functions and potentially alter the behaviour of the building behavior that can lead to unexpected results.

I hope you tracked this issue and I'd expect the compiler to emit errors on retroactively added builder functions from outside your own module boundary.

4 Likes

This is, to me, the most important point. We can work around all the limitations of the current proposal but this is a major one that will not allow function builders to go beyond the (rather simple) scope of the composite pattern: for example, any kind of context-aware computation could not be modeled with the current implementation, and that would be a gigantic miss, even for a first iteration.

3 Likes

A question, and apologies if this is already answered somewhere:

How would the builder handle Never-returning functions? Would clients need to write _ = fatalError(…) inside a builder? Or would plain old fatalError(…) work?

The latter seems in keeping with the principle that throws works like normal without builder intervention, but seems like it would require a special exception in the proposal.

An overload of buildExpression that accepts Never and does nothing should do the trick.

Ah but SwiftUI is not the language Swift, it's a framework, and it's a framework that completely transforms the way people makes apps, so I don't we should mind that involves some workarounds that can be removed in due time.