ABSTRACT
In the last post, we discussed the basics of Swift generics, and how they are pretty much built into the language. One way this is demonstrated, is by the way that Swift implements generic (associated type) protocols. In this post, we’ll explore this a bit further.
THE BASIC DEFINITION
Instead of using the classic “<T>
” syntax, protocols define an “associatedtype
” keyword. This keyword replaces the “typealias
” keyword in older versions of Swift:
protocol GenericBaseProtocol { associatedtype T var myProperty: T {get set} init(_ myProperty: T ) }
The above makes the protocol type generic. It will require you to declare a typealias
, in order to define T
:
// You can declare both Comparable and non-Comparable types with this protocol. // This is Comparable struct GenStructB: GenericBaseProtocol { typealias T = Int var myProperty: T = 0 init(_ myProperty: T ) { self.myProperty = myProperty } } // This is non-Comparable class GenClassA: GenericBaseProtocol { typealias T = [String:String] var myProperty: T = [:] required init(_ myProperty: T ) { self.myProperty = myProperty } }
Note that the above does not make the protocol, itself, generic. It makes internal data types generic, and requires that these data types be specifically assigned at the time classes (or structs) are defined from the protocol.
MULTIPLE ASSOCIATED TYPES
You can have multiple associated types:
protocol GenericBaseProtocolWithTwoTypes { associatedtype T associatedtype S var myProperty1: T {get set} var myProperty2: S {get set} init(myProperty1: T, myProperty2: S) }
Which requires you to define and implement BOTH types:
struct GenericTwoTypesStruct: GenericBaseProtocolWithTwoTypes { typealias T = Int typealias S = [String:String] var myProperty1: T var myProperty2: S init(myProperty1: T, myProperty2: S) { self.myProperty1 = myProperty1 self.myProperty2 = myProperty2 } } class GenericTwoTypesClass: GenericBaseProtocolWithTwoTypes { typealias T = Int typealias S = [String:String] var myProperty1: T var myProperty2: S required init(myProperty1: T, myProperty2: S) { self.myProperty1 = myProperty1 self.myProperty2 = myProperty2 } }
This will not work (Trying to define and use only one of the associated types):
class GenericTwoTypes: GenericBaseProtocolWithTwoTypes { typealias T = Int var myProperty1: T required init(myProperty1: T) { self.myProperty1 = myProperty1 } }
INTO ACTION
Now, let’s look at how we can implement and specialize the generics.
Let’s begin by saying we’d like our protocol to act as an “Equatable
” or “Comparable
” protocol. These allow instances to be compared for equality and/or precedence.
There’s a couple of ways that we can do that:
Declaring the Protocol to be Comparable
In this method, we actually declare the protocol as an extension of the Comparable
Swift Behavior protocol:
protocol GenericBaseProtocolBothCompType: Comparable { associatedtype T: Comparable var myProperty: T {get set} init(_ myProperty: T ) }
Note that both the protocol and the associated type are defined to be Comparable
.
If we do this, then we’ll need to make sure that we implement at least the required Equatable
method:
static func == (lhs: Self, rhs: Self) -> Bool
We are also required to specify one of the Comparable
required static funtions:
static func < (lhs: Self, rhs: Self) -> Bool
Like so:
// This extension satisfies the Equatable protocol. extension GenericBaseProtocolBothCompType { static func ==(lhs: Self, rhs: Self) -> Bool { return lhs.myProperty == rhs.myProperty } } // This extension satisfies the Comparable protocol. extension GenericBaseProtocolBothCompType { static func <(lhs: Self, rhs: Self) -> Bool { return lhs.myProperty < rhs.myProperty } }
The associated type needs to be Comparable
, because that allows these functions to work. If it is not restricted to Comparable
, then you’ll get syntax errors when trying to define them.
Note that we are using protocol extensions to define these functions as default implementations.
Adding Conditional Protocol Extension Functions
The other method allows us to extend the entirely generic protocol that we defined initially. We do this by making the implementation of the functions, themselves, to be dependent on whether or not the type T
is Comparable
:
// This extension uses the Equatable protocol (Comparable extends Equatable). Note the capitalized "Self". extension GenericBaseProtocol where T: Equatable { static func ==(lhs: Self, rhs: Self) -> Bool { return lhs.myProperty == rhs.myProperty } } // This extension uses the Comparable protocol. extension GenericBaseProtocol where T: Comparable { static func <(lhs: Self, rhs: Self) -> Bool { return lhs.myProperty < rhs.myProperty } static func >(lhs: Self, rhs: Self) -> Bool { return lhs.myProperty > rhs.myProperty } }
In the above case, we have extended the entirely generic protocol. However, these extensions will ONLY be available if type T
is Equatable
and/or Comparable
. Let’s prove this.
First, we’ll declare a class that uses Int
(Int
is a Comparable
type):
// Test the Comparable behavior let lhs2 = GenClassB3(3) let rhs2 = GenClassB3(4) let leftEqualToRight2 = lhs2 == rhs2 let leftGreaterThanRight2 = lhs2 > rhs2 let leftLessThanRight2 = lhs2 < rhs2 let rightEqualToLeft2 = rhs2 == lhs2 let rightGreaterThanLeft2 = rhs2 > lhs2 let rightLessThanLeft2 = rhs2 < lhs2 let leftEqualToLeft2 = lhs2 == GenClassB3(3) let rightEqualToRight2 = lhs2 == GenClassB3(4)
That all works as expected.
Now, lets use an Array
of String
([String]
is not directly Equatable
):
// Here, we define a type that is not Comparable. class GenClassB3A: GenericBaseProtocol { typealias T = [String] var myProperty: T = [] required init(_ myProperty: T ) { self.myProperty = myProperty } } let lhs2A = GenClassB3A(["HI"]) let rhs2A = GenClassB3A(["Howaya"])
None of this will work, because the class that was created does not have an Equatable
type:
let leftEqualToRight2A = lhs2A == rhs2A let leftEqualToRight2A = lhs2A == rhs2A let leftGreaterThanRight2A = lhs2A > rhs2A let leftLessThanRight2A = lhs2A < rhs2A let rightEqualToLeft2A = rhs2A == lhs2A let rightGreaterThanLeft2A = rhs2A > lhs2A let rightLessThanLeft2A = rhs2A < lhs2A let leftEqualToLeft2A = lhs2A == GenClassB3A(["HI"]) let rightEqualToRight2A = lhs2A == GenClassB3A(["Howaya"])
However…
As I demonstrate in the sample playground below, the “parameterized” extensions (generally using the where
clause) act like high-specificity CSS. In other words, if an extension is more specific than another one, it is used instead of the less-specific one. In particular, here:
// This is the default extension, implementing non-functional stubs. extension GenericBaseProtocol { static func ==(lhs: Self, rhs: Self) -> Bool { return false } static func <(lhs: Self, rhs: Self) -> Bool { return false } static func >(lhs: Self, rhs: Self) -> Bool { return false } var isEquatable:Bool { return false } var isComparable:Bool { return false } }
The above is the least-specific or default, extension. It is used when there are no more specific versions. We take advantage of this to return a false for a conformance test, and give non-functional operator stubs.
Below, we declare a couple of more specific extensions. If the associated type is Equatable
, then the first extension below overrides the ==()
operator and isEquatable
calculated property.
If the type is Comparable
, then all of the operators are overridden, and the isComparable
calculated property returns true.
// This extension is used when the associated type conforms to the Equatable protocol. extension GenericBaseProtocol where T: Equatable { static func ==(lhs: Self, rhs: Self) -> Bool { return lhs.myProperty == rhs.myProperty } var isEquatable:Bool { return true } } // This extension is used when the associated type conforms to the Comparable protocol. extension GenericBaseProtocol where T: Comparable { static func <(lhs: Self, rhs: Self) -> Bool { return lhs.myProperty < rhs.myProperty } static func >(lhs: Self, rhs: Self) -> Bool { return lhs.myProperty > rhs.myProperty } var isComparable:Bool { return true } }
As you can see, if you run the playground, you don’t have to split up the “generic” playground. Like CSS, only the part that is less specific is overridden. The stuff that is still the most specific (even though it is the default “general” extension) is still applied.
Let’s explore this a bit more closely.
Here, we define an instance of a class with a Comparable
data type (Int
):
// Here, we define a Comparable type
class GenClassB3: GenericBaseProtocol {
typealias T = Int
var myProperty: T = 0
required init(_ myProperty: T ) {
self.myProperty = myProperty
}
}
This will cause the two extensions (both of them) to override the default extension:
// This is the default extension, implementing non-functional stubs. extension GenericBaseProtocol { static func ==(lhs: Self, rhs: Self) -> Bool { return false } static func <(lhs: Self, rhs: Self) -> Bool { return false } static func >(lhs: Self, rhs: Self) -> Bool { return false } var isEquatable:Bool { return false } var isComparable:Bool { return false } }// This extension uses the Equatable protocol (Comparable extends Equatable). Note the capitalized "Self". // If the class is Equatable, then we return a true for isEquatable. // This extension is used when the associated type conforms to the Equatable protocol. extension GenericBaseProtocol where T: Equatable { static func ==(lhs: Self, rhs: Self) -> Bool { return lhs.myProperty == rhs.myProperty } var isEquatable:Bool { return true } } // This extension is used when the associated type conforms to the Comparable protocol. extension GenericBaseProtocol where T: Comparable { static func <(lhs: Self, rhs: Self) -> Bool { return lhs.myProperty < rhs.myProperty } static func >(lhs: Self, rhs: Self) -> Bool { return lhs.myProperty > rhs.myProperty } var isComparable:Bool { return true } }
Next, we’ll use a data type that is Equatable
, but not Comparable
(Bool
). This will cause only the Equatable
-constrained extension to take effect:
// Here, we define an Equatable type class GenClassB4: GenericBaseProtocol { typealias T = Bool var myProperty: T = false required init(_ myProperty: T ) { self.myProperty = myProperty } } // This is the default extension, implementing non-functional stubs. extension GenericBaseProtocol { static func ==(lhs: Self, rhs: Self) -> Bool { return false } static func <(lhs: Self, rhs: Self) -> Bool { return false } static func >(lhs: Self, rhs: Self) -> Bool { return false } var isEquatable:Bool { return false } var isComparable:Bool { return false } }// This extension uses the Equatable protocol (Comparable extends Equatable). Note the capitalized "Self". // If the class is Equatable, then we return a true for isEquatable. // This extension is used when the associated type conforms to the Equatable protocol. extension GenericBaseProtocol where T: Equatable { static func ==(lhs: Self, rhs: Self) -> Bool { return lhs.myProperty == rhs.myProperty } var isEquatable:Bool { return true } }// This extension is used when the associated type conforms to the Comparable protocol. extension GenericBaseProtocol where T: Comparable { static func <(lhs: Self, rhs: Self) -> Bool { return lhs.myProperty < rhs.myProperty } static func >(lhs: Self, rhs: Self) -> Bool { return lhs.myProperty > rhs.myProperty } var isComparable:Bool { return true } }
Finally, we’ll define a type that is neither Equatable
, nor Comparable
(An Array
of String
). This will cause the protocol to fall back to its default implementation:
// Here, we define a type that is neither Equatable, nor Comparable class GenClassB5: GenericBaseProtocol { typealias T = [String] var myProperty: T = [] required init(_ myProperty: T ) { self.myProperty = myProperty } }// This is the default extension, implementing non-functional stubs. extension GenericBaseProtocol { static func ==(lhs: Self, rhs: Self) -> Bool { return false } static func <(lhs: Self, rhs: Self) -> Bool { return false } static func >(lhs: Self, rhs: Self) -> Bool { return false } var isEquatable:Bool { return false } var isComparable:Bool { return false } }// This extension uses the Equatable protocol (Comparable extends Equatable). Note the capitalized "Self". // If the class is Equatable, then we return a true for isEquatable. // This extension is used when the associated type conforms to the Equatable protocol. extension GenericBaseProtocol where T: Equatable { static func ==(lhs: Self, rhs: Self) -> Bool { return lhs.myProperty == rhs.myProperty } var isEquatable:Bool { return true } } // This extension is used when the associated type conforms to the Comparable protocol. extension GenericBaseProtocol where T: Comparable { static func <(lhs: Self, rhs: Self) -> Bool { return lhs.myProperty < rhs.myProperty } static func >(lhs: Self, rhs: Self) -> Bool { return lhs.myProperty > rhs.myProperty } var isComparable:Bool { return true } }
CONCLUSION
Swift has a really nice implementation of generic programming. It has generics built into the fundamental language, itself, and its use of associated types in protocols is especially effective.
On this page, I just briefly touched on the possibilities of the use of generics in associated types, and demonstrated the way the where
clause can be applied to give you “smart protocols,” based on type constraints.
SAMPLE PLAYGROUND
// This is a completely generic protocol. You can use any type for "T". protocol GenericBaseProtocol { associatedtype T var myProperty: T {get set} init(_ myProperty: T ) } // You can declare both Comparable and non-Comparable types with this protocol. // This is Comparable class GenClassB: GenericBaseProtocol { typealias T = Int var myProperty: T = 0 // When you conform to a protocol with an init(), you need to add the "required" keyword to your implementation. required init(_ myProperty: T ) { self.myProperty = myProperty } } // This is non-Comparable class GenClassA: GenericBaseProtocol { typealias T = [String:String] var myProperty: T = [:] required init(_ myProperty: T ) { self.myProperty = myProperty } } // This will not work. You cannot redefine associated types in a protocol extension. //extension GenericBaseProtocol { // associatedtype T: Comparable // // var myProperty: T {get set} //} // This will not work. You cannot add an associatedType via an extension. //extension GenericBaseProtocol { // associatedtype S //} // Now, here we will add conditional extensions to the original, generic protocol. // These allow classes based on this protocol to act as Equatable and Comparable, if the data type is Comparable, // or just Equatable, if the data type is Equatable, but not Comparable. // This extension is for when the class is not Equatable. // The == always returns false, and we have an isEquatable Bool that tells us the class is not Equatable. // Thanks to Alain T. for his guidance: https://stackoverflow.com/a/48711730/879365 // This is the default extension, implementing non-functional stubs. extension GenericBaseProtocol { static func ==(lhs: Self, rhs: Self) -> Bool { return false } static func <(lhs: Self, rhs: Self) -> Bool { return false } static func >(lhs: Self, rhs: Self) -> Bool { return false } var isEquatable:Bool { return false } var isComparable:Bool { return false } } // This extension uses the Equatable protocol (Comparable extends Equatable). Note the capitalized "Self". // If the class is Equatable, then we return a true for isEquatable. // This extension is used when the associated type conforms to the Equatable protocol. extension GenericBaseProtocol where T: Equatable { static func ==(lhs: Self, rhs: Self) -> Bool { return lhs.myProperty == rhs.myProperty } var isEquatable:Bool { return true } } // This extension is used when the associated type conforms to the Comparable protocol. extension GenericBaseProtocol where T: Comparable { static func <(lhs: Self, rhs: Self) -> Bool { return lhs.myProperty < rhs.myProperty } static func >(lhs: Self, rhs: Self) -> Bool { return lhs.myProperty > rhs.myProperty } var isComparable:Bool { return true } } // Here, we define a Comparable type class GenClassB3: GenericBaseProtocol { typealias T = Int var myProperty: T = 0 required init(_ myProperty: T ) { self.myProperty = myProperty } } // Test the Comparable behavior let lhs2 = GenClassB3(3) let rhs2 = GenClassB3(4) print ( "lhs2 is" + (lhs2.isEquatable ? "" : " not") + " Equatable." ) print ( "lhs2 is" + (lhs2.isComparable ? "" : " not") + " Comparable." ) print ( "rhs2 is" + (rhs2.isEquatable ? "" : " not") + " Equatable." ) print ( "rhs2 is" + (rhs2.isComparable ? "" : " not") + " Comparable." ) let leftEqualToRight2 = lhs2 == rhs2 let leftGreaterThanRight2 = lhs2 > rhs2 let leftLessThanRight2 = lhs2 < rhs2 let rightEqualToLeft2 = rhs2 == lhs2 let rightGreaterThanLeft2 = rhs2 > lhs2 let rightLessThanLeft2 = rhs2 < lhs2 let leftEqualToLeft2 = lhs2 == GenClassB3(3) let rightEqualToRight2 = lhs2 == GenClassB3(4) // Here, we define a type that is not Comparable. class GenClassB3A: GenericBaseProtocol { typealias T = [String] var myProperty: T = [] required init(_ myProperty: T ) { self.myProperty = myProperty } } let lhs2A = GenClassB3A(["HI"]) let rhs2A = GenClassB3A(["Howaya"]) print ( "lhs2A is" + (lhs2A.isEquatable ? "" : " not") + " Equatable." ) print ( "lhs2A is" + (lhs2A.isComparable ? "" : " not") + " Comparable." ) print ( "rhs2A is" + (rhs2A.isEquatable ? "" : " not") + " Equatable." ) print ( "rhs2A is" + (rhs2A.isComparable ? "" : " not") + " Comparable." ) // Because of the game we played up there, these will compile, but the comparisons will always fail. let leftEqualToRight2A = lhs2A == rhs2A let leftGreaterThanRight2A = lhs2A > rhs2A let leftLessThanRight2A = lhs2A < rhs2A let rightEqualToLeft2A = rhs2A == lhs2A let rightGreaterThanLeft2A = rhs2A > lhs2A let rightLessThanLeft2A = rhs2A < lhs2A let leftEqualToLeft2A = lhs2A == GenClassB3A(["HI"]) let rightEqualToRight2A = lhs2A == GenClassB3A(["Howaya"]) // Here, we define an Equatable (but not Comparable) type class GenClassB4: GenericBaseProtocol { typealias T = Bool var myProperty: T = false required init(_ myProperty: T ) { self.myProperty = myProperty } } let lhs2B = GenClassB4(true) let rhs2B = GenClassB4(true) print ( "lhs2B is" + (lhs2B.isEquatable ? "" : " not") + " Equatable." ) print ( "lhs2B is" + (lhs2B.isComparable ? "" : " not") + " Comparable." ) print ( "rhs2B is" + (rhs2B.isEquatable ? "" : " not") + " Equatable." ) print ( "rhs2B is" + (rhs2B.isComparable ? "" : " not") + " Comparable." ) let leftEqualToRight2B = lhs2B == rhs2B let leftGreaterThanRight2B = lhs2B > rhs2B let leftLessThanRight2B = lhs2B < rhs2B let rightEqualToLeft2B = rhs2B == lhs2B let rightGreaterThanLeft2B = rhs2B > lhs2B let rightLessThanLeft2B = rhs2B < lhs2B let leftEqualToLeft2B = lhs2B == GenClassB4(true) let rightEqualToRight2B = lhs2B == GenClassB4(false) // This defines a new protocol, based on Comparable. protocol GenericBaseProtocolBothCompType: Comparable { associatedtype T: Comparable var myProperty: T {get set} init(_ myProperty: T ) } // You cannot define a protocol extension initializer. //extension GenericBaseProtocolBothCompType { // init(_ myProperty: T ) { // self.myProperty = myProperty // } //} // This extension satisfies the Equatable protocol. extension GenericBaseProtocolBothCompType { static func ==(lhs: Self, rhs: Self) -> Bool { return lhs.myProperty == rhs.myProperty } } // This extension satisfies the Comparable protocol. extension GenericBaseProtocolBothCompType { static func <(lhs: Self, rhs: Self) -> Bool { return lhs.myProperty < rhs.myProperty } } // This is an error. It will not work, because Dictionary is not Comparable. //class GenClassA2: GenericBaseProtocolBothCompType { // typealias T = [String:String] // var myProperty: T = [:] //} class GenClassB2: GenericBaseProtocolBothCompType { typealias T = Int var myProperty: T = 0 required init(_ myProperty: T ) { self.myProperty = myProperty } } let lhs = GenClassB2(3) let rhs = GenClassB2(4) let leftEqualToRight = lhs == rhs let leftGreaterThanRight = lhs > rhs let leftLessThanRight = lhs < rhs let rightEqualToLeft = rhs == lhs let rightGreaterThanLeft = rhs > lhs let rightLessThanLeft = rhs < lhs let leftEqualToLeft = lhs == GenClassB2(3) let rightEqualToRight = lhs == GenClassB2(4) // This defines a new protocol, based on Comparable, and leaves the associated type as a free agent. protocol GenericBaseProtocolBothCompType2: Comparable { associatedtype T var myProperty: T {get set} init(_ myProperty: T ) } // However, these won't work, as the data type is not Comparable. //// This extension satisfies the Equatable protocol. //extension GenericBaseProtocolBothCompType2 { // static func ==(lhs: Self, rhs: Self) -> Bool { // return lhs.myProperty == rhs.myProperty // } //} // //// This extension satisfies the Comparable protocol. //extension GenericBaseProtocolBothCompType2 { // static func <(lhs: Self, rhs: Self) -> Bool { // return lhs.myProperty < rhs.myProperty // } //} // These will work, because you are specifying that the type needs to be Equatable/Comparable. // This extension satisfies the Equatable protocol. extension GenericBaseProtocolBothCompType2 where T: Equatable { static func ==(lhs: Self, rhs: Self) -> Bool { return lhs.myProperty == rhs.myProperty } } // This extension satisfies the Comparable protocol. extension GenericBaseProtocolBothCompType2 where T: Comparable { static func <(lhs: Self, rhs: Self) -> Bool { return lhs.myProperty < rhs.myProperty } } class GenericClassFromType2: GenericBaseProtocolBothCompType2 { typealias T = Int var myProperty: T = 0 required init(_ myProperty: T ) { self.myProperty = myProperty } } let lhs4 = GenericClassFromType2(3) let rhs4 = GenericClassFromType2(4) let leftEqualToRigh4t = lhs4 == rhs4 let leftGreaterThanRight4 = lhs4 > rhs4 let leftLessThanRight4 = lhs4 < rhs4 let rightEqualToLeft4 = rhs4 == lhs4 let rightGreaterThanLeft4 = rhs4 > lhs4 let rightLessThanLeft4 = rhs4 < lhs4 let leftEqualToLeft4 = lhs4 == GenericClassFromType2(3) let rightEqualToRight4 = lhs4 == GenericClassFromType2(4) // This protocol has two associated types. protocol GenericBaseProtocolWithTwoTypes { associatedtype T associatedtype S var myProperty1: T {get set} var myProperty2: S {get set} init(myProperty1: T, myProperty2: S) } // This will not work. You need to define and implement BOTH types. //class GenericTwoTypes: GenericBaseProtocolWithTwoTypes { // typealias T = Int // // var myProperty1: T // // required init(myProperty1: T) { // self.myProperty1 = myProperty1 // } //} struct GenericTwoTypesStruct: GenericBaseProtocolWithTwoTypes { typealias T = Int typealias S = [String:String] var myProperty1: T var myProperty2: S // You do not need the "required" keyword when implementing a struct from the protocol. init(myProperty1: T, myProperty2: S) { self.myProperty1 = myProperty1 self.myProperty2 = myProperty2 } } class GenericTwoTypesClass: GenericBaseProtocolWithTwoTypes { typealias T = Int typealias S = [String:String] var myProperty1: T var myProperty2: S required init(myProperty1: T, myProperty2: S) { self.myProperty1 = myProperty1 self.myProperty2 = myProperty2 } }