Protocol Witnesses an alternative to Protocols

9 minute read

Protocols are great, they allow us to write more flexible, modular, reusable and testable software.

Even Apple encourages Protocol oriented programming (POP) for many reasons, listed before and in general because it promotes code reuse, by defining once specific behaviors you can create code that works with any type that conforms to those protocols.

Working with protocols is a natural fit for Swift, Swift was designed with protocols in mind, and the language includes many features that make it easy to use.

So what’s protocol witness?

This term I heard from the PointFree video series that caught my attention.

Every time you adopt or conform to a protocol, you must provide all the requirements (methods, properties, etc)

Protocol witnesses are generated by the Swift compiler, when a protocol is used in a generic context, the compiler generates a separate implementation for each concrete type that conforms to the protocols.

These implementations called protocol witnesses, are optimized for the specific type.

If you have two concrete implementations of a protocol, Swift generates for you two witnesses one for each implementation.

This entry is an exercise to show how to translate the most used characteristics of protocols to concrete data types (Structs)

For convenience, I will take the same examples from the official documentation:

1. Property Requirements

protocol FullyNamed {
  var fullName: String { get }
}
struct Person: FullyNamed {
  var fullName: String
}
let john = Person(fullName: "John Appleseed")
john.fullName // "John Appleseed"

In this case, every time we see a property, we have a transformation from the self to a String

Something to notice is in a protocol always has an implicit reference to self

We achieve that by declaring a generic in our new Struct

struct FullyNaming<A> {
  let fullName: (A) -> String
}

struct Person {
  let fullName: String
}

// the witness for a Person
let naming = FullyNaming<Person>(fullName: {
  return "\($0.fullName)"
})

let john = Person(fullName: "John Appleseed")
naming.fullName(john) // "John Appleseed"

Another witness

struct Address {
  let street: String
  let detail: String
}

// the witness for an Address
let namingAddress = FullyNaming<Address>(fullName: {
  return $0.street + ", " + $0.detail
})
 
let home = Address(street: "Main Avenue", detail: "123")
namingAddress.fullName(home) // "Main Avenue, 123"

2. Method Requirements

protocol RandomNumberGenerator {
  func random() -> Double
}

class LinearCongruentialGenerator: RandomNumberGenerator {
  var lastRandom = 42.0
  let m = 139968.0
  let a = 3877.0
  let c = 29573.0

  func random() -> Double {
    lastRandom = ((lastRandom * a + c).truncatingRemainder(dividingBy:m))
    return lastRandom / m
  }
}

let generator = LinearCongruentialGenerator()

In this case, a method requirement is modeled using a closure

struct RandomNumberGenerating<A> {
  let random: (A) -> Double
}

class LinearCongruentialGenerator {
  var lastRandom = 42.0
  let m = 139968.0
  let a = 3877.0
  let c = 29573.0
}

// a witness for ``LinearCongruentialGenerator``
let generatingRandom = RandomNumberGenerating<LinearCongruentialGenerator>(
  random: {
    $0.lastRandom = (($0.lastRandom * $0.a + $0.c).truncatingRemainder(dividingBy: $0.m))
    return $0.lastRandom / $0.m
  }
)

let generator = LinearCongruentialGenerator()
print("Here's a random number: \( generatingRandom.random(generator) )")
print("And another one: \( generatingRandom.random(generator) )")

3. Mutating Method Requirements

protocol Togglable {
  mutating func toggle()
}

enum OnOffSwitch: Togglable {
  case off, on

  mutating func toggle() {
    switch self {
    case .off:
      self = .on
    case .on:
      self = .off
    }
  }
}

var lightSwitch = OnOffSwitch.off
//lightSwitch.toggle() // now the switch if ```on```

A mutating method means that the instance could change, so we must be explicit with using the inout keyword

struct Toggling<A> {
  let toogle: (inout A) -> Void
}

enum OnOffSwitch {
  case off, on
}

let toogling = Toggling<OnOffSwitch>(toogle: {
  switch $0 {
  case .off: $0 = .on
  case .on: $0 = .off
  }
})

var initialOffSwitch = OnOffSwitch.off
initialOffSwitch // off
  
// You must be explicit using ``&``
toogling.toogle(&initialOffSwitch)
initialOffSwitch // on

4. Initializer Requirements

protocol SomeProtocol {
  init(someParameter: Int)
}

class SomeClass: SomeProtocol {
  required init(someParameter: Int) {
    // initializer implementation goes here
  }
}
let instanceClass = SomeClass(someParameter: 5)

A init method means a way of create a new instance using the parameters defined in the protocol, in this case we could name as “create”, “build”, “generate”, etc

struct SomeWitness<A> {
  let create: (Int) -> A
}

class SomeClass {
  let value: Int
  init(value: Int) {
    self.value = value
  }
}
let witnesses = SomeWitness(create: {
  SomeClass(value: $0)
})

let newInstance = witnesses.create(5)

