Function builders

I am thinking about how to support for loops in function builders.
Was something like the following already considered?

func collect(_ f: @Wrapper () -> WrappedResult) { ... }
collect {
    a()
    for elem in [1, 2, 3, 4] {
        b(elem)
    }
    c()
}

could be transformed to:

collect {
    var _wrapper = Wrapper()
    _wrapper.expressionWithIgnoredResult(a())
    for elem in [1, 2, 3, 4] {
        _wrapper.expressionWithIgnoredResult(b())
    }
    _wrapper.expressionWithIgnoredResult(c())
    return _wrapper.finish()
}

That is, instead of static methods, just use normal methods and always create some instance of the Wrapper. This wrapper instance would have to collect all the results, instead of all of the individual variable assignments in the original proposal. In this model, support for if or for would be really straightforward, as they simply lead to some methods being called conditionally or within the loop.
If more detailed knowledge over the structure of the wrapped function is necessary, then we could introduce even more methods which would be inserted into the function (e.g. beginConditional / endConditional / beginLoop / endLoop or whatever we'd need).

3 Likes

This bug will be fixed in the 5.1 release (https://github.com/apple/swift/pull/25944).

Note that unfortunately function builders will remain a private feature in 5.1; we'll have to maintain compatibility with existing code that uses the @_functionBuilder attribute, but we can still pursue alternative designs for the un-underscored feature.

5 Likes

:smiley:

Is this a sufficiently static feature that SwiftUI could eventually adopt the final version without ABI impact as long as all of the relevant symbols the old @_functionBuilder implementation used remain available?

1 Like

It needs cooperation from the library implementation, but yes, we’ve intentionally done that. You would just need a new SDK that exposes the new interface, but it should deploy back.

If the un-underscored feature would re-shape differently than SwiftUI builder, how should we learn how SwiftUI builds everything if this feature would remain private?

It feels strange to have a hidden DSL in the language that is used only by Apple frameworks and the documentation for its behavior might be not fully publicly available.


To be clear with you. I understand that this decision was likely made due to a short time frame for everything and because this is a critical feature that can't be changed to drastically as it will break SwiftUI over and over. However if @functionBuilder and @_functionBuilder will be very different in the end, then we're about to create a language dialect here which is quite disappointing in my opinion.

3 Likes

I’m not sure what difference you expect there to be that would amount to a “dialect”. The future SwiftUI DSL will be the current SwiftUI DSL but without as many restrictions, and it will deploy backwards, so remembering the restrictions (and differences like buildOptional vs. buildIf) will be pointless.

2 Likes

Just out of curiosity, where are we on this proposal at the moment? Still waiting for more feedback and/or community consensus? Incorporating existing feedback into the draft? Putting it on the "back burner" for a bit in order to prioritize other proposals like Property Wrappers?

4 Likes

I need to incorporate feedback into the draft, but mostly I need to work on the implementation so that it matches the proposal.

2 Likes

Forgive me if I'm misunderstanding how the Swift Evolution process works, but if you are going to all the trouble of making the implementation match the current proposal, wouldn't that imply that the proposal isn't going to change much further (if at all)? Or is the plan to put it up for a second review once a full implementation is complete?

It hasn’t gone up for any review; this is a pitch thread.

1 Like

Ahhh, got it—that was the source of my confusion. Thanks! :+1:

As of today's 5.1 build, this example is mostly working:

@_functionBuilder
public struct ActionReducerBuilder {

   public static func buildBlock() -> Cancellable {
       MultiReducer(children: [])
   }

   public static func buildBlock(_ content: Cancellable) -> Cancellable {
       content
   }

   public static func buildBlock(_ content: Cancellable...) -> Cancellable {
       MultiReducer(children: content)
   }
}

Where can be used for example by a struct that gathers those Cancellables:

public struct ReducerGroup: Group {

    public let cancellableBag = CancellableBag()

    init(@ActionReducerBuilder builder: () -> Cancellable) {
        let cancellable = builder()
        cancellable.cancelled(by: cancellableBag)
    }
}

I omit the following Reducer implementation because of being out of the conversation scope:

   override var reducerGroup: ReducerGroup {
        ReducerGroup {
            Reducer(of: OneTestAction.self, on: self.dispatcher) { _ in
                self.changes += 1
            }
        }
    }

While that is working fine for 1 or more Reducers, I cannot compile:

   override var reducerGroup: ReducerGroup {
        ReducerGroup { }
    }

With the error: Cannot convert value of type '() -> ()' to expected argument type '() -> Cancellable'

Maybe I'm doing something wrong or the implementation is not finished in the first merged PR. Any feedback is appreciated.

It's a known limitation of the current implementation that is too risky to try to fix in 5.1 because it would require changes to how closures are type-checked even when they aren't function builders.

5 Likes

So there won't be capability of implementing empty blocks atm and on the final 5.1 implementation I assume. Moreover, I'd like to know how far the 5.1 implementation will be in this topic, will it mimic what @ViewBuilder is or will be different for what the OOS community will create.

Function builders are a private feature in Swift 5.1; defining your own function-builder types using this private feature is allowed but unsupported, in the same way that it's allowed but unsupported to use underscored attributes like @_fixedLayout. Among other things, this means that future releases of Swift may not interpret this private feature in exactly the same way, although there are limits to how much this can change because (at least in the short term) those releases will need to support SwiftUI code building with the SDKs from Xcode 11, which only provide function builders using the private feature.

The Core Team is committed to providing a public, evolution-approved feature as soon as we can.

8 Likes

Thank you for the response! That public specification is not aiming 5.1 release (which I assume It'll be on fall, when Xcode 11 goes live)?

It was clear even when we were implementing function builders internally that the minimum feature necessary to support SwiftUI in Xcode 11 wasn't going to do all of what we wanted for Swift long-term, which is why this pitch is substantially different from what was implemented. Then I made this pitch and got a lot of great feedback, and it became totally obvious that we weren't going to have the opportunity to iterate on it properly in Swift 5.1. Property wrappers should give you a good idea of much time and effort real design iteration can take.

We plan to restart that iterative design process soon, before 5.1 is released, but there's no chance it will be ready in 5.1. I don't know and shouldn't speculate about when exactly Swift 5.1 and Xcode 11 will be released.

16 Likes

Is there any way to track the availability of various pieces of this implementation?

For instance, buildExpression doesn't seem to be working in Xcode 11 Beta 3. It would be nice if there was a summary whether or not some piece of the implementation is present in a Swift branch, and some idea of how long until this is available to experiment with.

It may be little too late, but I think there is a better solution that is simpler and not limited to just builder functions.

see: Function environment parameters

I'm glad the decision was made to make this a private feature for now. I strongly support the need for DSL s in Swift, but share a lot of others' concerns.

Most of my experience with using and creating many DSLs in the past was with a dynamic language, Groovy, which at least when I was doing it would typically leverage the language's support for closure delegates at runtime, where closures are first-class objects and a delegate can be set to any instance of anything before calling the closure. There are a lot of issues with that of course, as it is all about dynamic property and method resolution.

I do currently ship a Swift open source framework that has some simple builders using "clunky" builder instances passed to closures (e.g. https://flint.tools/manual/guides/routes#declaring-the-url-routes-for-your-features-actions).

Obviously SwiftUI is my only real contact point with this feature and I understand from this thread that SwiftUI has special needs re: generic function signatures and these should be resolved in the fullness of time with variadic generics.

Those current limitations/sharp edges that are present in SwiftUI re: number of expressions capped at 10 and oblique side effects of that (one can imagine View bodies with multiple if statements causing major confusion here in particular)... I personally find in the context of SwiftUI to be really offensive – particularly because of the lack of reasonable error reporting.

This is actually the topic that gives me the greatest concern. I do not think we(*) can add function builders for DSLs as a public feature to Swift until we have a 100% solid strategy for clear and unambiguous error reporting at compile time.

With e.g. a Groovy DSL that is based on property and function access in the current closure scope being resolved on an instance of a "builder" (the closure delegate), while the problems are only found at runtime, they would typically be fairly unambiguous — along the lines of "No function called 'html' found".

As I understand it, being type-based the current proposal can only spit out errors about missing types and builder argument type mismatches which are not at all conducive to regular developers who want to use a DSL created by somebody (like me) as a quick and error-reducing shortcut to achieving something. The moment they spin off into type system errors the language feature has failed them.

If there is no reasonable way for errors in function builder closure bodies to be clearly reported as errors in the context of the DSL (not in the context of the Swift Type system) I will have to say -1 to this kind of proposal and instead advocate for an approach where functions and properties are used instead of types and resolved against an instance of a builder.

The rationale is that autocomplete and basic compiler error reporting works in this case. Problems are manifest in terms of the DSL:

var body: some View {
    list {
       text("Hello")
       thisDoesNotExist()
    }
}

If these functions resolve against a builder instance, they can still be verified at compile time, autocomplete suggestions can still work, and missing functions can result in simple errors like:

The ViewBuilder DSL does not have a function thisDoesNotExist() did you meant thisOtherOne()?

I have a very strong fear response that we will end up with impenetrable compiler/type system errors and broken autocomplete suggestions for many years to come and hobble the success of SwiftUI and other frameworks that provide DSLs as a result. It will have a reputation of "Ooooh no we don't use that as nobody can debug it".

Which is almost the very opposite of why DSLs exist.

*: who am I kidding, I don't understand enough of the compiler stuff to contribute)

5 Likes
Terms of Service

Privacy Policy

Cookie Policy