[Pitch 2] Light-weight same-type requirement syntax

I'm still convinced that this feature is both bad syntax, as well as semantics-wise, due to conflating two orthogonal concepts (generic parameters and associated types) into one syntax:

-1

Previous comments

I can't comment on how feasible generic protocols would be to implement in Swift, but as an avid user of generic traits in Rust I can at least try to give some context as to why one would want them (or something equivalent) in Swift:

It's fairly long, so folding it:

Type-safe, compiler-checked value conversions

Type-safe, compiler-checked value conversions

Swift (like Rust) chose to make type conversions explicit. This is great from a correctness and safety perspective, but tends to be very hindering when trying to write abstract code that's generic over its types. The main reason for this is that Swift lacks a way to abstractly express type convertibility.

Say you have a function that is supposed to calculate the fitness of a given value in respect to a certain "ideal" value:

func fitness(actual: Double, ideal: Double) -> Double {
    (actual == ideal) ? 0.0 : abs(actual - ideal) / max(abs(actual), abs(ideal))
}

(This formula is taken from step 7 of the "fitness distance" algorithm defined by the WebRTC spec, which I had to implement recently and where I found myself—yet again—in need of generic protocols.)

Now say that the values you need to compute the fitness for have different types then Double, say Int:

func fitness(actual: Int, ideal: Int) -> Double {
    fitness(actual: Double(actual), ideal: Double(ideal))
}

or Float:

func fitness(actual: Float, ideal: Float) -> Double {
    fitness(actual: Double(actual), ideal: Double(ideal))
}

Now, these two latter functions work just on their own. But they are of no use when you need to abstract over the input type.

Sure, for this particular scenario you could write a protocol like this:

protocol DoubleConvertible {
    func asDouble() -> Double
}

and just have all necessary types conform to said protocol.

func fitness<Value>(actual: Value, ideal: Value) -> Double where Value: DoubleConvertible {
    fitness(actual: actual.asDouble(), ideal: ideal.asDouble())
}

But this approach has two major limitations:

  • it only "solves" the issue for Double. What about Float? Or Int? Or String? …

  • you can't make fitness(actual:ideal:) generic over multiple types:

    func fitness<Input, Output>(actual: Input, ideal: Input) -> Output where Input: ???Convertible {
        fitness(actual: actual.as???(), ideal: ideal.as???())
    }
    

If we were to declare type-specific protocols for every type we possibly might want to convert to, we would be polluting our project's namespace with an immense amount of redundant garbage (and not be gaining much from it semantically, either).

Just do the math: Given N interchangeably convertible types we would need do define N individual protocols of the pattern protocol <…>Convertible { … } and come up with N unique, yet expressive method names to go with them?

And what if this N gets getting bigger and bigger? And what if instead of dealing with concrete types you were working on generic types or methods/functions, and hence no way to have the Swift compiler pick the right explicit protocol for a given generic type T?

This is where generic protocols safe the day!

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

But wait, can't we do this already with a protocol with associated types, like so? …

protocol ConvertibleInto {
    associatedtype T
    
    func into() -> T
}

Well, it depends. While you could easily implement Foo: CustomStringConvertible in terms of …

struct Foo {
    let bar: Int
}

extension Foo: ConvertibleInto {
    typealias T = String
    
    func into() -> T {
        return …
    }
}

… you would get in trouble as soon as you were to decide that it would be nifty to also be able to convert instances of Foo to Data:

// error: redundant conformance of 'Foo' to protocol 'ConvertibleInto':
extension Foo: ConvertibleInto {
    // error: invalid redeclaration of 'T':
    typealias T = Data
    
    // error: invalid redeclaration of 'into()':
    func into() -> T {
        return  …
    }
}

In today's Swift a protocol (regardless of whether it has associated types, or not) can only be conformed to once by any single type. In order to achieve polymorphic semantics of protocol ConvertibleInto we however would need to have a way to allow for multiple conformances per type.

Generic protocols would allow for this:

extension Foo: ConvertibleInto<String> {
    func into() -> String {
        return  …
    }
}

extension Foo: ConvertibleInto<Data> {    
    func into() -> Data {
        return  …
    }
}

In addition to a hypothetical ConvertibleInto<T> we would probably also want to have access to a corresponding ConstructibleFrom<T> protocol going the other way:

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

As such a hypothetical Real type might be constructible from both Float and Double, e.g.:

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

(We will be using extension<…> as a hypothetical syntax for introducing generic arguments into an extension scope.)

Going even further one might want to have the Swift compiler automatically derive conformance of ConstructibleInto<U> for every type U: ConstructibleFrom` with a default implementation like so:

extension<T, U> T: ConvertibleInto<U> where U: ConstructibleFrom<T> {
    func into() -> U {
        return U(from: self)
    }
}

One might also want to have every type T auto-derive conformance to T: ConstructibleFrom<T> like so:

extension<T> T: ConvertibleInto<T> {
    func into() -> T {
        return self
    }
}

With generic protocols at one's disposal one could —for example— unify all of …

  • UIColor's var cgColor: CGColor
  • UIImage's var cgImage: CGImage?
  • UIColor's var ciImage: CIImage?

… into the single universal ConvertibleInto<T> protocol, and individually conform to it like so:

extension UIImage: ConvertibleInto<CGImage> {
    func into() -> CGImage {
        return …
    }
}

… which would then allow one to nicely write …

func draw(image: T) where T: ConvertibleInto<CGImage> {
    let cgImage: CGImage = image.into()
    // …
}

… having it accept images of any type that's convertible to CGImage (e.g. UIImage, NSImage, CGImage, …).

Generic overloading

Generic overloading

With all this talk about multiple conformances of protocols one might wonder "wait, isn't what what Swift has function overloading for? We already can implement variants of a function based in argument and/or return types, why need protocols for that?"

And of course some truth to this sentiment.

Let's assume we wanted to write an efficient and type-safe linear algebra framework for Swift. We would probably end up defining types for scalars, vectors and matrixes, like so:

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

And it would not take long until one needed some way to perform arithmetic operations on them, such as multiplication:

extension Vector {
    // vector scaling:
    func *(lhs: Self, rhs: Scalar<T>) -> Vector<T> { /* ... */ }
    
    // dot product:
    func *(lhs: Self, rhs: Vector<T>) -> Scalar<T> { /* ... */ }

    // vector matrix product:
    func *(lhs: Self, rhs: Matrix<T>) -> Vector<T> { /* ... */ }
}

Notice how each method's return type directly depends on the type of rhs.

This works as long as one is dealing with explicit types, exclusively. But sooner or later one would want to be able to generalize over scalars, vectors and matrixes. (After all from the point of algebra they are just tensors of 0, 1, or 2 dimensions respectively.)

As it turns out there is no way to express overloading in today's Swift from the perspective of protocols. It's a dead spot in Swift's generics type system.

If one had generic protocol at one's disposal however one could express things like this:

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

// 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 { /* ... */ }
}

Now whenever one needs to be generic over a type U and require it to be multipliable with Vector<T>, one could express it through where Vector<T>: Multiplication<U>.

These are just two of the many use-cases for generic protocols off the top of my head. There are many more.

I don't see how avoiding having to write Collection<.Element == Int> in favor of Collection<Int> is worth effectively shutting the door for any possibility of proper generic protocols ever making it into Swift.

Not being able to express overloading (and abstracting of it in generic contexts) in protocols is one of one of the biggest gaps in Swift today and a daily annoyance for anybody writing generics-heavy code in Swift.

18 Likes