Function builders

Pretty excited about this; cool to see how it dovetails with recent proposals like return omissions.
A couple thoughts after a first read:

  • buildFunction could use a motivating example. The general concept of combining components into a single result type is logical to me, but the proposal would benefit from a demonstration of this functionality.
  • buildDo is, IMO, unjustified. If a group of components has distinct semantic meaning in some context, a named function that combines the components is a better signifier than making do blocks magical. Trailing closure syntax means there's no syntactic benefit to using do; furthermore, an ordinary do is the statement that "does" the least (just introducing new scope), and I don't think changing that interpretation in the context of a function builder is intuitive.
  • I echo the previously-expressed concerns about composition. Some way to splat an array of expressions would be nice; using for as mentioned in Future Directions seems like a natural fit. As a user, I'd be naturally inclined to write something like this:
for string in subitems {
    Text(string)
}
5 Likes

I'm not the guy you're replying to, but I'm definitely feeling this. We haven't hit Java levels of attribute abuse yet, but it seems to me that we're headed that way, fast.

I understand this attribute would be hidden in a library implementation, unbeknownst to its consumers, but I'm still not a fan.

I would love to see namespaces for attributes (e.g. @dynamic.callable, @dynamic.memberLookup, @functionBuilder.HTML, @propertyDelegate.Lazy), and a Rust-like syntax to elide repeated @ when multiple attributes are required, like:

@[ dynamic.callable, dynamic.memberLookup, functionBuilder.HTML, fooAble.bar ]
struct MyFancyStruct { }

I think it's really important for us to come up with an attribute manifesto, and figure out exactly what we want out of attribute system, and how our language evolution can progress without turning into enterprise Java.

13 Likes

Proposal suggestion: I'd prefer for there to be some way to differently/specifically handle expressions inside builder functions whose result type is Void. This would have the advantage of handling asserts and preconditions reasonably, and would obviate the need for a special rule about assignments, since assignments typecheck to Void.

Two possibilities:
(1) Expressions of Void result type should be implicitly ignored (and no Component created for them) unless the builder explicitly has a buildExpression(_ void: Void) -> Component static method.

(2) Allow for buildExpression overrides which return either Void themselves or return Component?, in which case there may be no Component for that expression in that level's buildX variadic. This is more general and let's you avoid having a (probably common) .empty case in the enum backing your DSL, but means most builders will have boilerplate buildExpression(_ void: Void) -> Void {} implementations.

4 Likes

Current Core Team thinking is that marker attributes like @dynamicCallable should just be eliminated, but you’re right that aggregating attributes in general is an interesting problem that we should be thinking about.

3 Likes

Not the original target of this question, but having the attribute namespace be filled up dynamically by previous declarations visible in the code feels ... weird. I'd prefer @builder(HTMLBuilder) simply because I expect the attributes the compiler supports to be a fixed list (for any particular compiler version).

9 Likes

Will the names for the various builders have/support qualified lookup?

Just read through the proposal and the thread. It's definitely a +1 from me.

I think there's no doubt this is useful and solves a real problem (look no further than SwiftUI).

However, I'm not a fan of the HTML use case. I prefer templated HTML pages to be HTML-like, with another language interpolated in, rather than the other way around. For one, because you can use the massive ecosystem of existing HTML WYSIWYG tools to get started with an initial HTML structure (into which you can add your dynamic bindings).

I also do not like that this proposal uses static functions for everything, it just seems needlessly restrictive. If the thinking here is that it's intended to prevent stateful behaviour, as some kind of counter-measure against abuse. I think that's futile. If people wanted stateful behaviour, they could just use thread local storage. It's just icky.

I think it's better to initialize new instances of a function builder for every "outermost block", and to call methods on that instance. This gives access to a shared state, who knows what kind of cool uses people could come up with.

Wouldn't it be funny to implement async/await using this mechanism? :rofl:

3 Likes

Runtime Validation, Early Failure Detection, Untrusted Input

This is a feature request that I think would require changes in the proposal.

Not all DSLs are like HTML or SwiftUI, which accept any number of components. Some DSLs will have constraints or will need to perform validation.

For the sake of the discussion, let's imagine a strict HTML builder which wants to enforce, for example, that a HTML document contains a single head, which itself contains a single title:

html {
    head {
        title { "I'm the title." }
        title { "Or am I?" } // We do not want to allow this
    }
    head {                   // We do not want to allow this
        title { "You shall not pass!" }
    }
}

With the current state of the proposal, validation can only happen at the end of the gathering of elements, inside the buildBlock(_ components: Component...) function and its peers.

It is possible, from this method, to raise a fatal error. But the end user will not be able to see where the erroneous code is.

So here are two ideas for further discussion. Maybe they can both enter the future directions of the pitch. Or maybe they should be considered right away:

  1. Early error detection: Augment or replace variadic methods with methods that accept elements one by one. Those methods will be able to fatalError on the first added element which fails validation.
  2. Recoverable errors: Make it possible to throw errors from @functionBuilder types, so that validating DSLs can be fed with untrusted input.
19 Likes

