Make function builders compatible with ExpressibleByStringInterpolation

The following code compiles successfully and the program prints A.

struct StringWrapper {
    let string: String
}

extension StringWrapper: ExpressibleByStringInterpolation {
    public init(stringLiteral: String) {
        self.init(string: stringLiteral)
    }

    public init(stringInterpolation: StringWrapperInterpolation) {
        self.init(string: stringInterpolation.content)
    }
}

struct StringWrapperInterpolation: StringInterpolationProtocol {
    var content: String

    public init(literalCapacity: Int, interpolationCount: Int) {
        content = ""
    }

    public mutating func appendLiteral(_ literal: String) {
        content.append(literal)
    }

    public mutating func appendInterpolation(_ i: Int) {
        content.append("<<\(i)>>")
    }
}

@_functionBuilder
struct FB {
    public static func buildBlock(_ content: StringWrapper...) -> String {
        content.map(\.string).joined()
    }
}

@FB
var functionBuilderValue: String {
    StringWrapper("A")
}

print(functionBuilderValue)

If I replace the definition of functionBuilderValue with the following snippet the program still compiles and prints A<<2>> as expected.

let interpolatedInt: StringWrapper = "\(2)"
@FB
var functionBuilderValue: String {
    StringWrapper("A")
    interpolatedInt
}

If I replace the definition of functionBuilderValue with the following snippet the program won't compile with the error Cannot convert value of type 'String' to expected argument type 'StringWrapper'

@FB
var functionBuilderValue: String {
    StringWrapper("A")
    "\(2)"
}

So, inside the body of a function builder, ExpressibleByStringInterpolation doesn't work, which is unexpected. ExpressibleByStringLiteral doesn't work either, but it's less of a problem because we can modify the function builder to take string literals.

Is there any technical problem blocking a fix for this?

A workaround is to specify the type we want to interpolate:

@FB
var functionBuilderValue: String {
    StringWrapper("A")
    "\(2)" as StringWrapper
}

Could be a regression. It works on Swift 5.1 (Playground 3.3.1), but not on Swift 5.3 (Xcode 12.0.1). Maybe you can file a bug report? See below.


On an unrelated note. Do you really need a custom StringInterpolation? It may be generally less confusing to use the default one.

Not a regression. Not a bug.

buildBlock combines results from statement blocks. A statement with a string literal will have type String, because it is the default literal type.

What you're showing is that you want to supply contextual type information for an expression, before the expression takes on the default literal type (which it does when there is no contextual type information).

The designers of function/result builders have thought of that! In fact, they have built something just for that:

/// If declared, provides contextual type information for statement
/// expressions to translate them into partial results.
static func buildExpression(_ expression: Expression) -> Component { ... }

...and as expected, adding the following to your builder makes it work exactly as you want:

static func buildExpression(_ content: StringWrapper) -> StringWrapper {
  content
}
4 Likes

Ah, so that's how one uses it.

Awesome! Thank you very much.