Enumerating All Possible States With Parameter Packs + CaseIterable

I want a way to generate all possible combinations of values when given a list of types that conform to CaseIterable for a testing harness. I'm at a point where I've gotten stuck, and I'm not quite sure how to reason about the problem at this level of abstraction.

Example

enum CompassDirection: CaseIterable {
    case north, south, east, west
}

enum MyEnum: CaseIterable {
    case foo, bar, baz
}

enum WorkoutType: CaseIterable {
    case cardio
    case hiit
    case strength
}

let combo = combination(CompassDirection.self, WorkoutType.self, MyEnum.self)
// [(.north, .cardio, .foo), (.north, .cardio, .bar), (.north, .cardio, .baz), ...]

Non-parameter pack 2 case scenario.
This is pretty straightforward, and easy enough to understand, but doesn't support arbitrary numbers of types.

func combination<A: CaseIterable, B: CaseIterable>(_ type1: A, type2: B) -> [(A, B)] {
    var result: [(A, B)] = []
    for a in A.allCases {
        for b in B.allCases {
            result.append((a, b))
        }
    }
    return result
}

Attempt with parameter packs

I thought this might be the syntax I wanted, but it doesn't seem to work in two places.

  1. I can't iterate generically over the tuple elements.
  2. "<type> cannot conform to <protocol>".
func comb<each T: CaseIterable>(
    _ a: repeat (each T).Type
) -> [(repeat each T)] {
    var result: [(repeat each T)] = []
    let allAllCases = (repeat (each T).allCases)  // You now have a tuple of lists...
    // ... how do I iterate over the tuple elements here?
    return result
}


let combo = comb(CompassDirection.self, WorkoutType.self) // Type 'CompassDirection.Type' cannot conform to 'CaseIterable'

Is this possible in Swift? How? Or, why not?

After some research, it seems like this isn't possible without https://github.com/apple/swift-evolution/blob/main/proposals/0393-parameter-packs.md#pack-iteration

There are two missing features in the current implementation:

  • for loops over packs
  • repeat with a closure literal inside the pattern

However, while it is awkward, you can simulate both of the above using a local generic function:

func takesPack<each T: P>(t: repeat each T) -> Int {
  var result = 0
  func operateOnElement<E: P>(e: E) {
    result += p.computeSomeInt()
  }
  repeat operateOnElement(each t)
  return result
}

You can even simulate a 'break' to early exit from the iteration using throw/catch:

func takesPack<each T: P>(t: repeat each T) -> Int? {
  var result = 0
  func operateOnElement<E: P>(e: E) {
    if ... { throw MyError.error }
    result += p.computeSomeInt()
  }
  do {
    repeat try operateOnElement(each t)
    return result
  } catch {
    return nil
  }
}

These are only meant to be temporary stop-gaps, of course.

2 Likes

I guess what is really is missing is only the ability to destructure a tuple:

func product<First, each Rest>(
  _ first: [First], _ rest: repeat [each Rest]
) -> [(First, repeat each Rest)] {
  guard case (let before, repeat let each after) = (repeat each rest) else {
    return first
  }

  return first.flatMap { element in
    product(before, repeat each after).map { tuple in
      (element, repeat each tuple)
    }
  }
}

The destructuring happens on line 4, in the guard case statement.

Then your comb function would just be

func comb<First: CaseIterable, each Rest: CaseIterable>(
  _ first: First.Type, _ rest: repeat (each Rest).Type
) -> [(First, repeat each Rest)] {
  return product(first.allCases, repeat (each rest).allCases)
}