5. Protocol Inheritance

protocol TextRepresentable {
  var textualDescription: String { get }
}

// Inheritance
protocol PrettyTextRepresentable: TextRepresentable {
  var prettyTextualDescription: String { get }
}

// Concrete type Needs to conform to the 2 protocols
class SnakeAndLadders: PrettyTextRepresentable {
  var prettyTextualDescription: String { "pretty Description 🌱" }

  var textualDescription: String { "textual" }
}

let game = SnakeAndLadders()
print(game.textualDescription) // textual
print(game.prettyTextualDescription) // pretty Description 🌱

Inherit from another protocol means to have a property that refers to that witness

struct TextRepresenting<A> {
  let textualDescription: (A) -> String
}

struct PrettyTextRepresenting<A> {
  let textRepresenting: TextRepresenting<A> // inheritance
  let prettyTextualDescription: (A) -> String
}

class SnakeAndLadders { }

let game = SnakeAndLadders()

let textRepresenting = TextRepresenting<SnakeAndLadders>(textualDescription: { _ in
  "textual Witnesses"
})

let gameRepresenting = PrettyTextRepresenting<SnakeAndLadders>(
  textRepresenting: textRepresenting,
  prettyTextualDescription: { _ in "pretty Witness 🌿" }
)

print("\(gameRepresenting.textRepresenting.textualDescription(game))") // textual Witnesses
print("\(gameRepresenting.prettyTextualDescription(game))") // pretty Witness 🌿

6. Protocol Composition

protocol Named {
  var name: String { get }
}

protocol Aged {
  var age: Int { get }
}

struct Person: Named, Aged {
  var name: String
  var age: Int
}

func wishHappyBirthday(to celebrator: Named & Aged) {
  print("Happy birthday, \(celebrator.name), you're \(celebrator.age)!")
}

let birthdayPerson = Person(name: "Malcolm", age: 21)
wishHappyBirthday(to: birthdayPerson) // Happy birthday, Malcolm, you're 21!

struct Naming<A> {
  let name: (A) -> String
}

struct Aging<A> {
  let age: (A) -> Int
}

struct Person {
  var name: String
  var age: Int
}

// Recibe two witnesses
func wishHappyBirthday<A>(person: A, _ naming: Naming<A>, _ aging: Aging<A>) {
  print("Happy birthday, \(naming.name(person)), you are: \(aging.age(person))!")
}

let witnessName = Naming<Person>(name: { $0.name })
let witnessAge = Aging<Person>(age: { $0.age })

let somePerson = Person(name: "Malcolm", age: 21)
wishHappyBirthday(person: somePerson, witnessName, witnessAge) // Happy birthday, Malcolm, you are: 21!

let anotherPerson = Person(name: "Joel", age: 50)
wishHappyBirthday(person: anotherPerson, witnessName, witnessAge) // Happy birthday, Joel, you are: 50!

7. Protocol Extensions

protocol RandomNumberGenerator {
  func random() -> Double
}

extension RandomNumberGenerator {
  func randomBool() -> Bool {
    return random() > 0.5
  }
}

class LinearCongruentialGenerator: RandomNumberGenerator {
  private var lastRandom = 42.0
  private let m = 139968.0
  private let a = 3877.0
  private let c = 29573.0

  func random() -> Double {
    lastRandom = ((lastRandom * a + c).truncatingRemainder(dividingBy:m))
    return lastRandom / m
  }
}

let generator: RandomNumberGenerator = LinearCongruentialGenerator()
generator.random() //  0.3746499199817101
generator.randomBool() // true

Defining a protocol extension for a witness follow the same process as you define a extension for a protocol:

struct RandomNumberGenerating<A> {
  let random: (A) -> Double
}

extension RandomNumberGenerating {
  var randomBool: (A) -> Bool {
    return {
      random($0) > 0.5
    }
  }
}

class LinearCongruentialGenerator {
  var lastRandom = 42.0
  let m = 139968.0
  let a = 3877.0
  let c = 29573.0
}

let generatingRandom = RandomNumberGenerating<LinearCongruentialGenerator>(
  random: {
    $0.lastRandom = (($0.lastRandom * $0.a + $0.c).truncatingRemainder(dividingBy: $0.m))
    return $0.lastRandom / $0.m
  }
)

let generator = LinearCongruentialGenerator()
generatingRandom.random(generator) // 0.729023776863283
generatingRandom.randomBool(generator) // true

8. Providing Default Implementations

protocol TextRepresentable {
  var textualDescription: String { get }
}

protocol PrettyTextRepresentable: TextRepresentable {
  var prettyTextualDescription: String { get }
}

extension PrettyTextRepresentable  {
  var prettyTextualDescription: String {
    return textualDescription
  }
}

class SnakeAndLadders: PrettyTextRepresentable {
  var textualDescription: String { "textual Protocol" }
}

let game = SnakeAndLadders()
game.textualDescription  // textual Protocol
game.prettyTextualDescription // textual Protocol

