Function builders

That's correct. We still intend to generalize the function-builder transform to cover more kinds of statement, including local declarations, but we didn't have time to get that done in the 5.1 release.

Note that this is somewhat independent of putting function builders through the evolution process to make them a publicly-supported feature.

8 Likes

Is there a way to make compiler dump the built AST? I'm trying to write a function builder, but getting type errors, and I'm struggling to see where do they come from.

You can invoke swiftc -frontend -dump-ast <path_to_swift_file> -sdk $(xcrun --show-sdk-path)

3 Likes

This is getting kind of frozen, are there updates from the side of the core team on making a formal proposal to review and enter in the design and review process? Dates?

3 Likes

Curious if it's delayed till after 5.2. I'm hoping the New Diagnostics will have a meaningful improvement on end-user UX. Given that work's being done in a world where Function Builders do/will exist, debugging and Type inference around writing/using them might actually be useful! :smile:

I hope that diagnosis system can be used later on future open source implementations or will stick into refine private APIs

I realize I'm extremely late to this discussion and haven't poured through all previous posts, but I have a practical concern with a real-world function builder implementation at the moment that I want to make sure is voiced somewhere as this feature is still under development.

Imagine the following code where, similar to SwiftUI's View.body, steps is evaluated repeatedly whenever state changes. But unlike SwiftUI, one of the arguments provided to buildBlock causes the remainder of arguments to cease evaluating. This is simple enough to emulate by passing escaping closures into each Step except in the case of buildIf or buildEither.

var steps: Step {
   Steps {
      Step1()
      BreakOrContinueStep() // This step implies the remaining ones may not be evaluated
      if sideEffect() { // Bad: the constructed builder evaluates sideEffect() immediately
         Step2a()
      } else {
         Step2b()
      }
      Step3(sideEffect())  // OK: Step3 takes @autoclosure argument
   }
}

Now, this is somewhat mitigated by making buildBlock take @autoclosure arguments. However, that has some drawbacks:

  1. You cannot have a variadic @autoclosure argument. Multiple versions of buildBlock must be created to support multi-step results.
  2. The steps cannot provide unique pre-execution information, such as a 'weight' for progress reporting.

Some minor adjustments that would resolve the issue:

  1. Adjust the conditional build function signatures to grant more control. Ex: static func buildIf(_ condition: Bool, _ block: Block) -> Block. To achieve the existing implementation would then require the arguments both be autoclosures.
  2. Allow automatic wrapping of build function results such that buildIf remains unchanged but a developer can intercept the result . So, the existing signature wouldn't change but buildIf might return ConditionalBlock which is then recognized as a parameter to buildBlock(_ @autoclosure () -> ConditionalBlock) -> Block which is passed to buildBlock(_ blocks: Block...) -> Block. This might fit in with buildExpression?
1 Like

Making one of the expressions "short-circuit" the builder without any control flow and therefore no hint to the reader is not really something we want to encourage or support.

2 Likes

If we were concerned about the reader, why allow custom DSLs in the first place? This seems very arbitrary to me, and limits the creative potential of the feature. We trust developers with overloading arithmetic operators, after all, and that hasn't caused mass chaos.

Your very first post hails this feature as a hallmark of declarative programming. I'm only suggesting we have the right tools to make control flow constructs declarative. The current implementation interpolates Swift's idea of control flow constructs into the DSL, rather than allowing a more robust declarative DSL.

We do indeed discourage people from overloading custom operators in a way that would behave unexpectedly for programmers. If you e.g. had a matrix library that made * skip the evaluation of the RHS if the LHS was zero, I would tell you that that's unwise.

Function builders are intentionally not an unrestricted DSL feature. If we wanted them to be an unrestricted DSL feature, they would trigger some sort of user-provided term-rewriting metaprogram. I'm sure some people would prefer if they did, but our sense is that that sort of feature is more often abused than not. Instead, function builders are intended to address a very narrow but useful class of embedded DSLs by making it easy to collect a sequence of (possibly-conditional) values during the otherwise-normal execution of a function. It is very much intentional that the function builder transform does not alter the ordinary control flow of the function.

6 Likes

I respect that you have considered this much more than I have, and I can now see that control flow within the function builder is expected to be more analogous to #if-like behavior.

At the risk of looking silly, below is a more concrete example of what I am trying to accomplish. I don't think there's a risk of readability - to the contrary, it avoids unnecessary nesting like an async/await feature would, but with the added benefits of being a stricter API contract.

struct RecordAliases: Widget {
	let id: Int
	let type: String

	@Dependency
	var name: String

	@Dependency
	var aliasIDs: [Int]

	@Dependency
	var aliasNames: [String]

	var result: WidgetResult {
		WidgetResult {
			if self.type == "user" {
				Require($name, Database.users[self.id].name)
			} else if self.type == "customer" {
				Require($name, Database.customers[self.id].name)
			} else {
				Failure("Unexpected id type")
			}
			
			Partial(
				AliasResult(
					title: "aliases for \(self.name) [\(self.id)]"
				)
			)
         
			if self.type == "user" {
				Require($aliasIDs, Database.users[self.id].aliases)
			} else {
				Require($aliasIDs, Database.customers[self.id].aliases)
			}
			
			if self.aliasIDs.count == 0 {
				Success(
					AliasResult(
						title: "aliases for \(self.name) [\(self.id)]",
						aliases: [],
						message: "No aliases found"
					)
				)
			} else {
				Require($aliasNames, Database.aliases[self.aliasIDs].name)

				Success(
					AliasResult(
						title: "aliases for \(self.name) [\(self.id)]",
						aliases: zip(self.aliasIDs, self.aliasNames).map { (id: $0, name: $1) }
					)
				)
			}
		}
	}
}

