Greetings! Here's a pitch for a small feature that will hopefully alleviate a large annoyance.
Introduction
A new string interpolation syntax that allows a developer to provide a default string when interpolating an optional value.
Motivation
String interpolations are a streamlined and powerful way to include values within a string literal. When one of those values is optional, however, interpolating is not so simple; in many cases, a developer must fall back to unpalatable code or output that exposes type information.
For example, placing an optional string in an interpolation yields an important warning and two suggested fixes, only one of which is ideal:
let name: String? = nil
print("Hello, \(name)!")
// warning: string interpolation produces a debug description for an optional value; did you mean to make this explicit?
// print("Hello, \(name)!")
// ^~~~
// note: use 'String(describing:)' to silence this warning
// print("Hello, \(name)!")
// ^~~~
// String(describing: )
// note: provide a default value to avoid this warning
// print("Hello, \(name)!")
// ^~~~
// ?? <#default value#>
The first suggestion, adding String(describing:)
, silences the warning but includes nil
in the output of the string — maybe okay for a quick shell script, but not really appropriate result for anything user-facing.
The second suggestion is good, allowing us to provide whatever default string we'd like:
let name: String? = nil
print("Hello, \(name ?? "new friend")!")
However, the nil-coalescing operator (??
) only works with values of the same type as the optional value, making it awkward or impossible to use when providing a default for non-string types. In this example, the age
value is an optional Int
, and there isn't a suitable integer to use when it's nil
:
let age: Int? = nil
print("Your age: \(age)")
// warning, etc....
To provide a default string when age
is missing, we have to write some gnarly code, or split out the missing case altogether:
let age: Int? = nil
// Optional.map
print("Your age: \(age.map(String.init(describing:)) ?? "missing")")
// Ternary expression
print("Your age: \(age != nil ? "\(age!)" : "missing")")
// if-let statement
if let age {
print("Your age: \(age)")
} else {
print("Your age: missing")
}
Proposed solution
The standard library should add an interpolation overload that lets you write the intended default as a string, no matter what the type of value.
let age: Int? = nil
print("Your age: \(age, default: "missing")")
This addition will improve the clarity of code that uses string interpolations and encourage developers to provide sensible defaults instead of letting nil
leak into string output.
Detailed design
The implementation of this new interpolation overload looks like this:
extension String.StringInterpolation {
mutating func appendInterpolation<T>(
_ value: T?,
default: @autoclosure () -> String
) {
self.appendLiteral(value.map(String.init(describing:)) ?? `default`())
}
}
You can try this out yourself by copy/pasting the snippet above into a project or playground, or by experimenting with this Swift Fiddle.
Source compatibility
This proposal adds one new API to the standard library, which should not be source-breaking for any existing projects. If a project or a dependency has added a similar overload, it will take precedence over the new standard library API.
ABI compatibility
This proposal is purely an extension of the ABI of the
standard library and does not change any existing features.
Implications on adoption
The new API will be included in a new version of the Swift runtime, and could be marked as backward deployable.
Future directions
None considered.
Alternatives considered
None considered.