Mastering Swift Protocols
Understand Swift Protocols and use them correctly
In everyday jargon, we talk about protocols to refer to the set procedures or system of rules governing events. Whenever you are taking part in an event, you need to adhere to the protocol.
Comprehensive definition
Swift protocols are not different from events protocols, they too provide a means of defining a no result-guaranteed group of actions that a set of objects can perform or respond to.
Let’s unpack the key elements of the previous statement, and see how they can help understand Apple’s official definition of protocols: “A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality.”
- Perform or Respond to:
protocol OpenProtocol {
var debugDescription: String { get }
func doSomething()
}
A protocol may require:
- instance methods which are
the actions that the adopting entities must all be able to perform.
- computed properties which are in essence,
disguised getters.
Hence they fall within theactions that all adopting entities must be able to respond to.
Note: A protocol can be defined as conforming to an existing protocol, in that case, all the requirements of the parent protocol are ipso facto the requirements of the child protocol, and conformance to child protocol guarantees conformance to the parent protocol. This is referred to as protocol inheritance, just like classes, protocols can also be composed in that case we speak about protocol composition.
protocol OpenProtocol1: OpenProtocol { // inheritance
func doTheFirstThing()
}protocol OpenProtocol3: OpenProtocol { // inheritance
func doTheSecondThing()
}protocol ComposedProtocol: OpenProtocol, OpenProtocol3 {
// composition
}
2. No result-guaranteed
An object that conforms to a protocol promises that it will implement all non-optional methods of that protocol, but never makes any promise on the results.
Furthermore, an object that conforms to a protocol promises that it will provide all computed properties of the protocol, but never makes any promise on the value returned.
2. Set of objects
When a protocol is defined, it always restricts the kind of entities
ie. types
that can conform to it.
- Without any specification, the protocol is open and any type (class, struct, enum, etc.) can conform to it:
protocol OpenProtocol {
// can be adopted by any type
var debugDescription: String { get }
func doSomething()
}
A protocol can be class-bounded, to indicate that only class types can conform to it. Note that theclass
keyword used here is just a typealias
for AnyObject
.
protocol ClassBoundedProtocol: class {
// can only be adopted by classes
var debugDescription: String { get }
func doSomething()
}
A protocol can be bounded to a specific class, to indicate that only classes subclassing the specific class can conform to it.
protocol ErrorPresenter: UIViewController {
// can only be adopted by uiviewcontroller types
func hideAllErrors()
func show(error: Error)
func hide(error: Error)
}
It is important to mention that a protocol is adopted on the type level and not on the instance level. The latter makes it justifiable to allow for static requirements on a protocol, hence we can define a protocol by:
protocol StaticRequirementContainingProtocol: OpenProtocol {
// MARK: - Static Requirements static var typeName: String { get }
static func isTypeNameEquals(to comparingToTypeName: String) -> Bool
}
It is important to mention, when dealing with static requirements, that we need to use safe type accessors
such as type(of: T)
when we wish to perform static protocol requirements
calls on given objects of any conforming type.
func validateType(aProtocol: StaticRequirementContainingProtocol) -> Bool {
type(of: aProtocol).isTypeNameEquals(to: "")
}
Naming Convention
The complete Swift naming guidelines can be found here.
- Protocols names are
UpperCamelCase
. - Protocols that describe what something is should read as nouns (e.g.
Collection
). - Protocols that describe a capability should be named using the suffixes
able
,ible
, oring
(e.g.Equatable
,ProgressReporting
). - If an
existing type
is so tightly bound to a protocol’s role, avoid collision by appendingProtocol
to the protocol name. (e.g.CarEngineProtocol
)
Opaque Types
A function can return an opaque when there is a need to hide type identity. An opaque type is defined as some Protocol
where Protocol is the protocol behind which we wish to hide the type identity. A function returning an opaque is defined with the following signature:
func object(forKey key: String) -> some OpenProtocol
The key achievement of the above is that at the call site the returned variable is just anOpenProtocol
object. However, the compiler and the implementing class have full knowledge of the underlying concrete type of the object returned.
An opaque type can be seen as the reverse of a generic type. The types to which the generic types are mapped are dictated by the call site, and the function implementation is based on the abstract footprint of acceptable types. Those roles are reversed for a function with an opaque return type. An opaque type lets the function implementation pick the type for the value it returns in a way that is abstracted away from the call site.
Some possible applications of opaque types can bepublic contracts
orAPIs
that allow intercommunications between modules, frameworks. The public interface (public contract) may consist of operations like passing commands or requesting data; those operations can be restricted to return some Protocol
. Hiding type information is useful at the APIs
which constitute the boundaries between the various modules because the underlying type of the return value can remain private
.
public class AGivenModule { private struct AStruct: OpenProtocol { //private type
var debugDescription: String
func doSomething() {}
} private struct AnotherStruct: OpenProtocol { //private type
var debugDescription: String
func doSomething() {}
}private lazy var aProperty = AStruct(debugDescription: "Debug")
private lazy var anotherProperty = AnotherStruct(debugDescription: "Other") // Public API
public func object(forKey key: String) -> some OpenProtocol {
if aProperty.debugDescription==key {
return aProperty
} else if anotherProperty.debugDescription==key {
return anotherProperty
} return AStruct(debugDescription: "")
}
}
Protocol requirements
Properties
Properties requirements can be specified as get only
or both get and set
. You cannot have a set only
property requirement since you can only set something that you should be able to read.
Properties can be defined on an instance or a type level. The latter is achieved by the use of the static
qualifier. The class
cannot be used in protocol requirements since the protocol definition does not tie it to a particular class.
protocol PropertiesProtocol: class {
var aGetOnlyProperty: String { get }
var aGetAndSetProperty: String { get set } static var aTypeLevelProperty: String { get set } // class var aClassLevelProperty: String { set } // Not possible
// var aSetOnlyProperty: String { set } // Not possible
}
Methods
Instance and type methods can be required; those protocol methods cannot provide default parameters in the protocol definition. Variadic and generic parameters are allowed. Protocol methods cannot return opaque types. Similarly, with type property requirements, the only keyword allowed to indicate a type method is static.
Mutating methods are allowed in non-class bounded protocols and should be used when the method might modify the value of any of the properties of the adopting objects.
protocol SomeCache {
static func setTime(lifeTime: TimeInterval) // type method func registerForMemoryWarning() // intance method mutating func clear() // mutating instance method,
// only allowed in non-class bounded protocol func savedObject<T>() -> T? // Generic method
func store(value: Any...) // Method with variadic parameters
}
Initializers
Initializer requirements, which can be failable or not, should be defined
without the func
keyword and must be named init
.
protocol SomeCollectionProcol {
init() // non failable init
}protocol SomeCollectionProcol {
init?(collectionLiteral: String) // failable init
}
Even though Swift allows for initializers requirements in Protocols, my personal take on this is that they should be avoided as much as possible. My argument is that abstractions, from the call site perspective, should ideally be post-existence and not pre-existence tools. The conforming objects must exist prior to applying any logic driven by the abstraction requirements onto them. By providing an
init
requirement, we are implicitely saying that we could create conforming objects using the protocol requirements. Given the fact that aninit
is a type and not instance method, this implies that we either know the type beforehand or we retrieve its underlying type via type accessors.
From the previous statement, arise the following questions:
- If we know the underlying concrete type, why bother to see it as an abstraction behind
protocol type,
especially if it is an object that is internally stored and used? - If we have a conforming object already, why bother getting its type just for the sake of creating another conforming object?
- Knowing that abstractions should not depend on details; are we not breaking
dependency inversion principle
by providing an init that receives concrete type details as some of its arguments?
If and only if, you can provide valid answers to the above questions, you can then consider having initializer requirements in your protocol definition.
Note: You will need to use type accessors and not the concrete type names to create protocol instances.
Protocol operations
is
operator
Theis
operator is used for checking for conformance against a Protocol
. It is used with the following syntax:
let doesConformToProtocol = anObject is SomeCache
The result is a boolean, evaluated to true if anObject
conforms to SomeCache
and false otherwise.
as
operator
The as
operator is used for casting a given object into Protocol
object. The casting can be performed forcefully using as!
(non-failable) or gracefully as?
(failable).
if let cache = anObject as? SomeCache {
cache.registerForMemoryWarning() // non optional
}// or(anObject as? SomeCache)?.registerForMemoryWarning()// orlet cache = anObject as? SomeCache
cache?.registerForMemoryWarning()
Note: While the as?
can be used at any point without any prior checks, as!
must only be used when the result is guaranteed to succeed. It should ideally be protected by a prior check against is
operator, which must have been evaluated to true.
if anObject is SomeCache {
registerForMemoryWarning(anObject)
}fun registerForMemory(_ anObject: AnyObject) {
let cache = anObject as! SomeCache
cache.registerForMemoryWarning()
}
&
operator
When an instance property or a method argument should conform to multiple Protocols
, one option is to represent its type by a new Protocol
that can be defined using protocol composition. Let’s assume two protocolsPrototolA
and ProtocolB;
we can build a third one,ProtocolC
as follows:
protocol ProtocolA {}
protocol ProtocolB {}
protocol ProtocolC: ProtocolA, ProtocolB {} // protocol compositionclass A {
var aProperty: ProtocolC
}func accept(protocol: ProtocolC) {}
However, protocol composition only makes sense when there is an empirical meaning of ProtocolC
. Whenever the latter does not have any essential meaning or does not establish a certain correlation between PrototolA
and ProtocolB
, it is errosnoues to use composition. To our rescue comes the &
operator, best suited to indicate the mere fact that a given object must simultaneously conform to multiple protocols and/or be of a certain concrete class-type.
class A {
var aProperty: ProtocolA & ProtocolB
}func accept(protocol: ProtocolA & ProtocolB) {}
The &
operator can be applied between multiple types provided that at least one the operands is aProtocol
and no more than one operand is of a class-type. When the need arise to reduce the verbosity, it can be achieved by the use oftypealias:
typealias PropertyType = ProtocolA & ProtocolB & ProtocolB & AClass
Protocol conformance
It is important to mention that a Protocol
does not impose the storage mechanism of the properties on adopting types. The protocol get only
requirements can be implemented as stored
(let and var) or computed
properties. Similarly, the get and set
requirements can either be implemented as stored properties(var only) or as computed properties providing respectively providing both a set and a get.
Even if default parameters are not allowed in the protocol methods definitions, the conforming types can implement the required methods with signatures providing default parameters. Though the compiler will validate the conformance to the protocol, at the call site of any of these methods on the protocol objects, one will still need to pass all parameters. The only advantage in implementing a protocol method with default parameters is to allow for the omission of those parameters when the said method is called on the object, seen as the representation of its concrete type but not a protocol object.
To illustrate the points in the two preceding paragraphs, let’s consider a protocol SomeCache
defined as follows:
protocol SomeCache {
var lifetime: String { get } // get only property
var storageCount: Double { get } // get only property var settableProperty: String { get set } // set and get property
var anotherSettableProperty: String { get set } // set and get property func invalidate(keepingStoredData shouldKeepStoreData: Bool)
}
and consider a struct conforming to it:
struct MyStruct: SomeCache {
let storage = NSMutableSet() private(set) let lifeTime: TimeInterval = 1000 // get only
// property requirement implemented as stored let property
var storageCount: Int { storage.count } // get only property
// implemented as computed property var settableProperty: String { // get and set
// property requirement implemented
// as computed properties providing getter and setter
get { "" }
set { /* perform the set operation */ }
} var anotherSettableProperty: String = "Test" // get and set
// property requirement implemented as stored var property
// method requirement implemented with default parameter func invalidate(keepingStoredData shouldKeepStoreData: Bool = true) { }}let concreteTypeObject: MyStruct = MyStruct()
concreteTypeObject.invalidate() // can be called considering default paramlet protocolObject: SomeCache = MyStruct()
protocolObject.invalidate(keepingStoredData: false) // cannot be called considering default param
Default Implementation
To provide a default implementation of a given requirement (property or method) ie. one that is conferred to any adopting types, you will have to use extensions. If you provide the implementations for all requirements, any qualifying type can adopt the protocol at no extra code addition cost, just mark the type as conforming to the protocol and voila. A type-specific implementation of a required method or property supersedes the default implementation.
protocol ProtocolWithExtensionProvingSomeRequirement {
var property: String { get }
var anotherProperty: Double { get }
}extension ProtocolWithExtensionProvingSomeRequirement {
var property: String { "A given string" }
}struct StructNotProvingOwnImplementation: ProtocolWithExtensionProvingSomeRequirement {
// needs to provide implementation for anotherProperty var anotherProperty: Double { 0.0 }}struct StructProvidingOwnImplementation: ProtocolWithExtensionProvingSomeRequirement {
// needs to provide implementation for anotherProperty var property: String { "This will be used instead " } // will be returned instead of the default implementation return value var anotherProperty: Double { 0.0 }}protocol ProtocolWithExtensionProvidingAllsRequirements {
var property: String { get }
}extension ProtocolWithExtensionProvidingAllsRequirements {
var property: String { "A given string" }
}struct StructProviding: ProtocolWithExtensionProvidingAllsRequirements {
// no need to provide any implementation}
When you find yourself writing a default implementation, especially of a method requirement with no code or with dummy code such as
print,
this is usually symptomatic of an incorrect interface segregation. Indeed, default implementation as the words stipulate are meant to be implementation and not no-implementation. The latter means that not all of the anticipated conforming types do not need that method, you should therefore consider breaking the protocols into many and using protocol composition to defined suitable protocols for those conforming types that need to provide all the methods and properties of the composed protocols.
Note: We will discuss optional methods in Objc Swift protocols that could help address the above issue.
Conformance for third-party types
It is only possible to mark a type as conforming to a protocol at its definition when one owns it, i.e it is defined in the current module. However, for types that are part of Swift, Objc, third-party library, framework, or an external module, that one doest not own, the conformance has to be done using type extension. The latter will be responsible for providing all requirements not already provided by the original implementation of the unowned type.
Let’s assume the type String
that is defined in Swift. If we wish to throw string objects as DisplayableError
objects, we will have to do so using extensions as follows:
protocol DisplayableError: Error {
var errorInfo: String { get }
mutating func invalide()
}extension String: DisplayableError {
var errorInfo: String { self }
func invalide() { self = "" }
}
The above type of conformance is said to be unconditional since it will be adopted by all String
types. We speak about conditional conformance
when there is a restriction on types to which the protocol requirements implementation is conferred. Assume, the protocol BoundedSummableCollection
defined as follows:
protocol BoundedSummableCollection {
var min: Double? { get }
var max: Double? { get }
func sum() -> Double?
}
If our intention is that only collections holding floating values should be able to conform to this protocol, we can provide the following conditional conformance:
extension Array:BoundedSummableCollection where Element: BinaryFloatingPoint { var max: Double? {
guard let maxElement = (sorted { (lhs, rhs) -> Bool in
lhs.magnitude > rhs.magnitude
}).first else
return nil
}
return Double(maxElement)
} var min: Double? {
guard let minElement = (sorted { (lhs, rhs) -> Bool in
lhs.magnitude < rhs.magnitude
}).first else {
return nil
}
return Double(minElement)
} func sum() -> Double {
let elementsSum = reduce(0,+)
return Double(elementsSum)
}
}
Note: We could have achieved the above using default implementations as follows:
extension BoundedSummableCollection where Self == Array<Double> { }
// constraint is a type, use '=='orextension BoundedSummableCollection where Element is Numberic {}
// constraint is a protocol, use 'is'
Note: As highlighted in the example, when the constraint is a concrete type, we use ==
and when the constraint is a protocol, we have to use is
.
In those extensions, we can respectively use self
as an object of type Array<Double> or an object conforming to Numeric
, which allows performing all manipulations needed to similarly achieve what we did using the conditional conformance. Just because a given type or its extension provides all protocol requirements, doesn’t make it protocol-conforming; protocol conformance is only guaranteed by an explicit adoption and is never imply. We need to explicitly declare that Array
conforms to BoundedSummableCollection
in order to “activate” the conformance.
extension Array: BoundedSummableCollection where Double == Int {}
Providing initializer requirements
Initializer requirements can be implemented either as designated or convenience initializers, with a compulsory need for therequired
modifier except for final classes because those can’t be subclassed.
protocol SomeCollectionProcol {
init?(collectionLiteral: String) // failable init
}class ACollectionClass: SomeCollectionProcol {
required init?(collectionLiteral: String) { }
}
required
is used to force all subclasses to provide an explicit or inherited implementation of the initializer requirement so that conformance is guaranteed for subclasses. If a subclass overrides a designated initializer from a superclass, and also implements a matching initializer requirement from a protocol, mark the initializer implementation with both therequired
andoverride
modifiers.
Associated Type Protocols
The BoundedSummableCollection
protocol has one major drawback. If its requirements apply to any Numeric
collections, the value types demanded by them on the other side, are not consistent with various Numerics
. Indeed, the maximum, the minimum and the sum of integers, unsigned integers, floats,
must all be respectively integers, unsigned integers, floats, double.
Currently, to apply BoundedSummableCollection
to any collections holding non-double type elements, we will have to go through various back-and-forth casting exercises. Associated types provide a means for allowing the adopting types to specify the dynamic types returned or accepted by a protocol’s requirements. The dynamic types are defined using the keyword associatedtype
, a given protocol can have many associated types, and associated types can have class-type or protocol constraints.
protocol BoundedSummableCollection {
associatedtype T: Numeric
// associated type with protocol constraint associatedtype MappingObjcContainerClass: NSObject
// associated type with type constraint
// must be a class type var min: T? { get }
var max: T? { get }
func sum() -> T?
}
With this improved definition of BoundedSummableCollection
, adopting collections must provide the type associated with T which is constrained to be a Numeric
and the one associated to MappingObjcContainerClass
which is constrained to be an NSObject
. Once the associated types are defined, the requirements must be implemented in terms of the specified types and not the generic types used in the protocol definition.
extension Array: BoundedSummableCollection where Element == Int {
typealias T = Int
typealias MappingObjcContainerClass = NSArray var max: Int? { // use Int? instead of T?
return (sorted { (lhs, rhs) -> Bool in
lhs.magnitude > rhs.magnitude
}).first
} var min: Int? { // use Int? instead of T?
return (sorted { (lhs, rhs) -> Bool in
lhs.magnitude < rhs.magnitude
}).first
} func sum() -> Int { reduce(0,+) } // // use Int instead of T
}
Let’s assume that we store a collection ofBoundedSummableCollections
let boundedSummables: [BoundedSummableCollection]
The compiler will throw the following error :
protocol
BoundedSummableCollection
can only be used as a generic constraint because it has Self or associated type requirements.
This problem is caused by the fact that Swift generics are not covariant, which would have allowed the compiler to use Any for the collection element type; type erasure is a technique used to circumvent this problem. A comprehensive document about type erasure can be found here.
ObjC Swift Protocols
For a Swift protocol to be used in the ObjC realm, it needs to be marked as @objc
. The immediate downside of marking a protocol as @objc
is that it can only be adopted only by class-types that inherit from NSObject
or @objc
class-types, excluding therefore a possible adoption by structures or enumerations. Conversely, one of the benefits of an@objc
marked Swift protocol is that it can contain optional requirements, which can be left unimplemented by conforming types. Requirements are made optional by preceding them with the optional
qualifier in the protocol’s definition, all optional requirements must be @objc
.
@objc protocol ObjcProtocol {
@objc optional var property: String { get }
var anotherProperty: Double { get }
@objc optional func doSomething()
}class AdoptingClass: ObjcProtocol {
// doesn't have to implement `property` & `doSomething` requirements var anotherProperty: Double { 0.0 }
}let protocolObject: ObjcProtocol = AdoptingClass()let property = protocolObject.property //Accessing optional property
protocolObject.doSomething?() // Calling optional method
None of the requirements of
@objc
protocol can accept or return anon-objc
types. Return types of optional properties are made optional by the compiler, optional method are called with a?
after the method name.
Applications for protocols
When a feature is present in a language, it does not suffice to understand it, it is also required to identify where its usage is best suited. In other terms, adding protocols in a codebase for the sake of doing so, is not commendable. The protocol-driven development trend has led to the widespread misuse of protocols. I have tried to identify some of the use cases where I believe, protocols
provide the greatest value at the lowest cost.
- Abstraction layers
Delegation is a design pattern that allows for an executing object(the delegating object) to make another object (the delegate) responsible for handling the results of some of its actions and/or observations. The formal Apple definition can be found here. The delegating and the delegate objects will be, one the low-level module and the other the high-level module. They should communicate via an abstraction according to the dependency Inversion Principle.
Use protocols to define the abstractions under which the delegate objects will be stored in delegating objects; make sure to class-bound delegate protocol
whenever you need to store the delegates weakly. For a deep understanding of Swift weak referencing
please read this article. Delegate protocols should be named with the “Delegate” suffix.
struct Contact {
let name: String
let surname: String
let cellPhoneNumber: String
let officeNumber: String
}class ContactRepository: ContactFetcherDelegate {
// delegate type, low level-module func contactController(_ contactController: ContactController,
didAddContact: Contact) {
/* update contact list storage */
} func contactController(_ contactController: ContactController,
didDelete: Contact) {
/* update contact list storage */
}
}struct ContactController { // delegating type, high-level module
public weak var delegate: ContactFetcherDelegate?
}protocol ContactFetcherDelegate: class {
// Abstraction layer between low and high-level modulefunc contactController(_ contactController: ContactController,
didAddContact: Contact)
// contactController uniquely identifies a ContactController
func contactController(_ contactController: ContactController,
didDelete: Contact)
}
It is important, and even required, that all delegate requirements convey information that uniquely identifies the delegating object since a single object can be the delegate of multiple delegating objects. The delegate can use that information as a differentiator.
In general, we can use Protocols
for any valid abstractions needed. You should be able to adequately define a suitable protocol
if you can correctly answer the following :
- Is the abstraction needed?
- What should the requirements of the abstraction?
- What should the constraints of the abstraction?
- Does it need dynamic behavior?
- Can we identify generic behavior for conforming types and provide default implementations?
2. Heterogeneous collections of types with commonalities
To store objects of different types in a type-constraint collection one can use Any
as the element type. However, collections are often created because of existing commonalities amongst the collection objects, as few as those could be. So, even if the collection objects are of different types, it is usually possible to identify the commonalities
and to extract them into a protocol
. Using a Protocol
rather than Any
as the element type of a collection offers the great advantage of reduced casting prior to acting on objects and increased predictability of the objects’ behavior.
let objets: [AnyObject] // not advisable, if possible identify
// common properties in any possible
// object of that can go into the collectionprotocol CollectionElement { // extract commonalities into a
// protocol
}let objets: [CollectionElement] // replace colletion element type
// with protocol
3. Type exclusion
When we need to restrict thetypes
that can be returned or accepted by a method, Protocols
come in handy. The most obvious example is Error
protocol
defined by Apple, it is defined as a no-requirement protocol, which appears to be odd at first glance. However, it plays the crucial role of restricting throwable
only to objects only conforming to Error
, hence prevents random unintentional type throws.
Assume we want a Swift atomic type
for primitives, we will need to make sure that an Atomic object can only be created with a type we know to be a primitive
. We can define the Primitive
and Atomic
types as follows:
protocol Primitive {} // primitive protocol
extension Int: Primitive {}
extension Bool: Primitive {}
extension: UInt: Primivite {}struct Atomic<Primitive> {
private var primitive: Primitive
}
4. Access restriction
Assume we want to ship a codebase that imports third-party modules
, and that some of the pre-existing models in the said modules don’t restrict access to their properties. If we wish to prevent erroneous updates of these models, we can hide them behind
a protocol that specifies requirements with stricter access policies
. In doing so, one must avoid the need for any additional lines of code by defining in such a way that conformance is guaranteed by the existing implementations of those pre-existing models.
class Person { // existing model that allows for modification of properties, that can be modified var name: String = ""
var surname: String = ""
var cellPhoneNumber: String = ""
var officeNumber: String = "" init() {}
}public protocol ContactDetailsProvider {
// restricting protocol, only provide get access var name: String { get }
var surname: String { get }
var cellPhoneNumber: String { get }
var officeNumber: String { get }
}extension Person: ContactDetailsProvider { } // No code-guaranteed conformance protocol ContactFetcherDelegate: class { // Abstraction layer
// between low and high-level module func contactController(_ contactController: ContactController,
didAddContact: Person) // hide Person
// object behind protocol with restricted access func contactController(_ contactController: ContactController,
didDelete: Person) // hide Person
// object behind protocol with restricted access
}
Additional documentation
- Protocols, https://docs.swift.org/swift-book/LanguageGuide/Protocols.html
- Delegation, https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/Delegation.html
- Friday Q&A 2015–11–20: Covariance and Contravariance, https://www.mikeash.com/pyblog/friday-qa-2015-11-20-covariance-and-contravariance.html
- Type Erasure in Swift, https://medium.com/@chris_dus/type-erasure-in-swift-84480c807534
Follow me on Twitter @SergeMbamba.