Skip to content

Instantly share code, notes, and snippets.

@regexident
Created November 14, 2017 09:16
Show Gist options
  • Save regexident/f8386ae211299f1e43268ba1a945e7f2 to your computer and use it in GitHub Desktop.
Save regexident/f8386ae211299f1e43268ba1a945e7f2 to your computer and use it in GitHub Desktop.
Draft for extended section on generic protocols in Swift Raw

Generic protocols

One of the most commonly requested features is the ability to parameterize protocols themselves. For example, a protocol that indicates that the Self type can be constructed from some specified type T or converted into T:

// T -> Self:
protocol ConstructibleFrom<T> {
  init(from value: T)
}

// Self -> T:
protocol ConvertibleInto<T> {
  func into() -> T
}

What differentiates

protocol ConstructibleFrom<T> {
  init(from value: T)
}

from just using existing protocols with associated types

protocol ConstructibleFrom {
  associatedtype T
  init(from value: T)
}

is the ability for a given type to conform to the protocol for more than one type T (while defining associatedtype T = ... more than once would result in a compiler error).

A Real type might be constructible from both Float and Double, e.g.:

struct Real { /* ... */ }
extension Real: ConstructibleFrom<Float> { /* ... */ }
extension Real: ConstructibleFrom<Double> { /* ... */ }

Assuming that every type T where U: ConstructibleFrom<T> should also conform to T: ConvertibleInto<U> implicitly (and assuming support for conditional conformance and parameterized extensions), conformance to T: ConvertibleInto<U> could be automatically derived for any type T where U: ConstructibleFrom<T>. Similarly conformance to T: ConvertibleFrom<T> could be automatically derived for any copyable type T as construction from itself could be considered equivalent to copying.

This now allows for abstractions over conversions from one generic type into another generic type, e.g.:

func converted<S, T>(_ values: S) -> [T]
  where S: Sequence, S.Element: ConvertibleInto<T>
{
  return values.map { $0.into() }
}

let floats: [Float] = [1.0, 2.0, 3.0]
let doubles: [Double] = [1.0, 2.0, 3.0]
let r1: [Real] = converted(floats)
let r2: [Real] = converted(doubles)

Note that converted(_:) is not exposed to any concrete type in above snippet. It just constrains S to be a sequence of values that can be converted into T. A function such as the one shown above, albeit rather trivial in nature, is not expressible in Swift without generic protocols.

Overloading functions or properties

Another common use-case of generic protocols is the implementation of an event handler:

protocol Event {}

protocol EventHandler<T: Event> {
  func handle(event: T)
}

struct FooEvent: Event { let foo: Int = 42 }
struct BarEvent: Event { let bar: Float = 42.00 }

class Handler { /* ... */ }

extension Handler: EventHandler<FooEvent> {
  func handle(event: FooEvent) { print("Handled foo: \(event.foo)") }
}

extension Handler: EventHandler<BarEvent> {
  func handle(event: BarEvent) { print("Handled bar: \(event.bar)") }
}

While Swift does support function overloading, it does not support expressing such overloads in protocols generically. As such, again, the above is not expressible in Swift without generic protocols. In fact function overloading could inversely be implemented in terms of generic protocols.

Overloading associated types

Functions and properties however are not the ony thing that generic protocols allow one to provide multiple specifications for: associated types can be specialized, too.

Given a generic protocol like this:

protocol Multiplication<Rhs = Self> {
    associatedtype Output
    
    func *(lhs: Self, rhs: Rhs) -> Output
}

and the following types

struct Scalar<T: Numeric> { /* ... */ }
struct Vector<T: Numeric> { /* ... */ }
struct Matrix<T: Numeric> { /* ... */ }

one can implement type-safe and efficient algebraic logic as such:

// vector scaling:
extension Vector: Multiplication<Scalar<T>> {
    typealias Output = Vector<T>
    func *(lhs: Self, rhs: Rhs) -> Output { /* ... */ }
}

// dot product:
extension Vector: Multiplication<Vector<T>> {
    typealias Output = Scalar<T>
    func *(lhs: Self, rhs: Rhs) -> Output { /* ... */ }
}

// vector matrix product:
extension Vector: Multiplication<Matrix<T>> {
    typealias Output = Vector<T>
    func *(lhs: Self, rhs: Rhs) -> Output { /* ... */ }
}

Going one step further one could implement Multiplication<U> for Vector<T> so that it can be multiplied with any Scalar<U> where T: Multiplication<U>, resulting in a Vector<V> where V: (T as Multiplication<U>).Output, i.e. the result of a multiplication of T with U.

extension<U> Vector: Multiplication<Scalar<U>> where T: Multiplication<U> {
    typealias Output = Vector<(T as Multiplication<U>).Output>
    func *(lhs: Self, rhs: Rhs) -> Output { /* ... */ }
}

Note how each of the conformances have different types for Output which are neither directly related to Self, nor Rhs, and could also be arbitrary unrelated types. Again, neither of these implementations would be a possible in Swift without generic protocols.

Confusion with existentials

A common misconception surrounding generic protocols is that they would provide a solution for storing protocols with associated types within sequences. Modeling Sequence with generic parameters (protocol Sequence<Element> { /* ... */ }) rather than associated types (protocol Sequence { associatedtype Element = /* ... */ }) however, is tantalizing yet wrong: you don't want a type conforming to Sequence in multiple ways, or (among other things) your for..in loops stop working, and you lose the ability to dynamically cast down to an existential Sequence without binding the Element type (again, see "Generalized existentials"). The actual required feature here is a different one: the ability to say "Any type that conforms to Sequence whose Element type is String", which is covered by the section on "Generalized existentials", below.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment