Mirror / CustomReflectable / CustomLeafReflectable
Reflection in Swift is a limited affair, providing read-only access to information about objects. In fact, this functionality might be better described as introspection rather than reflection.
But can you really blame them for going with that terminology?
“Introspection” is such a bloviated and acoustically unpleasant word
compared to “Reflection.”
And how could anyone pass on the nominal slam dunk of
calling the type responsible for reflecting an object’s value a Mirror
?
What equivalent analogy can we impart for the act of introspection?
struct Long
?
(As if we didn’t have better things to do than
ask our Swift code about the meaning of Self
)
Without further ado,
let’s take a peek inside the inner workings of how Swift exposes
that of our own code:
Mirror
, Custom
, and Custom
.
Swift provides a default reflection, or structured view,
for every object.
This view is used by Playgrounds
(unless a custom description is provided),
and also acts as a fallback when describing objects
that don’t conform to the
Custom
or Custom
protocols.
You can customize how instances of a type are reflected
by adopting the Custom
protocol
and implementing the required custom
property.
The computed custom
property returns a Mirror
object,
which is typically constructed by reflecting self
and
passing the properties you wish to expose to the reflected interface.
Properties may be either
keyed and passed in a dictionary
or unkeyed and passed in an array.
You can optionally specify a display style
to override the default for the kind of type (struct
, class
, tuple
, etc.);
if the type is a class,
you also have the ability to adjust how ancestors are represented.
Conforming to CustomReflectable
To get a better understanding of how this looks in practice, let’s look at an example involving an implementation of a data model for the game of chess:
A chessboard comprises 64 squares,
divided into
8 rows (called ranks) and
8 columns (called files).
A compact way to represent each location on a board
is the 0x88 method,
in which the rank and file are encoded into 3 bits of each
nibble
of a byte (0b0rrr0fff
).
For simplicity, Rank
and File
are typealias’d to UInt8
and use one-based indices;
a more complete implementation might use enumerations instead.
struct Chess Board {
typealias Rank = UInt8
typealias File = UInt8
struct Coordinate: Equatable, Hashable {
private let value: UInt8
var rank: Rank {
return (value >> 3) + 1
}
var file: File {
return (value & 0b00000111) + 1
}
init(rank: Rank, file: File) {
precondition(1...8 ~= rank && 1...8 ~= file)
self.value = ((rank - 1) << 3) + (file - 1)
}
}
}
If we were to construct a coordinate for b8 (rank 8, file “b” or 2), its default reflection wouldn’t be particularly helpful:
let b8 = Chess Board.Coordinate(rank: 8, file: 2)
String(reflecting: b8) // Chess Board.Coordinate(value: 57)
57
in decimal is equal to 0111001
in binary,
or a value of 7 for rank and 1 for file,
which adding the index offset of 1 brings us to
rank 8 and file 2.
The default mirror provided for a structure
includes each of an object’s stored properties.
However, consumers of this API shouldn’t need to know
or even care about the implementation details of how this information is stored.
It’s more important to reflect the programmatic surface area of the type,
and we can use Custom
to do just that:
extension Chess Board.Coordinate: Custom Reflectable {
var custom Mirror: Mirror {
return Mirror(self,
children: ["rank": rank, "file": file])
}
}
Rather than exposing the private stored value
property,
our custom mirror provides the computed rank
and file
properties.
The result: a much more useful representation
that reflects our understanding of the type:
String(reflecting: b8) // Chess Board.Coordinate(rank: 8, file: 2)
Introspecting the Custom Mirror
The String(reflecting:)
initializer is one way that mirrors are used.
But you can also get at them directly through the custom
property
to access the type metatype and display style,
and enumerate each of the mirror’s children.
b8.custom Mirror.subject Type // Chess Board.Coordinate.Type
b8.custom Mirror.display Style // struct
for child in b8.custom Mirror.children {
print("\(child.label ?? ""): \(child.value)")
}
Customizing the Display of a Custom Mirror
There are differently-shaped mirrors for each kind of Swift value, each with their own particular characteristics.
For example, the most important information about a class is its identity, so its reflection contains only its fully-qualified type name. Whereas a structure is all about substance and offers up its type name and values.
If you find this to be less than flattering for your special type
you can change up your look by specifying an enumeration value
to the display
attribute in the Mirror
initializer.
Here’s a sample of what you can expect for each of the available display styles when providing both labeled and unlabeled children:
Labeled Children
extension Chess Board.Coordinate: Custom Reflectable {
var custom Mirror: Mirror {
return Mirror(self,
children: ["rank": rank, "file": file])
display Style: display Style)
}
}
Style | Output |
---|---|
class |
Chess |
struct |
Coordinate(rank: 8, file: 2) |
enum |
Coordinate(8) |
optional |
8 |
tuple |
(rank: 8, file: 2) |
collection |
[8, 2] |
set |
{8, 2} |
dictionary |
[rank: 8, file: 2] |
Unlabeled Children
extension Chess Board.Coordinate: Custom Reflectable {
var custom Mirror: Mirror {
return Mirror(self,
unlabeled Children: [rank, file],
display Style: display Style)
}
}
Style | Output |
---|---|
class |
Chess |
struct |
Coordinate() |
enum |
Coordinate(8) |
optional |
8 |
tuple |
(8, 2) |
collection |
[8, 2] |
set |
{8, 2} |
dictionary |
[ 8, 2] |
Reflection and Class Hierarchies
Continuing with our chess example,
let’s introduce some class by defining an abstract Piece
class
and subclasses for pawns, knights, bishops, rooks, queens, and kings.
(For flavor, let’s add standard valuations
while we’re at it).
class Piece {
enum Color: String {
case black, white
}
let color: Color
let location: Chess Board.Coordinate?
init(color: Color, location: Chess Board.Coordinate? = nil) {
self.color = color
self.location = location
precondition(type(of: self) != Piece.self, "Piece is abstract")
}
}
class Pawn: Piece { static let value = 1 }
class Knight: Piece { static let value = 3 }
class Bishop: Piece { static let value = 3 }
class Rook: Piece { static let value = 5 }
class Queen: Piece { static let value = 9 }
class King: Piece {}
So far, so good.
Now let’s use the same approach as before to define a custom mirror
(setting the display
to struct
so that we get more reflected information than we would otherwise for a class):
extension Piece: Custom Reflectable {
var custom Mirror: Mirror {
return Mirror(self,
children: ["color": color,
"location": location ?? "N/A"],
display Style: .struct))
}
}
Let’s see what happens if we try to reflect a new Knight
object:
let knight = Knight(color: .black, location: b8)
String(reflecting: knight) //
Piece(color: black, location: (rank: 8, file: 2))
Hmm… that’s no good.
What if we try to override that in an extension to Knight
?
extension Knight {
override var custom Mirror: Mirror {
return Mirror(self,
children: ["color": color,
"location": location ?? "N/A"])
}
} // ! error: overriding declarations in extensions is not supported
No dice (to mix metaphors).
Let’s look at three different options to get this working with classes.
Option 1: Conform to CustomLeafReflectable
Swift actually provides something for just such a situation —
a protocol that inherits from Custom
called Custom
.
If a class conforms to Custom
,
its reflections of subclasses will be suppressed
unless they explicitly override custom
and provide their own Mirror
.
Let’s take another pass at our example from before,
this time adopting Custom
in our initial declaration:
class Piece: Custom Leaf Reflectable {
…
var custom Mirror: Mirror {
return Mirror(reflecting: self)
}
}
class Knight: Piece {
…
override var custom Mirror: Mirror {
return Mirror(self,
children: ["color": color,
"location": location ?? "N/A",
"value": Knight.value],
display Style: .struct)
}
}
Now when go to reflect our knight,
we get the correct type — Knight
instead of Piece
.
String(reflecting: knight)
// Knight(color:Piece.Color.black, location: (rank: 8, file: 2), value: 3)
Unfortunately, this approach requires us to do the same for each of the subclasses. Before we suck it up and copy-paste our way to victory, let’s consider our alternatives.
Option 2: Encode Type in Superclass Mirror
A simpler alternative to going the Custom
route
is to include the type information as a child of the mirror
declared in the base class.
extension Piece: Custom Reflectable {
var custom Mirror: Mirror {
return Mirror(self,
children: ["type": type(of: self),
"color": color,
"location": location ?? "N/A"],
display Style: .struct)
}
}
String(reflecting: knight)
// Piece(type: Knight, color: black, location: (rank: 8, file: 2))
This approach is appealing because it allows reflection to be hidden as an implementation detail. However, it can only work if subclasses are differentiated by behavior. If a subclass adds stored members or significantly differs in other ways, it’ll need a specialized representation.
Which begs the question: why are we using classes in the first place?
Option 3: Avoid Classes Altogether
Truth be told,
we only really introduced classes here
as a contrived example of how to use Custom
.
And what we came up with is kind of a mess, right?
Ad hoc additions of value
type members for only some of the pieces
(kings don’t have a value in chess because they’re a win condition)?
An “abstract” base class that crashes if you try to instantiate it
(for current lack of a first-class language feature)?
Yikes.
These are the kinds of things that give OOP a bad name.
We can mitigate nearly all of these problems by adopting a protocol-oriented approach like the following:
First, define a protocol for Piece
and a new protocol to encapsulate pieces that have value, called Valuable
:
protocol Piece {
var color: Color { get }
var location: Chess Board.Coordinate? { get }
}
protocol Valuable: Piece {
static var value: Int { get }
}
Next, define value types —
an enum for Color
and structures for each piece:
enum Color: String {
case black, white
}
…
struct Knight: Piece, Valuable {
static let value: Int = 3
let color: Color
let location: Chess Board.Coordinate?
}
…
Although we can’t provide conformance by protocol through an extension,
we can provide a default implementation for the requirements
and declare adoption in extensions to each concrete Piece
type.
As a bonus, Valuable
can provide a specialized variant
that includes its value
:
extension Piece {
var custom Mirror: Mirror {
return Mirror(self,
children: ["color": color,
"location": location ?? "N/A"],
display Style: .struct)
}
}
extension Valuable{
var custom Mirror: Mirror {
return Mirror(self,
children: ["color": color,
"location": location ?? "N/A",
"value": Self.value],
display Style: .struct)
}
}
…
extension Knight: Custom Reflectable {}
…
Putting that all together, we get exactly the behavior we want with the advantage of value semantics.
let b8 = Chess Board.Coordinate(rank: 8, file: 2)
let knight = Knight(color: .black, location: b8)
String(reflecting: knight)
// Knight(color: black, location: (rank: 8, file: 2))
Ultimately,
Custom
is provided mostly for compatibility with
the classical, hierarchical types in Objective-C frameworks like UIKit.
If you’re writing new code, you need not follow in their footsteps.
When Swift first came out, longtime Objective-C developers missed the dynamism provided by that runtime. And — truth be told — truth be told, things weren’t all super great. Programming in Swift could, at times, feel unnecessarily finicky and constraining. “If only we could swizzle…” we’d grumble.
And perhaps this is still the case for some of us some of the time, but with Swift 4, many of the most frustrating use cases have been solved.
Codable is a better solution to JSON serialization
than was ever available using dynamic introspection.
Codable can also provide mechanisms for fuzzing and fixtures
to sufficiently enterprising and clever developers.
The Case
protocol fills another hole,
allowing us a first-class mechanism
for getting an inventory of enumeration cases.
In many ways,
Swift has leap-frogged the promise of dynamic programming,
making APIs like Mirror
largely unnecessary beyond logging and debugging.
That said, should you find the need for introspection, you’ll know just where to look.