How can I restore type generic type inference in Swift result builders?

Viewed 226

When designing result builders which consume and product generic types, I often find myself needing to spell out types in an annoyingly explicit way. Consider this toy example:

@resultBuilder enum SomeBuilder {
    static func buildBlock(_ v: Result<Int,Error>) -> Result<Int,Error> { v }
}
struct SomeStruct {
    init(@SomeBuilder _ v: () -> Result<Int,Error>) {}
}

let x = SomeStruct {
    Result.success(0)
}

SomeBuilder is almost the simplest possible @resultBuilder: it takes in single values of type Result<Int,Error> and returns them as-is. SomeStruct is a simple struct with an initializer that takes a single @SomeBuilder closure which produces Result<Int,Error>. Consequently, it makes sense to me that this code should compile. Swift should be able to figure out that Result.success(0) refers to Result<Int,Error>.success(0) because this is the only type that could make sense in this context. However, this example fails to compile with

_:9:5: error: cannot convert value of type 'Result<Int, _>' to expected argument type 'Result<Int, Error>'
    Result.success(0)
    ^
_:9:5: note: arguments to generic parameter 'Failure' ('_' and 'Error') are expected to be equal
    Result.success(0)
    ^
_:9:5: error: generic parameter 'Failure' could not be inferred
    Result.success(0)
    ^
_:9:5: note: explicitly specify the generic arguments to fix this issue
    Result.success(0)
    ^
          <Int, <#Failure: Error#>>

On the same line, Swift claims that it cannot infer the Failure type of Result.success(0) and then claims that it knows the failure must be Error. I can get this code to compile by using

let x = SomeStruct {
    Result<Int,Error>.success(0)
}

but in real examples, being this verbose with the types is extremely inhibitive to the DSLs I want to design.

Is this a bug in the compiler? How can I get Swift to infer generic types in result builders such as this one?

1 Answers

I am fairly confident that this should be classified as a bug, but I do have an explanation and a solution. Consider how Swift transforms this result builder.

let x = SomeStruct {
    Result.success(0)
}

becomes (more-or-less)

let x = SomeStruct {
    let v0 = Result.success(0)
    return SomeBuilder.buildBlock(v0)
}

In isolation, there is no way for the compiler to infer the type of v0. This is only known once it is used as a parameter for buildBlock, but Swift does not infer types of declarations from their later usage.

However, if we add a trivial buildExpression to SomeBuilder, this transformation is different.

@resultBuilder enum SomeBuilder {
    static func buildExpression(_ v: Result<Int,Error>) -> Result<Int,Error> { v }
    static func buildBlock(_ v: Result<Int,Error>) -> Result<Int,Error> { v }
}

Now, Swift transforms

let x = SomeStruct {
    Result.success(0)
}

into (more-or-less)

let x = SomeStruct {
    let v0 = SomeBuilder.buildExpression(Result.success(0))
    return SomeBuilder.buildBlock(v0)
}

In this code, Swift is able to infer that the type of v0 is Result<Int,Error> because that is the type that buildExpression returns, and Swift is able to infer that the type of Result.success(0) is Result<Int,Error> because this is the type that buildExpression takes as an argument.

For more details on what these @resultBuilder transformations look like, see this incredible WWDC session about result builders.

Conclusion

@resultBuilder enum SomeBuilder {
    static func buildExpression(_ v: Result<Int,Error>) -> Result<Int,Error> { v }
    static func buildBlock(_ v: Result<Int,Error>) -> Result<Int,Error> { v }
}
struct SomeStruct {
    init(@SomeBuilder _ v: () -> Result<Int,Error>) {}
}

let x = SomeStruct {
    Result.success(0)
}

compiles successfully. If you want generic type inference to work in result builders like you're used to in other parts of the language, you need to have a buildExpression taking the relevant type, even if it just returns its input and even if you otherwise would not need any buildExpressions at all! Once you have done this, your result builders will be much more readable as you will not need to unnecessarily spell out types when they are clear from context. This works in complicated builders and not just in toy examples such as this one.

Related