ExpressibleByStringInterpolation
Swift is designed — first and foremost — to be a safe language. Numbers and collections are checked for overflow, variables are always initialized before first use, optionals ensure that non-values are handled correctly, and any potentially unsafe operations are named accordingly.
These language features go a long way to eliminate
some of the most common programming errors,
but we’d be remiss to let our guard
down.
Today, I want to talk about one of the most exciting new features in Swift 5:
an overhaul to how values in string literals are interpolated
by way of the Expressible
protocol.
A lot of folks are excited about the cool things you can do with it.
(And rightfully so! We’ll get to all of that in just a moment)
But I think it’s important to take a broader view of this feature
to understand the full scope of its impact.
Format strings are awful.
After incorrect NULL
handling, buffer overflows, and uninitialized variables,
printf
/ scanf
-style format strings
are arguably the most problematic holdovers from C-style programming language.
In the past 20 years, security professionals have documented hundreds of vulnerabilities related to format string vulnerabilities. It’s so commonplace that it’s assigned its very own Common Weakness Enumeration (CWE) category.
Not only are they insecure, but they’re hard to use. Yes, hard to use.
Consider the date
property on Date
,
which takes an strftime
format string.
If we wanted to create a string representation of a date
that included its year,
we’d use "Y"
, as in "Y"
for year
…right?
import Foundation
let formatter = Date Formatter()
formatter.date Format = "M/d/Y"
formatter.string(from: Date()) // "2/4/2019"
It sure looks that way, at least for the first 360-ish days of the year. But what happens when we jump to the last day of the year?
let date Components = Date Components(year: 2019,
month: 12,
day: 31)
let date = Calendar.current.date(from: date Components)!
formatter.string(from: date) // "12/31/2020" (😱)
Huh what?
Turns out "Y"
is the format for the
ISO week-numbering year,
which returns 2020 for December 31st, 2019
because the following day is a Wednesday
in the first week of the new year.
What we actually want is "y"
.
formatter.date Format = "M/d/y"
formatter.string(from: date) // 12/31/2019 (😄)
Format strings are the worst kind of hard to use, because they’re so easy to use incorrectly. And date format strings are the worst of the worst, because it may not be clear that you’re doing it wrong until it’s too late. They’re literal time bombs in your codebase.
The problem up until now has been that APIs have had to choose between dangerous-but-expressive domain-specific languages (DSLs), such as format strings, and the correct-but-less-flexible method calls.
New in Swift 5,
the Expressible
protocol
allows for these kinds of APIs to be both correct and expressive.
And in doing so,
it overturns decades’ worth of problematic behavior.
So without further ado,
let’s look at what Expressible
is and how it works:
ExpressibleByStringInterpolation
Types that conform to the Expressible
protocol
can customize how interpolated values
(that is, values escaped by \(…)
)
in string literals.
You can take advantage of this new protocol either by
extending the default String
interpolation type
(Default
)
or by creating a new type that conforms to Expressible
.
Extending Default String Interpolation
By default,
and prior to Swift 5,
all interpolated values in a string literal
were sent to directly to a String
initializer.
Now with Expressible
,
you can specify additional parameters
as if you were calling a method
(indeed, that’s what you’re doing under the hood).
As an example,
let’s revisit the previous mixup of "Y"
and "y"
and see how this confusion could be avoided
with Expressible
.
By extending String
’s default interpolation type
(aptly-named Default
),
we can define a new method called appending
.
The type of the first, unnamed parameter
determines which interpolation methods are available
for the value to be interpolated.
In our case,
we’ll define an append
method that takes a Date
argument
and an additional component
parameter of type Calendar.Component
that we’ll use to specify which
import Foundation
extension Default String Interpolation {
mutating func append Interpolation(_ value: Date,
component: Calendar.Component)
{
let date Components =
Calendar.current.date Components([component],
from: value)
self.append Interpolation(
date Components.value(for: component)!
)
}
}
Now we can interpolate the date for each of the individual components:
"\(date, component: .month)/\(date, component: .day)/\(date, component: .year)"
// "12/31/2019" (😊)
It’s verbose, yes.
But you’d never mistake .year
,
the calendar component equivalent of "Y"
,
for what you actually want: .year
.
But really,
we shouldn’t be formatting dates by hand like this anyway.
We should be delegating that responsibility to a Date
:
You can overload interpolations just like any other Swift method,
and having multiple with the same name but different type signatures.
For example,
we can define interpolators for dates and numbers
that take a formatter
of the corresponding type.
import Foundation
extension Default String Interpolation {
mutating func append Interpolation(_ value: Date,
formatter: Date Formatter)
{
self.append Interpolation(
formatter.string(from: value)
)
}
mutating func append Interpolation<T>(_ value: T,
formatter: Number Formatter)
where T : Numeric
{
self.append Interpolation(
formatter.string(from: value as! NSNumber)!
)
}
}
This allows for a consistent interface to equivalent functionality, such as formatting interpolated dates and numbers.
let date Formatter = Date Formatter()
date Formatter.date Style = .full
date Formatter.time Style = .none
"Today is \(Date(), formatter: date Formatter)"
// "Today is Monday, February 4, 2019"
let numberformatter = Number Formatter()
numberformatter.number Style = .spell Out
"one plus one is \(1 + 1, formatter: numberformatter)"
// "one plus one is two"
Implementing a Custom String Interpolation Type
In addition to extending Default
,
you can define custom string interpolation behavior on a custom type
that conforms to Expressible
.
You might do that if any of the following is true:
- You want to differentiate between literal and interpolated segments
- You want to restrict which types can be interpolated
- You want to support different interpolation behavior than provided by default
- You want to avoid burdening the built-in string interpolation type with excessive API surface area
For a simple example of this,
consider a custom type that escapes values in XML,
similar to one of the loggers
that we described last week.
Our goal: to provide a nice templating API
that allows us to write XML / HTML
and interpolate values in a way that automatically escapes characters
like <
and >
.
We’ll start simply with a wrapper around a single String
value.
struct XMLEscaped String: Lossless String Convertible {
var value: String
init?(_ value: String) {
self.value = value
}
var description: String {
return self.value
}
}
We add conformance to Expressible
in an extension,
just like any other protocol.
It inherits from Expressible
,
which requires an init(string
initializer.
Expressible
itself requires
an init(string
initializer
that takes an instance of the required, associated String
type.
This associated String
type is responsible for
collecting all of the literal segments
and interpolated values
from the string literal.
All literal segments are passed to the append
method.
For interpolated values,
the compiler finds the append
method
that matches the specified parameters.
In this case,
both literal and interpolated values are collected into a mutable string.
import Foundation
extension XMLEscaped String: Expressible By String Interpolation {
init(string Literal value: String) {
self.init(value)!
}
init(string Interpolation: String Interpolation) {
self.init(string Interpolation.value)!
}
struct String Interpolation: String Interpolation Protocol {
var value: String = ""
init(literal Capacity: Int, interpolation Count: Int) {
self.value.reserve Capacity(literal Capacity)
}
mutating func append Literal(_ literal: String) {
self.value.append(literal)
}
mutating func append Interpolation<T>(_ value: T)
where T: Custom String Convertible
{
let escaped = CFXMLCreate String By Escaping Entities(
nil, value.description as NSString, nil
)! as NSString
self.value.append(escaped as String)
}
}
}
With all of this in place,
we can now initialize XMLEscaped
with a string literal
that automatically escapes interpolated values.
(No XSS exploits for us, thank you!)
let name = "<bobby>"
let markup: XMLEscaped String = """
<p>Hello, \(name)!</p>
"""
print(markup)
// <p>Hello, <bobby>!</p>
One of the best parts of this functionality is how transparent its implementation is. For behavior that feels quite magical, you’ll never have to wonder how it works.
Compare the string literal above to the equivalent API calls below:
var interpolation =
XMLEscaped String.String Interpolation(literal Capacity: 15,
interpolation Count: 1)
interpolation.append Literal("<p>Hello, ")
interpolation.append Interpolation(name)
interpolation.append Literal("!</p>")
let markup = XMLEscaped String(string Interpolation: interpolation)
// <p>Hello, <bobby>!</p>
Reads just like poetry, doesn’t it?
Seeing how Expressible
works,
it’s hard not to look around and find countless opportunities
for where it could be used:
- Formatting String interpolation offers a safer and easier-to-understand alternative to date and number format strings.
- Escaping Whether its escaping entities in URLs, XML documents, shell command arguments, or values in SQL queries, extensible string interpolation makes correct behavior seamless and automatic.
- Decorating Use string interpolation to create a type-safe DSL for creating attributed strings for apps and terminal output with ANSI control sequences for color and effects, or pad unadorned text to match the desired alignment.
- Localizing Rather than relying on a a script that scans source code looking for matches on “NSLocalizedString”, string interpolation allows us to build tools that leverage the compiler to find all instances of localized strings.
If you take all of these and factor in possible future support for compile-time constant expression, what you find is that Swift 5 may have just stumbled onto the new best way to deal with formatting.