In other words, something kind of like a REST API framework. I realize and accept my use cases aren't the typical ones - just wanted to make sure this perspective is voiced appropriately. :slight_smile:

As a passing thought because this thread was bumped:
Although I don't think it is possible in the current function builders implementation, ideally it would be possible to create a function builder which transparently acts as if there wasn't a function builder.

Although such a builder wouldn't be valuable for itself, it would show that any restrictions other builders place upon the syntax are purely because they are intentional for the created DSL.

@functionBuilder
struct NotABuilder { /* … */}

func foo<R>(@NotABuilder do x: () -> R) -> R { x() }

func main() {
  foo {
    // arbitrary valid swift closure body here,
    // doing exactly what it would do in a normal closure.
  }
}
1 Like

This is probably a stupid question:

I noticed that if let doesn't work in SwiftUI. Because I'm not sure how much of SwiftUI's declarative syntax is achieved through function builders, I wonder if it's just a SwiftUI thing, or if if let won't be supported by function builders in general?

It’s a limitation of the currently shipping, non final version of function builders. As you can read in this thread, Apple is working to overcome many of these limitations in the next version.

5 Likes

I have problem with compilation when there is only one value in builder body

import UIKit
@_functionBuilder
struct AttributedStringBuilder {    
    static func buildBlock(_ components: String...) -> NSAttributedString {
        let result = NSMutableAttributedString(string: "")
        components.forEach { result.append(NSAttributedString(string: $0))}
        return result
    }
    
    static func buildBlock(_ sol: String) -> NSAttributedString {
        return NSAttributedString(string: sol)
    }
}
extension NSAttributedString {
    convenience init(@AttributedStringBuilder builder: () -> NSAttributedString) {
        self.init(attributedString: builder())
    }
}

// Failed to compile
/// ERROR: `Cannot convert value of type 'String' to closure result type 'NSAttributedString'`
let someText = NSAttributedString {
    "Folder "
}

/// This works fine
let workingText = NSAttributedString {
    "Folder "
    "File "
}

I run into exact same problem here:

I pinged @Douglas_Gregor on twitter in that regard, but have not received any answer yet.

As I replied on Twitter, this is a bug that has been fixed on master but did not make it into Xcode 11.4. It's also specifically called out in the thread on function builders implementation progress.

Doug

1 Like

Argh, dang it, twitter bugged out on me, I didn't get any notifications that you replied. Sorry about that. :frowning:

@Douglas_Gregor one question.

Does buildExpression have to return the same type as buildBlock?

For some reason I couldn't make my function builder work otherwise. Ideally I want buildExpression to only wrap the expression into some type eraser / container ((buildExpression(_: Expression) -> AnyLayout) and then buildBlock to pick it up and return an array buildBlock(_: AnyLayout...) -> [AnyLayout]). Right now I used the HTML example from the original draft to workaround the problem.

@_functionBuilder
public enum LayoutBuilder {
  typealias Component<Content> = [AnyLayout<Content>]
  
  // FIXME: Can we just return `AnyLayout<T.Content>` here?
  static func buildExpression<T>(
    _ expression: T
  ) -> [AnyLayout<T.Content>] where T: Layout {
    [expression.wrapIntoAnyLayout()]
  }

  // FIXME: Can this just be `(AnyLayout<Content>) -> AnyLayout<Content>`? 
  static func buildIf<Content>(
    _ children: Component<Content>?
  ) -> Component<Content> {
    children ?? []
  }

  // FIXME: Can we remove this? Why is this one even needed? 
  static func buildBlock<Content>(
    _ component: Component<Content>
  ) -> Component<Content> {
    component
  }

  // FIXME: Ideally we want the parameter to be `AnyLayout<Content>...`
  // and the return type `[AnyLayout<Content>]`
  static func buildBlock<Content>(
    _ children: Component<Content>...
  ) -> Component<Content> {
    // FIXME: Use `.flatMap(\.self)` when compiler bug is resolved.
    children.flatMap { $0 }
  }
}

Ideally something like should be enough:

@_functionBuilder
public enum LayoutBuilder {
  static func buildExpression<T>(
    _ expression: T
  ) -> AnyLayout<T.Content> where T: Layout {
    expression.wrapIntoAnyLayout()
  }

  static func buildIf<T>(
    _ child: T?
  ) -> AnyLayout<Content> where T: Layout {
    child.wrapIntoAnyLayout()
  }

  static func buildBlock<Content>(
    _ children: AnyLayout<Content>...
  ) -> [AnyLayout<Content>] {
    children
  }
}

No, but the definitions you have for buildBlock require that every buildExpression call must produce the same type, because Component<Content>... takes a homogeneously-typed set of arguments. Hence, you need to type-erase somewhere or you need to have N overloads of buildBlock to account for 0..<N input expressions.

Doug

1 Like