Is it possible to constrain variadic generics to a particular type

I have a function that converts an array into a tuple, if the element count is correct. For example:

let array = [1, 2, 3]
guard let (a, b, c) = array.explode() else {
  fatalError("The array must not have had 3 elements")
}
a == 1
b == 2
c == 3

Currently, I have this implemented by a series of functions for the different tuple sizes:

extension Collection {
  func explode() -> (Element, Element)?
  func explode() -> (Element, Element, Element)?
  func explode() -> (Element, Element, Element, Element)?
  // etc...
}

I was looking into implementing this using variadic generics instead, but either I haven't found the right incantation, or it's not supported yet. Here's what I'm trying:

// Error: Same-element constraints are not yet implemented
func explode<each T>() -> (repeat each T)
  where repeat each T == Element
{
  var itr = self.makeIterator()
  return (repeat itr.next() as! each T)
}

I can remove the where clause and write out all the types manually like this:

let (a, b, c): (Int, Int, Int) = [1, 2, 3].explode()!

But that becomes quite unwieldy as the number of elements scales. I already have manual variants implemented for 2–6 elements, so I'm really hoping to get something that can handle large numbers better.

Is there a way to write this right now?

I realize that my current implementation attempt also doesn't have a way to return nil if the element count doesn't match.

I don't think so at the moment.

Something like this:

extension Collection {
    func explode<each T>(into explodeCount: Int) throws -> (repeat each T) {
        guard count == explodeCount else { throw CustomError() }
        var itr = self.makeIterator()
        return (repeat itr.next() as! each T)
    }
}

Usage (type annotation is required):

let array:[Int] = [1, 2, 3]
guard let (a, b, c):(Int, Int, Int) = try? array.explode(into: 3) else { return }

It's not supported yet.

You can almost fake it, with a generic type alias, like this:

typealias SameElt<T, U> = U

func sameElt<T, U>(_ t: T, _ u: U) -> U { return u }

func explode<each T, U>(_ array: [U]) -> (repeat SameElt<each T, U>) {
  var itr = array.makeIterator()
  return (repeat sameElt((each T).self, itr.next()!))
}

However, this produces a diagnostic that the generic parameter each T does not appear in the signature of explode(), and indeed this reveals the underlying limitation.

The diagnostic is correct because while the caller can fix the length of each T by specifying a tuple type as the contextual result of the call expression, the element types of each T cannot be fixed from the function's signature in any way, and while the implementation of explode() happens to not actually depend on what element types are, it could, and the caller has no way of knowing this.

You can change the declaration of explode() so that it passes in a dummy pack for each T, which is then just ignored:

func explode<each T, U>(_: (repeat (each T)).Type, _ array: [U]) -> (repeat SameElt<each T, U>) {

With the above, this in fact works:

let (a, b, c) = explode(((), (), ()).self, [1, 2, 3])

print("a = \(a), b = \(b), c = \(c)")

However, it's awkward to use because of the ((), (), ()) (and in fact, any tuple metatype of length 3 could be used, (Int, Float, Bool).self for example, again because the element types are ignored.)

There is a preliminary implementation of same-element requirements in the Requirement Machine, but what's missing is support in various other places. For example, when IRGen lowers the function signature, it needs to consider the case where we must pass in the length of a pack, but not its element metadata, as would be the case with explode() above.

6 Likes