Learning Swift: Convertibles
Note: this post is part of a series about the Swift programming language, introduced at WWDC 2014. I’m no more experienced in Swift than anyone else outside Apple, but I learn best by coding and talking through a problem. If there’s a better way to approach some of these topics, get in touch on Twitter!
As the Swift language matures, most of the standard library has begun to settle into a stable shape. This includes a group of protocols that collectively define convertibles.
Unfortunately, we’re not talking about the cars: a convertible in Swift is, generally speaking, a data type that can be implicitly constructed from a literal. By conforming to one of the protocols in the standard library, your types can provide an extra level of convenience to you or anyone else using your code.
The Inner Workings of a Convertible
Let’s begin by breaking down our earlier definition to discuss exactly what a convertible is:
- Convertibles are constructed from literals: in code, you’ll type a value
directly inline – such as
3
,"foo"
, ortrue
. Other variables, even if they’re of the same primitive types, don’t qualify for literal conversion. - Convertibles are implicitly constructed: to convert from the literal’s native type to your convertible type, you just have to convince Swift of the final type (usually through type inference). You don’t need to explicitly call any constructors, and in most cases, you can avoid explicit casts as well.
The standard library provides these behaviors by defining a different
convertible protocol for each kind of literal the language recognizes. Often,
your types will conform to the basic Nil
, Boolean
, Integer
, Float
, or
String
convertible protocols. In more complex situations, you might also
support collections with the Array
and Dictionary
literal convertible
protocols. Finally, you could get in-depth with text by supporting the
UnicodeScalar
or ExtendedGraphemeCluster
protocols.
All of these protcols have a fairly common pattern: they each define a single
initializer that takes a value of the kind named in the protocol. For example,
IntegerLiteralConvertible
looks like this:
/// Conforming types can be initialized with integer literals
protocol IntegerLiteralConvertible {
typealias IntegerLiteralType
/// Create an instance initialized to `value`.
init(integerLiteralValue value: IntegerLiteralType)
}
By conforming to one of these protocols, your data type is declaring that it can be initialized with whatever literal the developer writes. The actual logic of translating that literal value to an instance of your type is left up to you to implement inside the initializer.
Simple Convertibles: Complex Numbers
A great use case for convertibles is the mathematical concept of a complex number. Frequently referred to as “imaginary numbers,” a complex number has two parts:
- A real component, which is just a regular number, and
- An imaginary component, which is a regular number times the “imaginary unit” i (the square root of -1).
By convention, we generally write a complex number as a + bi, where a is the real component and b is the imaginary component. The following, then, can all be treated as complex numbers:
- 2 + 3i
- 4 (which has an implicit imaginary component of 0i)
- 7i (which has an implicit real component of 0)
For this concept, we can define a new data type in Swift. Firing up a playground, we can write:
struct Complex {
var real: Double
var imaginary: Double
}
(I’ll use Doubles in this example for broader numeric support; if you only need
to work with integers, you could conceivably define Complex
with Int
members.)
At this point, we can construct an instance of Complex
using its inferred
initializer (which is given to all structs without any explicitly declared
initializers). We’d write something like:
let a = Complex(real: 1.0, imaginary: 2.0)
What happens, though, if we want a Complex
with only a real component? We
could certainly define it passing 0.0
for the imaginary
argument. However,
using convertibles, we can make that sort of real-only declaration much shorter.
Let’s implement FloatLiteralConvertible
for Complex
:
extension Complex: FloatLiteralConvertible {
init(floatLiteral value: FloatLiteralType) {
self.real = value
self.imaginary = 0.0
}
}
At this point, we can construct a Complex
from any Float
literal, so long as
we can convince Swift that the result type is supposed to be Complex
(and not
a bare Float
):
let b: Complex = 2.0
Notice how we aren’t explicitly calling any constructor on Complex
. Instead,
we’re taking advantage of Swift’s type coercion and the
FloatLiteralConvertible
protocol to implicitly call the initializer we just
defined. Once evaluated, this expression is equivalent to assigning
Complex(real: 2.0, imaginary: 0.0)
to the variable b
.
Crossing Type Boundaries
Implicit conversion to Complex
from Float
is cool, but was pretty
straightforward – the Complex
type already had float-like members, since we
defined it using Double
. We can get a little fancier by also providing support
for implicit Int
conversion:
extension Complex: IntegerLiteralConvertible {
init(integerLiteral value: IntegerLiteralType) {
self.real = Double(value)
self.imaginary = 0.0
}
}
This implementation looks really similar to our FloatLiteralConvertible
conformance, with one minor tweak. Since the value
being passed in can no
longer implicitly be cast to Double
, we need to construct a Double
value
when setting self.real
. Now, like above, we can make a Complex
using a plain
integer literal:
let c: Complex = 2
Note the lack of .0
after the number – this literal is an Int
rather than a
Float
, and so is using our newly defined protocol conformance.
These Types Have the Longest Names
Let’s continue with our implicit conversions, but leave the realm of number
literals. Swift provides a StringLiteralConvertible
that we can attempt to
implement, parsing complex expressions of the form “a+bi” when constructing a
Complex
.
We can sketch out a quick, very limited parser using an NSScanner inside the required initializer:
extension Complex: StringLiteralConvertible {
init(stringLiteral value: StringLiteralType) {
let scanner: NSScanner = NSScanner(string: String(value))
self.real = 0.0
self.imaginary = 0.0
if scanner.scanDouble(&self.real) == false {
return
}
if scanner.scanString("+", intoString: nil) == false {
return
}
if scanner.scanDouble(&self.imaginary) == false {
return
}
if scanner.scanString("i", intoString: nil) == false {
return
}
}
}
At this point, we should stop and discuss the effect and limitations of this
sort of implementation. While it serves to demonstrate String
conversion,
production code probably wouldn’t go this route for a variety of reasons.
First off, conforming to StringLiteralConvertible
says we can convert any
string literal to a Complex
instance, even those that look nothing like a
complex number. This implementation just falls back to returning something of
the form 0+0i in the worst cases, because it can’t do anything better – the
initializer in the protocol isn’t failable, so it can’t return nil
.
In addition, the use of NSScanner here is fairly primitive. Though NSScanner can
be part of a very powerful parsing algorithm, the way it’s used here is quite
brittle: using the shorthand “3-4i”, for example, would produce a Complex
with
an imaginary part of 0 instead of the expected 4. (We’d need to write “3+-4i”
to get the expected results.)
Finally, in Xcode 6.1, this code doesn’t even compile! Depending on some
particulars, we see one of a few confusing compiler error messages, usually
revolving around UnicodeScalarLiteralConvertible
or
ExtendedGraphemeClusterLiteralConvertible
– truly some of the longest protocol
names we’ve encountered yet.
These errors come from the fact that StringLiteralConvertible
conforms to
another convertible protocol, largely as a result of how Strings are built in
Swift:
- A
String
is a sequence of “Unicode extended grapheme clusters,” which the standard library simply calls “a unit of text that is meaningful to most humans.” - An extended grapheme cluster, in turn, is composed of one or more Unicode scalar values – single code points in the Unicode character space.
This means that any type we want to be String
literal convertible must also be
able to convert from grapheme clusters or Unicode scalars, as indicated by
StringLiteralConvertible
’s conformance. Thankfully, we can cheat a little bit
and just redirect those required initializers over to our String
-based
initializer, using some type aliases along the way:
extension Complex: StringLiteralConvertible {
typealias UnicodeScalarLiteralType = StringLiteralType
init(unicodeScalarLiteral value: UnicodeScalarLiteralType) {
self.init(stringLiteral: value)
}
typealias ExtendedGraphemeClusterLiteralType = StringLiteralType
init(extendedGraphemeClusterLiteral value: ExtendedGraphemeClusterLiteralType) {
self.init(stringLiteral: value)
}
// the existing String-based initializer from above remains here
}
Now, this extension should compile and we can start translating some strings
into Complex
instances:
let d: Complex = "1+2i" // gives 1+2i
let e: Complex = "2.0+1.0i" // gives 2+1i
let f: Complex = "3+-4i" // gives 3-4i
let g: Complex = "1 + 2 i" // gives 1+2i
…and we can see what happens when that translation fails:
let h: Complex = "3-4i" // gives 3+0i
let i: Complex = "foo" // gives 0+0i
let j: Complex = "4ty-two" // gives 4+0i
Still, this could be a useful time-saver, in our contrived little world of complex numbers.
In Swift Libraries
One of the best things about the various convertible protocols is how the Swift language itself uses them. Rather than simply expose this behavior for third-party types, Swift uses convertibles for some nifty tricks.
For example, Swift has a Selector
type, used when interoperating with
Objective-C code that passes around SEL
variables. The fastest way to
construct a Selector
instance knowing the name of an Objective-C selector is,
in fact, to use a String
literal conversion:
let s: Selector = "hashValue"
Much like our Complex
example above, the Selector
type defines all the
initializers required of String
, grapheme cluster, and Unicode scalar
convertibles. It also exposes an explicit constructor that takes a String
argument, in the event that you’re interpreting a non-literal as a Selector
.
Convertibles even worm their way into that most fundamental of Swift concepts,
the Optional
. As we know, optional types in Swift can either have a “real”
value, or can be nil
. To ease the declaration of an optional value as the
latter, the Optional
type conforms to NilLiteralConvertible
– presumably to
return Optional.None
, which indicates the lack of value under the hood.
All in all, literal conversions are one of those nifty conveniences in the Swift
language. They can be great for making your custom data types more readable when
used: simply typing 4
is much clearer than Complex(real: 4, imaginary: 0)
.
On the other hand, pervasive use can actually cloud the evaluated type of a
variable or expression – be careful not to obfuscate your code’s meaning with an
implicit type conversion.
Want this post’s code as a playground? Of course you do! Download it here.