Function builders

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. URL Routes | Flint framework).

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)

7 Likes

I think that if Variadic Generics are implemented in Swift, Function Builders will be much more powerful than they currently are.

2 Likes

While testing @functionBuilder for a while I noticed the following behaviour in subclasses:

import Foundation

@_functionBuilder
struct ArrayBuilder {

    static func buildBlock<T>(_ components: T...) -> [T] {
        return components
    }
}

class Base {
     // OK no problems
    @ArrayBuilder func createArray() -> [Int] {
        1
        2
        3
        4
    }
}

class Derived: Base {

    // Didn't inherit the @ArrayBuilder attribute
    // so function's body is invalid
    override func createArray() -> [Int] {
        5
        6
        7
        8
    }
}

This is expected? If so why subclasses doesn't inherit the builder annotation?

This is expected? If so why subclasses doesn't inherit the builder annotation?

This is just speculation, but I imagine the builder attribute is considered an implementation detail, not a part of the function's signature. The Base interface only requires subclasses to have a method called createArray which returns an array. The subclass gets to decide whether or not it wants to fulfill that requirement with a function builder.

If Swift would provide true union types we could circumvent the variadic generics issue as follows:

@blockFunction
static div(elements:List<T where T==Div,T==Ul,T==Span>)->Div
//or
@blockFunction
static div(elements:List<Union<Div,Ul,Span>>)->Div

Alternatively we could work with protocols and implement all containing types for a protocol related to the enclosing type:

protocol DivContainment {...}

extension Div:  DivContainment {...}
extension Span: DivContainment {...}
extension Ul:   DivContainment {...}

@blockFunction
static div(elements:List<DivContainment>)->Div

If someone already mentioned it, I'm sorry.

Edit: Another point would be an automatic conversion to an enum though I don't know how this could be achieved, yet.