Extensions with default implementations are nothing that initializers with default arguments

For this example, we can achieve the same overloading the initializer for our witness

struct TextRepresenting<A> {
  let textualDescription: (A) -> String
}

struct PrettyTextRepresenting<A> {
  let textRepresenting: TextRepresenting<A>
  let prettyTextualDescription: (A) -> String

	init(
    textRepresenting: TextRepresenting<A>
  ) {
    self.textRepresenting = textRepresenting
    self.prettyTextualDescription = {
      textRepresenting.textualDescription($0)
    }
  }

  init(
    textRepresenting: TextRepresenting<A>,
    prettyTextualDescription: @escaping (A) -> String
  ) {
    self.textRepresenting = textRepresenting
    self.prettyTextualDescription = prettyTextualDescription
  }
}

class SnakeAndLaddersGame { }

let textWitnesses = TextRepresenting<SnakeAndLaddersGame>(textualDescription: { _ in "Game to Text" })
let prettyWitnesses = PrettyTextRepresenting(textRepresenting: textWitnesses)

let game = SnakeAndLaddersGame()

prettyWitnesses.textRepresenting.textualDescription(game) // Game to Text
prettyWitnesses.prettyTextualDescription(game) // Game to Text

9. Associated Types

protocol ItemStoring {
  associatedtype DataType

  var items: [DataType] { get set}
  mutating func add(item: DataType)
}

extension ItemStoring {
  mutating func add(item: DataType) {
    items.append(item)
  }
}

struct NameDatabase: ItemStoring {
  var items = [String]()
}

var names = NameDatabase()
names.add(item: "James")
names.add(item: "Jess")
names.items // ["James", "Jess"]

Having an associated type means the addition of another generic for our witness type

struct StoreingItems<A, B> {
  let items: (A) -> [B]

  let addItem: (inout A, B) -> Void
}

struct DataBaseRepository {
  var items = [String]()
}

let witnessStoreItems = StoreingItems<DataBaseRepository, String>(
  items: { $0.items },
  addItem: { repository, newItem in
    repository.items.append(newItem)
  }
)

var repository = DataBaseRepository(items: [])
witnessStoreItems.items(repository) // []
witnessStoreItems.addItem(&repository, "James")
witnessStoreItems.addItem(&repository, "Jess")
witnessStoreItems.items(repository) // ["James", "Jess"]

Another example of an associated type:

protocol Stack {
  associatedtype Element
  func push(x: Element)
  func pop() -> Element?
}

class StackOfInt: Stack {
  typealias Element = Int

  var ints: [Int] = []

  func push(x: Int) {
    ints.append(x)
  }

  func pop() -> Int? {
    var ret: Int?
    if ints.count > 0 {
      ret = ints.last
      ints.removeLast()
    }
    return ret
  }
}

let integers = StackOfInt()
integers.push(x: 10)
integers.push(x: 30)
integers.push(x: 50)
print(integers.ints) // [10, 30, 50]

_ = integers.pop()
print(integers.ints) // [10, 30]
struct Stacking<A, Element> {
  let push: (inout A, Element) -> Void
  let pop: (inout A) -> Element?
}

class StackOfStrins {
  var strings: [String] = []
}

let stringsStacking = Stacking<StackOfStrins, String>(
  push: {
    $0.strings.append($1)
  },
  pop: {
    if $0.strings.count > 0 {
      let lastElement = $0.strings.last
      $0.strings.removeLast()
      return lastElement
    } else {
      return nil
    }
  }
)

var stack = StackOfStrins()
stringsStacking.push(&stack, "first")
stringsStacking.push(&stack, "second")
stringsStacking.push(&stack, "third")

stack.strings // ["first", "second", "third"]
_ = stringsStacking.pop(&stack)
stack.strings // ["first", "second"]

Summary to transform a protocol into a concrete type:

  1. If your protocol requires a property, use a closure
  2. For a function, use a closure
  3. Remember to protocols use a implicit self
  4. Static var or static functions means that we don’t need reference to self
  5. For an associated type, add a new generic
  6. Required initializers mean a way of constructing that instance, so always the out will be the Self generic.
  7. Protocol inheritance means adding a new attribute in the other witness
  8. Protocol extension is the same, you declare an extension for your witness
  9. Protocol extension with default implementation is a initializer with default arguments
  10. Instead of conforming the protocol (create a new type) now you create a value of that type

Wrapping up:

So if protocols are great why bother?

One advantage is that structs are too simple, that have fewer rules than protocols, in some cases when you have a complex protocol hierarchy, can be simplified using structs.

Furthermore, now you need to maintain only one type (the Struct), instead of maintaining the usual types that come after the conformation of a protocol (Real conformance, Mock conformance)

At the end is a new tool in your tool belt, so don’t try to force and learn to differentiate when using the correct tool for the problem are you facing.

References:

Categories:

Updated: