Protocol Witnesses: Part 2

Episode #34 • Oct 22, 2018 • Subscriber-Only

Last time we covered some basics with protocols, and demonstrated one of their biggest pitfalls: types can only conform to a protocol a single time. Sometimes it’s valid and correct for a type to conform to a protocol in many ways. We show how to remedy this by demonstrating that one can scrap any protocol in favor of a simple datatype, and in doing so opens up a whole world of composability.

Protocol Witnesses: Part 2
De-protocolizing Describable
00:05
Witnessing generic algorithms
04:00
De-protocolizing Combinable
07:00
De-protocolizing EmptyInitializable
07:59
De-protocolizing protocol composition
10:41
What’s the point?
12:17
Witnessing composition
14:02

Unlock This Episode

Our Free plan includes 1 subscriber-only episode of your choice, plus weekly updates from our newsletter.

De-protocolizing Describable

Ok, now that we’ve given a refresher on how protocols work in Swift, and showed one of their biggest limitations, what can we do to address that? It turns out that when you define a protocol in Swift and conform a type to that protocol, the compiler is doing something very special under the hood in order to track the relationship between those two things. We are going to give a precise definition to that construction, and we will even recreate it directly in Swift code. This will mean that we are going to do the work that the compiler could be doing for us for free, but by us taking the wheel for this we will get a ton of flexibility and composability out of it.

Let’s start slowly with the protocol we’ve just been talking about: Describable. We are going to de-protocolize this by creating a generic struct, where the generic parameter represents the type conforming to the protocol, and the struct will have fields corresponding to the requirements of the protocol.

So, Describable had one requirement that said that it could turn itself into a string. We will represent this as a generic struct that wraps a function from the generic parameter into string:

struct Describing<A> {
  var describe: (A) -> String
}

Let’s put this next to the Describable protocol so that you can see the similarities:

protocol Describable {
  var description: String { get }
}

struct Describing<A> {
  var describe: (A) -> String
}

From this type we create instances, which are called “witnesses” to the protocol conformance:

let compactWitness = Describing<PostgresConnInfo> { conn in
  "PostgresConnInfo(database: \"\(conn.database)\", hostname: \"\(conn.hostname)\", password: \"\(conn.password)\", port: \"\(conn.port)\", user: \"\(conn.user)\")"
}

You can use this witness directly by just invoking the description field directly:

compactWitness.describe(localhostPostgres)
// "PostgresConnInfo(database: "pointfreeco_development", hostname: "localhost", password: "", port: "5432", user: "pointfreeco")

We get the same output as before, but because our witnesses are just values, we can create as many of them as we want.

let prettyWitness = Describing<PostgressConnInfo> {
  """
  PostgresConnInfo(
    database: \"\($0.database)",
    hostname: \"\($0.hostname)",
    password: \"\($0.password)",
    port: \"\($0.port)",
    user: \"\($0.user)"
  )
  """
}

And we can use each witness the same way.

prettyWitness.description(localhostPostgres)
// "PostgresConnInfo(\n  database: \"pointfreeco_development",\n  hostname: \"localhost\",\n  password: \"\",\n  port: \"5432\",
  user: \"pointfreeco\"\n)"

We can keep going and define an additional witness that prints connection strings.

let connectionWitness = Describing<PostgressConnInfo> {
  "postgres://\($0.user):\($0.password)@\($0.hostname):\($0.port)/\($0.database)"
}

connectionWitness.description(localhostPostgres)
"postgres://pointfreeco:@localhost:5432/pointfreeco_development"
This episode is for subscribers only.

Subscribe to Point-Free

Access this episode, plus all past and future episodes when you become a subscriber.

See plans and pricing

Already a subscriber? Log in

Exercises

  1. Translate the Equatable protocol into an explicit datatype struct Equating.

  2. Currently in Swift (as of 4.2) there is no way to extend tuples to conform to protocols. Tuples are what is known as “non-nominal”, which means they behave differently from the types that you can define. For example, one cannot make tuples Equatable by implementing extension (A, B): Equatable where A: Equatable, B: Equatable. To get around this Swift implements overloads of == for tuples, but they aren’t truly equatable, i.e. you cannot pass a tuple of equatable values to a function wanting an equatable value.

    However, protocol witnesses have no such problem! Demonstrate this by implementing the function pair: (Combining<A>, Combining<B>) -> Combining<(A, B)>. This function allows you to construct a combining witness for a tuple given two combining witnesses for each component of the tuple.

  3. Functions in Swift are also “non-nominal” types, which means you cannot extend them to conform to protocols. However, again, protocol witnesses have no such problem! Demonstrate this by implementing the function pointwise: (Combining<B>) -> Combining<(A) -> B>. This allows you to construct a combining witness for a function given a combining witnesss for the type you are mapping into. There is exactly one way to implement this function.

  4. One of Swift’s most requested features was “conditional conformance”, which is what allows you to express, for example, the idea that an array of equatable values should be equatable. In Swift it is written extension Array: Equatable where Element: Equatable. It took Swift nearly 4 years after its launch to provide this capability!

    So, then it may come as a surprise to you to know that “conditional conformance” was supported for protocol witnesses since the very first day Swift launched! All you need is generics. Demonstrate this by implementing a function array: (Combining<A>) -> Combining<[A]>. This is saying that conditional conformance in Swift is nothing more than a function between protocol witnesses.

  5. Currently all of our witness values are just floating around in Swift, which may make some feel uncomfortable. There’s a very easy solution: implement witness values as static computed variables on the datatype! Try this by moving a few of the witnesses from the episode to be static variables. Also try moving the pair, pointwise and array functions to be static functions on the Combining datatype.

  6. Protocols in Swift can have “associated types”, which are types specified in the body of a protocol but aren’t determined until a type conforms to the protocol. How does this translate to an explicit datatype to represent the protocol?

  7. Translate the RawRepresentable protocol into an explicit datatype struct RawRepresenting. You will need to use the previous exercise to do this.

  8. Protocols can inherit from other protocols, for example the Comparable protocol inherits from the Equatable protocol. How does this translate to an explicit datatype to represent the protocol?

  9. Translate the Comparable protocol into an explicit datatype struct Comparing. You will need to use the previous exercise to do this.

  10. We can combine the best of both worlds by using witnesses and having our default protocol, too. Define a DefaultDescribable protocol which provides a static member that returns a default witness of Describing<Self>. Using this protocol, define an overload of print(tag:) that doesn’t require a witness.

References

Protocol-Oriented Programming in Swift

Apple • Tuesday Jun 16, 2015

Apple’s eponymous WWDC talk on protocol-oriented programming:

At the heart of Swift’s design are two incredibly powerful ideas: protocol-oriented programming and first class value semantics. Each of these concepts benefit predictability, performance, and productivity, but together they can change the way we think about programming. Find out how you can apply these ideas to improve the code you write.

Modern Swift API Design

Apple • Wednesday Jan 2, 2019

As of WWDC 2019, Apple no longer recommends that we “start with a protocol” when designing our APIs. A more balanced approach is discussed instead, including trying out concrete data types. Fast forward to 12:58 for the discussion.

Every programming language has a set of conventions that people come to expect. Learn about the patterns that are common to Swift API design, with examples from new APIs like SwiftUI, Combine, and RealityKit. Whether you’re developing an app as part of a team, or you’re publishing a library for others to use, find out how to use new features of Swift to ensure clarity and correct use of your APIs.

Protocols with Associated Types

Alexis Gallagher • Tuesday Dec 15, 2015

This talk by Alexis Gallagher shows why protocols with associated types are so complicated, and tries to understand why Swift chose to go with that design instead of other alternatives.

Protocol Oriented Programming is Not a Silver Bullet

Chris Eidhof • Thursday Nov 24, 2016

An old article detailing many of the pitfalls of Swift protocols, and how often you can simplify your code by just using concrete datatypes and values. Chris walks the reader through an example of some networking API library code, and shows how abstracting the library with protocols does not give us any tangible benefits, but does increase the complexity of the code.

Value-Oriented Programming

Matt Diephouse • Sunday Jul 29, 2018

Matt gives another account of protocol-oriented programming gone awry, this time by breaking down the famous WWDC talk where a shape library is designed using protocols. By rewriting the library without protocols Matt ends up with something that can be tested without mocks, can be inspected at runtime, and is more flexible in general.

Scrap your type classes

Gabriel Gonzalez • Wednesday May 2, 2012

Haskell’s notion of protocols are called “type classes,” and the designers of Swift have often stated that Swift’s protocols took a lot of inspiration from Haskell. This means that Haskellers run into a lot of the same problems we do when writing abstractions with type classes. In this article Gabriel Gonzalez lays down the case for scrapping type classes and just using simple datatypes.

Haskell Antipattern: Existential Typeclass

Luke Palmer • Sunday Jan 24, 2010

A Haskell article that demonstrates a pattern in the Haskell community, and why it might be an anti-pattern. In a nutshell, the pattern is for libraries to express their functionality with typeclasses (i.e. protocols) and provide Any* wrappers around the protocol for when you do not want to refer to a particular instance of that protocol. The alternative is to replace the typeclass with a simple concrete data type. Sound familiar?

Protocol Witnesses: App Builders 2019

Brandon Williams • Friday May 3, 2019

Brandon gave a talk about “protocol witnesses” at the 2019 App Builders conference. The basics of scraping protocols is covered as well as some interesting examples of where this technique really shines when applied to snapshot testing and animations.

Protocol-oriented programming is strongly recommended in the Swift community, and Apple has given a lot of guidance on how to use it in your everyday code. However, there has not been a lot of attention on when it is not appropriate, and what to do in that case. We will explore this idea, and show that there is a completely straightforward and mechanical way to translate any protocol into a concrete datatype. Once you do this you can still write your code much like you would with protocols, but all of the complexity inherit in protocols go away. Even more amazing, a new type of composition appears that is difficult to see when dealing with only protocols. We will also demo a real life, open source library that was originally written in the protocol-oriented way, but after running into many problems with the protocols, it was rewritten entirely in this witness-oriented way. The outcome was really surprising, and really powerful.

Downloads