Function builders

Why can't it be generic buildExpression<V: View> though?

I think I'm understand that problem being addressed. My first approach to something like a hard coded - hierarchical tree data structure is use enum types, something like:

enum HTMLNode {
    case div([HTMLNode])
    case p(String)
    ....
}

The missing part here is the flexibility showed in the example with that if statement in the builders body. So maybe we are not talking about hardcoded data structures but the opposite?

Thinking from here I believe this is an interesting approach but there are a couple of things I don't feel comfortable with:

  • The type declaration of the builders didn't match what I would expect as an API consumer, right? If I call p() inside a builder body it changes his behavior compared to call it on another context.
  • Feels like inside a builder things are slightly different than the rest of my program. Why don't just pass a builder as an argument a make some append calls instead of do the exact same thing in my back?
1 Like

I would say that it's better for structures even if they're fully hardcoded, but yes, the flexibility really shines with structures that are only partially hardcoded.

This is addressed in the proposal.

1 Like

Swift's current generics can't express a function that takes different views for each of its arguments; that's variadic arguments. You can do it if you're willing to erase type differences, but SwiftUI doesn't want to do that. Like I said, it's not a long-term restriction; this probably increases the pressure to do variadic generics sooner.

19 Likes

That's what I missed, got it.

4 Likes

It is clear that this is the alternative to SE-0257 which I was opposed to. I am in favor of this change but can you add why ExpressibleBy... type of protocol was not adopted for this use case? My biggest reservation on the current proposal is that if statements look as if they are expressions.

2 Likes

I really don't like this. It looks like a totally different language (which I guess is kind of the point... but ugh), and I find it very difficult to read. I think we're trying to shoehorn too many things in to this language.

There's large parts of the proposal which I just flat-out disagree with, like this part:

  • Third, how recognizable is the DSL from the use site? Or, balancing this with the divergence consideration above, What's the expected harm introduced by the DSL for an unsuspecting programmer who needs to understand a use site? In this case, it seems likely that a programmer would recognize that something strange is happening from the large number of statements which normally would have no purpose. Furthermore, if they're at all familiar with common HTML tags, they're likely to immediately grasp what the code must be doing even if they don't understand how it's doing it. In other words, the code in the DSL is quite visually distinctive, as well as fairly suggestive of its behavior. The expected harm is low.

IMO, this falls in the uncanny valley of looking like invalid code (just instantiating a bunch of objects that go nowhere), but actually being valid. I'm very uncomfortable with it. Compare it to, say, a String literal, the contents of which typically don't look like valid code, and indeed are not parsed as code by the compiler or highlighted as code by an IDE.

This seems like a massive, massive amount of harm just to avoid some punctuation characters.

23 Likes

The translation is far more complex than could reasonably be expressed in a literal protocol, and we wanted the result to be an ordinary function.

2 Likes

I'm still processing the pitch, but I just want to say that I've wanted a feature like this for a while, and I'm really excited about it!

5 Likes

This is phenomenal work! It manages to introduce a principled way of creating new DSLs such that Swift can have something like JSX without it being an entirely separate language. SwiftPM manifests could probably have (still could?) benefitted from this feature.

Question: What was the thought behind supporting special treatment of do blocks and buildDo? Clearly, the authors have some use case in mind but I don't know that I understand.

4 Likes

I’m not sure how important that is. I think the idea was that do naturally provides a kind of grouping, which might be relevant for some DSLs, but on balance I’m not sure expressing grouping through do is very fluent compared to calling some sort of group function provided with the DSL.

1 Like

This is a lot to take in, but I have one comment and one question while I digest it.

Comment: I found the Detailed design section to be difficult to understand. I find myself referring back to proposals somewhat regularly, so I'd love to see some more work here to make it easy to understand.

Question: How does this affect composition?

i.e. let's take this code:

div {
  if useChapterTitles {
    h1(chapter + "1. Loomings.")
  }
  p {
    "Call me Ishmael. Some years ago"
  }
  p {
    "There is now your insular city"
  }
}

