Improving result builder failures using @available
Result builders in Swift are a great API for creating DSLs (Domain Specific Languages). In recent projects, we have used them to capture details about telemetry events and defining a set of feature flags that can be locally overridden by developers.
We have a Flag
type that is generic over the value the feature flag will return. We also have OverridableFlag
which defines a flag that can be overridden. It has the name and description from the flag, but also an array of options that can be used to override.
public struct OverridableFlag {
public let name: Flag.Name
public let description: Flag.Description
public let options: [Option]
}
We build up a set of overridable flags in an OverridableFlags
type which provides a nice named “home” for the overridable flags to live in.
extension OverridableFlags {
@resultBuilder
public enum Builder {
public static func buildPartialBlock(first: OverridableFlag) -> OverridableFlags {
OverridableFlags(values: [first])
}
public static func buildPartialBlock(accumulated: OverridableFlags, next: OverridableFlag) -> OverridableFlags {
OverridableFlags(values: Array(accumulated) + [next])
}
}
}
However, astute readers may notice that there’s no public initialiser on the OverridableFlag
type, so how do users add their flags into our system?
The answer lies within another part of the API for result builders, they allow the ability to convert an expression to the input type for a build block. We’ve decided that as users of this system only really see the Flag
, keeping the OverridableFlag
an implementation detail is nice.
Most flags in our system have boolean values, so the first buildExpression
function is one that takes a Flag<Bool>
. Internally this uses an overridable
function on Flag
which takes an option builder.
extension OverridableFlags.Builder {
public static func buildExpression(_ flag: Flag<Bool>) -> OverridableFlag {
flag.overridable {
Option(name: "true", json: true)
Option(name: "false", json: false)
}
}
}
This allows users to define a set of overridable flags using the boolean flags they already have defined elsewhere in the system.
OverridableFlags {
Flag.shouldShowSomething
Flag.shouldAllowAction
...
}
When a user attempts to use a flag which doesn’t have a boolean value, they will see the following compiler error. In the following case we’ve provided a String
-based flag to the builder:
Cannot convert value of type ‘Flag
' to expected argument type 'Flag '
This is great, the builder won’t accidentally take a flag it doesn’t support. However, it does nothing to guide the user to fixing their mistake.
Enter the @available attribute
The @available
attribute in Swift is probably mostly used as a way to deprecate
functions.
Another ability is using the unavailable
argument, where instead of a deprecation warning, a compiler error is provided. Using this, we can add a buildExpression
function that takes a flag of any value and mark it as unavailable.
Note: The boolean case above will still continue to work because Swift will always use the most specific function that it can see.
extension OverridableFlags.Builder {
@available(*, unavailable, message: """
Unsupported Flag type
Use Flag.overridable with a set of options to create an overridable flag.
""")
public static func buildExpression<Value>(
_ flag: Flag<Value>
) -> OverridableFlag {
fatalError()
}
}
Now when user’s of our library try to add a flag of an unsupported type they see a message that we can control to aid them to a solution for the problem:
‘buildExpression’ is unavailable: Unsupported Flag type Use Flag.overridable with a set of options to create an overridable flag.
This is really useful for result builder code where the default error message may make less sense to the caller.