Swift Generic Protocols: What Are They Good For?
Generic protocols can’t be used they way we’re used to using them, that’s true. But they still serve a very important purpose in the swift world—defining type relationships.
Join the DZone community and get the full member experience.
Join For FreeIn the good old days, we had a pretty clear understanding of what we used protocols for, and we used them as interfaces. They were treated as first-order types that any conforming structure could hide behind. It was a pretty simple use case, and we were all accustomed to it. I mean really, just about every non-duck-typed programming language had this kind of construct. It didn’t matter if you called it a protocol, and interface, or a pure abstract class, we knew what these things were and how to use them.
Then Swift comes along and throws a wrench into everything with associated types.
For those of you who don’t know, an associated type as a type defined within a protocol with the typealias keyword. For example, in the NiftyProtocol
protocol, the SomeType
and SomeOtherType
types are declared as types, but are not defined within the protocol.
protocol NiftyProtocol {
typealias SomeType
typealias SomeOtherType
func someFunc(info: SomeType) -> SomeOtherType
}
These abstract types are called associated types in the Swift world. The thing is, you can’t do this with a protocol that contains associated types:
var niftyThing = NiftyProtocolImpl()
The compiler just won’t let you. Swift works really hard to make sure that all types and type relationships are clearly defined, and the niftyThing
object still has associated types hanging around, completely undefined. If the protocol didn’t have associated types, this would work just fine, but the protocol does, so it doesn’t.
So, if I can’t use these protocols for type abstraction, what are they good for? I mean, isn’t that why we have them in the first place?
What Generic Protocols Are Good For
Generic protocols can’t be used they way we’re used to using them, that’s true. But they still serve a very important purpose in the swift world—defining type relationships.
So Swift is designed to be strongly typed. It aggressively infers type information from other type definitions, and even requires developers to explicitly define nullable types (see optionals). The swift compiler is not about to let you be lazy with your typing after all the work it has to put in to try to ensure type correctness, believe me. So, what does this look like in practice?
Well, let’s start with some protocol definitions:
public protocol Automobile {
typealias FuelType
typealias ExhaustType
func drive(fuel: FuelType) -> ExhaustType
}
public protocol Fuel {
typealias ExhaustType
func consume() -> ExhaustType
}
public protocol Exhaust {
init()
func emit()
}
These are pretty simple but show some interesting type relationships. Here, the Automobile protocol is dependent on some FuelType
as well as some ExhaustType
. The Fuel
protocol is also dependent on some ExhaustType
, while the Exhaust
protocol is statically well defined.
Now, let’s look at some implementations:
public struct UnleadedGasoline<E: Exhaust>: Fuel {
public func consume() -> E {
print("...consuming unleaded gas...")
return E()
}
}
public struct CleanExhaust: Exhaust {
public init() {}
public func emit() {
print("...this is some clean exhaust...")
}
}
public class Car<F: Fuel,E: Exhaust where F.ExhaustType == E>: Automobile {
public func drive(fuel: F) -> E {
return fuel.consume()
}
}
We have the same type relationships in these implementations, but we need to declare relationships between types in order to appropriately constrain everything. The CleanExhaust
structure conforms to the Exhaust
protocol, which is already well defined. It can’t be generically defined as a result. At least, it can’t genericize any attributes associated with the Exhaust
protocol. The UnleadedGasoline
struct is a bit more flexible. Remember the Fuel
protocol has an associated type, ExhaustType
. This type doesn’t need to be explicitly defined within the UnleadedGasoline
structure, but if you don’t, you need to genericize the UnleadedGasoline
definition to that it can be associated with a concrete type when used. I’ve genericized the structure here, and specified that the E type must conform to the Exhaust
protocol.
Finally, take a look at the Car class - this is where the types are finally all related to one another. The Automobile
protocol defines the FuelType
and the ExhaustType
, and as the Fuel
protocol is also dependent on an ExhaustType
, we need to make sure that the Fuel
conforming object returns the same. Here, we do so by defining the ExhaustType
and ensuring that the ExhaustType
implemented by the submitted Fuel
conforming object is the same as that submitted via the class field in the class definition. We do this here via a where clause.
This where clause is vital in this design to tie together all the types into a cohesive dependency graph. We run the example like this:
var fusion = Car<UnleadedGasoline<CleanExhaust>, CleanExhaust>()
fusion
.drive(UnleadedGasoline<CleanExhaust>())
.emit()
And the output should be:
...consuming unleaded gas...
...this is some clean exhaust...
Now, there’s another way to do this as well, that’s arguably better and still ties the types together:
public class Car<F: Fuel>: Automobile {
public func drive(fuel: F) -> F.ExhaustType {
return fuel.consume()
}
}
var fusion = Car<UnleadedGasoline<CleanExhaust>>()
Here, we tie the types together via the use of the F.ExhaustType
associated type defined over the Fuel
protocol. The output from the example’s the same in this case, even with the slightly more terse type definitions.
So, what’re generic protocols good for in Swift? They allow you to define type relationships and defer concrete definitions of those type relationships. This leads to stronger typing, more robust design capabilities, and the possibility for more robust software when implemented correctly. The downside is more potential complexity and difficulty in understanding type relationships, though I’ve found that I’m thinking more and more easily in these terms as I work more in Swift. I doubted the effectiveness of associated types initially, but now that I understand them, I find myself using them more and more every day. I hope you do too.
Opinions expressed by DZone contributors are their own.
Comments