Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Swift Generic Protocols: What Are They Good For?

DZone's Guide to

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.

· Mobile Zone
Free Resource

Download this comprehensive Mobile Testing Reference Guide to help prioritize which mobile devices and OSs to test against, brought to you in partnership with Sauce Labs.

In 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.

Analysts agree that a mix of emulators/simulators and real devices are necessary to optimize your mobile app testing - learn more in this white paper, brought to you in partnership with Sauce Labs.

Topics:
swift ,generics ,protocols

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}