Mirror / CustomReflectable / CustomLeafReflectable - NSHipster
source link: https://nshipster.com/mirror/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
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 LongDarkTeaTimeOfTheSoul
?
(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
, CustomReflectable
, and CustomLeafReflectable
.
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
CustomStringConvertible
or CustomDebugStringConvertible
protocols.
You can customize how instances of a type are reflected
by adopting the CustomReflectable
protocol
and implementing the required customMirror
property.
The computed customMirror
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 ChessBoard {
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 = ChessBoard.Coordinate(rank: 8, file: 2)
String(reflecting: b8) // ChessBoard.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 CustomReflectable
to do just that:
extension ChessBoard.Coordinate: CustomReflectable {
var customMirror: 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) // ChessBoard.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 customMirror
property
to access the type metatype and display style,
and enumerate each of the mirror’s children.
b8.customMirror.subjectType // ChessBoard.Coordinate.Type
b8.customMirror.displayStyle // struct
for child in b8.customMirror.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 displayStyle
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 ChessBoard.Coordinate: CustomReflectable {
var customMirror: Mirror {
return Mirror(self,
children: ["rank": rank, "file": file])
displayStyle: displayStyle)
}
}
Style
Output
class
ChessBoard.Coordinate
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 ChessBoard.Coordinate: CustomReflectable {
var customMirror: Mirror {
return Mirror(self,
unlabeledChildren: [rank, file],
displayStyle: displayStyle)
}
}
Style
Output
class
ChessBoard.Coordinate
struct
Coordinate()
enum
Coordinate(8)
optional
8
tuple
(8, 2)
collection
[8, 2]
set
{8, 2}
dictionary
[ 8, 2]
You can’t change how tuples are reflected.
As described in our article about Void
,
tuples aren’t nominal types,
so you can’t write extensions to add conformance to CustomReflectable
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: ChessBoard.Coordinate?
init(color: Color, location: ChessBoard.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 displayStyle
to struct
so that we get more reflected information than we would otherwise for a class):
extension Piece: CustomReflectable {
var customMirror: Mirror {
return Mirror(self,
children: ["color": color,
"location": location ?? "N/A"],
displayStyle: .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 customMirror: 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 CustomReflectable
called CustomLeafReflectable
.
If a class conforms to CustomLeafReflectable
,
its reflections of subclasses will be suppressed
unless they explicitly override customMirror
and provide their own Mirror
.
Let’s take another pass at our example from before,
this time adopting CustomLeafReflectable
in our initial declaration:
class Piece: CustomLeafReflectable {
…
var customMirror: Mirror {
return Mirror(reflecting: self)
}
}
class Knight: Piece {
…
override var customMirror: Mirror {
return Mirror(self,
children: ["color": color,
"location": location ?? "N/A",
"value": Knight.value],
displayStyle: .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.
Types that wrap a single value like a string or number
can improve their reflected representation
simply by conforming to the CustomStringConvertible
protocol.
For example,
the nested Color
type can be unambiguously understood
by its raw value alone:
extension Piece.Color: CustomStringConvertible {
var description: String {
return self.rawValue
}
}
With this in place,
the raw string values will be used
anytime Piece.Color
is reflected
or shows up as a child in a mirror representation.
String(reflecting: Piece.Color.black) // black
String(reflecting: knight)
// Knight(color: black, location: (rank: 8, file: 2), value: 3)
Option 2: Encode Type in Superclass Mirror
A simpler alternative to going the CustomLeafReflectable
route
is to include the type information as a child of the mirror
declared in the base class.
extension Piece: CustomReflectable {
var customMirror: Mirror {
return Mirror(self,
children: ["type": type(of: self),
"color": color,
"location": location ?? "N/A"],
displayStyle: .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 CustomLeafReflectable
.
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: ChessBoard.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: ChessBoard.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 customMirror: Mirror {
return Mirror(self,
children: ["color": color,
"location": location ?? "N/A"],
displayStyle: .struct)
}
}
extension Valuable{
var customMirror: Mirror {
return Mirror(self,
children: ["color": color,
"location": location ?? "N/A",
"value": Self.value],
displayStyle: .struct)
}
}
…
extension Knight: CustomReflectable {}
…
Putting that all together, we get exactly the behavior we want with the advantage of value semantics.
let b8 = ChessBoard.Coordinate(rank: 8, file: 2)
let knight = Knight(color: .black, location: b8)
String(reflecting: knight)
// Knight(color: black, location: (rank: 8, file: 2))
Ultimately,
CustomLeafReflectable
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 CaseIterable
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.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK