Function builders

I have been experimenting with function builders a bit and they are superuseful but often i wished there was a way to inspect the generated code to verify its doing what its supposed to.

Could this be somehow exposed? (Ideally directly in Xcode)

I think this could be useful for ALL generated/synthesized code like Equatable/Hashable implementations

5 Likes

What you're asking for is a way to convert the compiler's AST (abstract syntax tree, one of the ways it represents code) back into source code. There's a module in the compiler called ASTPrinter that does this, but it only handles declarations, not statements or expressions, so it can't print the bodies of functions.

Adding support for printing statements and expressions would be extremely useful—it would not only help users understand synthesized code, but might also make swiftinterface files better. It's not a small task, but I bet a solo contributor could tackle it.

7 Likes

General question: Could Element and/or Result type be function type? If I haven't missed something, this is not explicitly forbidden. However, I was not able to get Element = Return = (Int)->Int function builder to work.

  • I was able to create function builder, but the block only accepted one function at the time (due to trailing closure behavior)
  • When I run the code it resulted in SIGSEGV
  • I was unable to mark children type in buildBlock function as @escaping due to variadic argument type

What's going on with local declarations in the builder closures? I get errors in Xcode (beta 7), but the spec/pitch suggests they will be supported. Are they just not implemented yet? I really hope they will be implemented.

44%20PM

Error: Closure containing a declaration cannot be used with function builder 'ViewBuilder'

But in the pitch:

... the ability to have local declarations and explicit control flow.
... Local declarations are left alone by the transformation.
... closure/function syntax is the most natural vehicle given: [...] the existing ability of functions to have local declarations ....

Rob

I don’t think this is supported ATM, neither will be I think when it comes out of beta. Function builders will be of private usage until swift open sources it in further versions.

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 "
}
Terms of Service

Privacy Policy

Cookie Policy