A natural thing might be to pull out a function that takes 2 strings to create 2 paragraphs:

div {
  if useChapterTitles {
    h1(chapter + "1. Loomings.")
  }
  pp("Call me Ishmael. Some years ago", "There is now your insular city")
}

// Okay, this is a bit contrived. But it hopefully gets the point across.
func pp(_: String, _: String) -> [HTML] { … }

Is that possible? I don't see where an array of HTMLs can be added in this way.

What if I wanted to use map instead?

div {
  if useChapterTitles {
    h1(chapter.title)
  }

  chapter.paragraphs.map(p)
}
11 Likes

The HTMLBuilder design in the proposal is really just for exposition, but you're right that the design as given has a problem with composition, and I agree that it would be good to allow multiple elements to be expanded in the parent. Our design choice as the author of HTMLBuilder is between two options:

  • If a statement should be able to just produce a collection of HTML, then we can allow that by just giving an overload of buildExpression that takes an [HTML] (or maybe generically any Collection where Element == HTML).
  • Alternatively, we can require that to be more explicit by, say, defining a function like func include(_ elements: [HTML]) and having it return some private-to-the-DSL type that will be recognized by buildExpression and then unwrapped.
2 Likes

(Moderator note: with the author's permission, I've moved this post here as it poses only technical questions.)

i read through the rationale in your draft proposal, and it just doesn't feel like the rationale explains the choice of solution.

The first problem is that there's a lot of punctuation here: commas, parentheses, and brackets. This is a pretty superficial problem, and it's probably not a showstopper by itself, but it is something that it'd be nice to avoid, because it does distract a little from the content.

i don't think this counts as a problem. maybe half a problem?

The second problem is that, because we're using array literals for the children, the type-checker is going to require the elements to have a homogeneous type. That's fine for our HTML example, but it's limiting in general, because some trees are more generic and would benefit from propagating the types of the children into the type of the node. For example, SwiftUI uses this for various optimizations within the view hierarchy.

the proposed solution doesn't address this problem. without variadic generics, the elements still need to have homogenous type. if the workaround is this, i don't want it.

The biggest problem, though, is that it's awkward to change the structure of this hierarchy. That's fine if our hierarchy is just the static contents of Moby Dick , but in reality, we're probably generating HTML that should vary significantly based on dynamic information.

the most straightforward answer to this is conditional element inclusion, not function building. it would be a step further from the original compile-time inclusion idea, but you could use it outside of a function building API, so it would benefit the whole language.

12 Likes

I second the question about buildDo. The only use in the proposal is in the Future Directions section. I presume there's maybe some current use in SwiftUI, but it would be helpful to have an example or explanation in the proposal for why it exists and buildBlock isn't always sufficient.

3 Likes

What would need to change if/when we added variadic generics? If this feature is ‘simply’ augmented by variadic generics, I don’t see much of a problem

6 Likes

My first thoughts are:

  • @HTMLBuilder in
    "func div(@HTMLBuilder makeChildren: …) -> … {…}"
    I think it should be @builder(HTMLBuilder)

  • build.* as static functions?
    If they were instance functions and an instance was created at the start and modified throughout, conditionals, loops, and break etc. would naturally work.
    It would also be more similar to SE-0228.

7 Likes

100% agree with your arguments, if no variadics, or some kind of ruby's blocks + metaprogramming this will harm the lang itself with people trying to replicated this thing: Apple Developer Documentation will be awful, IMHO. I think rather than introducing DSLs based on something so horrendous as the buildBlock(_:_:_:_:_:_:_:_:_:_:) func, we should relay in something like subscripts, or even Nick Lockwood's layout approach: GitHub - nicklockwood/layout: A declarative UI framework for iOS.

1 Like

I had a similar though but prefer the syntax @builder(HTML) since it working with a closure of: () -> [HTML]

1 Like

Well, as shown HTMLBuilder is the 'builder' type, so @builder(HTMLBuilder) is the minimal change.
But I agree that it would be better if HTML were it's own 'builder', with @builder(HTML).

2 Likes