Just wanted to voice some opinions around this proposal:

  1. Compilation time: Swift is already slow to compile, with all the safety checks. Adding a DSL as part of the language will increase the complexity and compile time.
  2. Dynamic Behaviors: Using DSL to declare UI or such structures is now compiled and hardcoded into the binary. It would be hard to achieve a data driven approach, or indeed, yet another layer has to be built on top of this DSL.
  3. Compatibility: Needless to say, DSL written in Swift itself is not compatible with other languages. Having a DSL as an independent effort with Swift support might open it up to more languages and help isolate the responsibility and goals, keeping Swift itself focused, and other efforts independent.

IMHO, a DSL can be introduced as a side project, along with other great ideas like this. In the meantime, I would personally encourage the community to move away from writing code, and solving these problems in a more abstract manner, saving us compilation time, learning curve, and complexity.

I feel that Swift should focus on enabling developers to write great apps and tools that enable others to write less code. So, adding a DSL discourages (in a way) possible innovation to declare UI and Package Manager spec in a more user-friendly manner, not using code.

6 Likes

My principle here is that only 2 types of user defined attribute deserve to be top level:

  1. A general custom attributes feature, specifically designed for that purpose
  2. There really is no base name that is good enough in context.
    Point 1 is not relevant here, and I think @builder is good enough in
    "func div(@builder(HTMLBuilder) makeChildren: …) -> … {…}"
2 Likes

I may be mistaken (or even completely missing the point), but isn't it something that a generalized version of C++20's co_yield could solve? In that case, the Void statements and for loops are not a problem.

Yeah, we'd need to write a keyword yield before each expression statement, but I don't see it as a big of a problem, it rather indicates that something's going on under the hood. Because right now it feels a bit like magic, another Swift-unique feature to learn and understand.

Basically what I'm saying is that instead of implicitly treating expression statements as build* function calls, let's do it explicitly (using a keyword yield before each such expression) with well-defined semantics that are similar to those in other languages.

25 Likes

+1 here. Looks like way more universal solution. It would cover many everyday patterns. This is in my top2 wishes along with if/switch expressions

1 Like

I may be mistaken, but I think you misunderstand the target audience. It sounds like you are thinking more of something like Lua, used for writing plugins in a number of applications. When embedding Lua, the host app often makes available custom objects and functions to be used by the scripts.

The goal here is to write fluent libraries. To take a personal example, I wrote a small command-line argument parser, which uses a declarative style for defining the grammar. The audience is the writer of the app which needs to parse its command-line.

Here's a small sample of what a declaration looks like today:

YACLP sample
let root = Command(description: "perform operations on a Horizon node", bindTarget: cnf)
    .tagged("config", binding: \Config.path, description: "specify a configuration file [default: \(cnf.path)]")
    .tagged("cfg-funder",
            binding: \Config.funderOverride,
            description: "override the funder secret key from the configuration file")
    .tagged("cfg-whitelist",
            binding: \Config.whitelistOverride,
            description: "override the whitelist secret key from the configuration file")

    .command(Commands.keypairs, description: "create keypairs for use by other commands") {
        $0
            .tagged("output", binding: \Config.file, description: "specify an output file [default \(cnf.file)]")
            .optional("amount", type: .int(nil), binding: \Config.amount)
    }
}

It's not too hard to imagine how the syntax could be cleaned up by eliminating the need for closure parameters, for example.

1 Like

Unfortunately, no.
IIUC the proposal haven’t yet included the for-in construct support.
You’ll need to create your own for-in function that return an array of HTML, then add buildExpression(_: [HTML]) -> Component

Something like SwiftUI ForEach.

1 Like

Thank you @Lantua

So for loops are not supported out of the box. And map (as asked by @mdiep above) is not supported either.

That's... pretty tough for DSL consumers. And that's pretty tough for DSL designers too, because nobody can say replacing native language loops with a ForEach is very sexy.

14 Likes

Yea, it’s in Future Direction though :wink:, so I suppose there’s some more groundwork needed to make that possible.

1 Like

Thanks for your answer.

Good. Nice to know that, :). Quick question: So basically SwiftUI is not the real representation of this pitch?, but SwiftUI will benefit from this proposal itself. That's what I understood with your explanation, am I right?

One thing that I didn't understand is if the DSL will be available globally to be used wherever we want or it will have some specific scope define by a method, extension or type

There are two alternatives which I would really like to see being discussed in the Alternatives section:

Kotlin uses another builder pattern to model eDSLs like the HTML example (https://kotlinlang.org/docs/reference/type-safe-builders.html). Strangely they have not even been mentioned in the draft proposal.
The idea there is that div for example is a simple function with a closure parameter that is called on a receiver being supplied by div. The other elements h1, p etc. are then methods of the receiver object which solves gathering the results quite nicely.

Monads have actually been mentioned shortly in the draft proposal ("Function builders have no ability to interact with local bindings and are therefore substantially less general than what you can do with, say, monads in Haskell. Some specific monad-like use cases could be supported by allowing function builders to carry local state [...]") but they have not been discussed in the Alternatives section.

25 Likes

If anyone is interested in playing around with function builders and the HTML DSL, I've posted the implementation underlying the proposal. It's slightly different from what the proposal shows because the proposal opted for simplicity.

8 Likes