Automatic Requirement Satisfaction in plain Swift

Great write-up!

The Cons-list is certainly an interesting workaround. Unfortunately I think it's a general problem that many of the interesting things we'd like to do with the language are limited by the current generics system. Especially when you get in to the realm of plugging in to Swift at compile-time (e.g. code transforms like function builders, static reflection like for default implementations based on member information), or really any kind of boilerplate elimination. We need to deal with arbitrary sequences of heterogeneous types (possibly with constraints), and that's why variadic generics is so important - even if it sounds a bit niche and looks like alien hieroglyphics. Fortunately it's (loosely?) on the roadmap for Swift 6.

Let me sketch out one possible design using variadics, and without relying on compile-time evaluation.

Most things that use variadic generics will have a variable number of members (e.g. the common example of a ZipN wrapper needs to store the N sequences which make it up, each of which has a different type), so there will need to be some way to iterate over them. So you could imagine a ZipN iterator looking something like this:

struct ZipNSequence<(T: Sequence)...>: Sequence {
  // ...
  struct Iterator: IteratorProtocol {
    var iterators: (T.Iterator...)
    mutating func next() -> (T.Element...)? {
      // Here. Constructs a tuple by iterating over the variadic members,
      // Each of which has a different type.
      return (iterators.next()...) 
    }
  }
}

There are lots of options for how we could design a Members<T...> structure, but the simplest is to allow extracting an object's members as a tuple by keypath application, just like the ZipSequence does with calling .next().

struct TypeInfo<Root> {
  // The "magic" here is that the parameters depend on compile-time
  // knowledge about `Root`.
  // Think of it like every single type had a hidden extension with its own 
  // definition of this, each with the correct type.
  static var members: Members<...magic...>

  struct Members<T...> {
    static var keyPaths: (KeyPath<Root, T>...) { /* compiler magic */ }

    static func valuesAsTuple(from instance: Root) -> (T...) {
      // Or something like this. Analogous to calling "iterator.next()"
      // on every item. Alternatively, add a 
      // "KeyPath<Root, T>.get(from: Root) -> T" member function.
      return (instance[keyPath: keyPaths...]...)
    }
  }
}

Then you could compare the tuples, since those are now Equatable to any arity.

extension Equatable where TypeInfo<Self>.Members<(T: Equatable)...> {
  public static func ==(lhs: Self, rhs: Self) -> Bool {
    return TypeInfo.members.valuesAsTuple(from: lhs) == 
             TypeInfo.members.valuesAsTuple(from: rhs) 
  }
}

If you had to do some more complex walking, like for Codable, you'd have to iterate, possibly with some kind of local generic binding (iteration over heterogeneous types isn't currently possible in Swift, which is one of the reasons we can't iterate over tuple components):

extension Encodable where TypeInfo<Self>.Members<(T: Encodable)...> {
  public func encode<T>(using encoder: T) where T: Encoder {
    encoder.createBox()
    for <ChildType: Encodable> child in TypeInfo.members.valuesAsTuple(from: self) {
      child.encode(using: encoder)
    }
    encoder.closeBox()
  }
}

I think that also looks a lot simpler than writing a bunch of conformances for every node in the Cons chain.

4 Likes