Variadic Generics

Another example that takes a different shape: a library I’ve contributed to, Curry, currently defines function currying for up to 21 arguments:

public func curry<A, B, C>(_ function: @escaping ((A, B)) -> C) -> (A) -> (B) -> C {
    return { (a: A) -> (B) -> C in { (b: B) -> C in function((a, b)) } }
}

This is unlike the zip-style function in that it needs to be able to unpack/repack the types involved. It would be quite cool to be able to define this with a single generic function!

12 Likes

That is another interesting example, but unfortunately I can't figure out how VG may help here - it may be the late hour! I can't wrap my head about both how to specify the parameter of the variadic curry function and its body.
The problem is that this function feel somehow / something recursive.

Edit: maybe with head / tail variadic unpacking? With the support of other totally invented stuff:

// Invented syntax:
// #dropLast(T) returns the variadic generic `T` but the last one
// #last(T) returns the last element of the variadic generic `T`
// #expand(v) expands / unpack a variadic *value* into the surrounding context

// base case
func curry<A, B, C>(_ f: @escaping ((A, B)) -> C) -> (A) -> (B) -> C {
  return { a in { b in f((a, b)) } }
}

// recurring case
func curry<...T>(_ f: @escaping (#dropLast(T)) -> #last(T)) -> <# what to put here? #> {
  return { h in curry({ t in f(h, #expand(t)) }) }
}

Added link on the opening post to a partially completed document about this topic!

3 Likes

I’ve been thinking of “#pack

func myFunc<#pack Ts>(#explode(Ts)) -> Int allWhere Ts: BinaryInteger

Over the past days I've been working to update the document, there are still things missing but overall it covers much more stuff now. Let me think how it's going!

Here is the link to current version.

Note: The link in the OP always posts to the latest version.

4 Likes

Another use-case that just came to my mind. I have not seen anyone yet make use of key-path equality and hashability, but it turned out be such a powerful tool in my case. It allows me to create partial or full updates on UI. One current pain point that for every type that triggers an update I have to type manually every possible case to allow statically type-safe forwarding of a set of key-paths.

For example this is one enum that I have in our project.

protocol Drivable: AnyObject {}
extension Drivable {
  typealias Driver = GenericDriver<Self>
  var driver: GenericDriver<Self> { ... }
}

typealias PartialKeyPathSet<Root> = Set<PartialKeyPath<Root>>

enum Paths: Hashable {
  typealias StatusBarDriver = RemoteDeviceStatusBar.Driver
  typealias PreviewDriver = Mode.Preview.Driver
  typealias ControlDriver = Mode.Control.Driver

  case statusBar(PartialKeyPathSet<StatusBarDriver>)
  case preview(PartialKeyPathSet<PreviewDriver>)
  case control(PartialKeyPathSet<ControlDriver>)

  var statusBarSet: PartialKeyPathSet<StatusBarDriver>? {
    if case .statusBar(let set) = self {
      return set
    }
    return .none
  }

  var previewSet: PartialKeyPathSet<PreviewDriver>? {
    if case .preview(let set) = self {
      return set
    }
    return .none
  }

  var controlSet: PartialKeyPathSet<ControlDriver>? {
    if case .control(let set) = self {
      return set
    }
    return .none
  }
}

Ideally I would want to create a variadic enum that implements the most logic already and requires me to only provide the types I want to use.

enum GenericPath<T...>: Hashable where T: Drivable {
  /* case per generic type parameter - made up syntax */
  case ...$(PartialKeyPathSet<T.Driver>)

  /* optional set per generic type parameter to extract the payload */
}

// The above example could be written as:
typealias UpdatePath = GenericPath<RemoteDeviceStatusBar, Mode.Preview, Mode.Control>

// Index case names similar to tuples.
UpdatePath.1([\.self]) // payload for `Mode.Preview` - `PartialKeyPathSet<Mode.Preview.Driver>`

This would remove a ton of boilerplate in my code.

In the current proposal you do mention variadic enums which is great, but I couldn't find a single example on how we would express the cases of the enum with their payloads.

1 Like

Hi there,
I have a question about the examples in step 1:
Would _... also be a thing? Like for extracting the last element of a tuple:

// ... only *last* element
let longTuple = (a: 1, b: 2, c: 3, d: 4, e: 5)
let (_... , last) = longTuple
// last => 5

Hmm, I guess my question does not make sense - you could of course just do:

let last = longTuple.e

Please ignore. :slight_smile:

To get a better overview on what I want to collapse into a single type here are 3 enum types that with variadic generic could 'potentially' become one.

enum GenericPath2<A, B>:
  Hashable
  where
  A: Drivable,
  B: Drivable
{
  case _0(PartialKeyPathSet<A.Driver>)
  case _1(PartialKeyPathSet<B.Driver>)

  var payload: (
    _0: PartialKeyPathSet<A.Driver>?,
    _1: PartialKeyPathSet<B.Driver>?
  ) {
    switch self {
    case ._0(let set):
      return (set, .none)
    case ._1(let set):
      return (.none, set)
    }
  }
}

enum GenericPath3<A, B, C>:
  Hashable
  where
  A: Drivable,
  B: Drivable,
  C: Drivable
{
  case _0(PartialKeyPathSet<A.Driver>)
  case _1(PartialKeyPathSet<B.Driver>)
  case _2(PartialKeyPathSet<C.Driver>)

  var payload: (
    _0: PartialKeyPathSet<A.Driver>?,
    _1: PartialKeyPathSet<B.Driver>?,
    _2: PartialKeyPathSet<C.Driver>?
  ) {
    switch self {
    case ._0(let set):
      return (set, .none, .none)
    case ._1(let set):
      return (.none, set, .none)
    case ._2(let set):
      return (.none, .none, set)
    }
  }
}

enum GenericPath4<A, B, C, D>:
  Hashable
  where
  A: Drivable,
  B: Drivable,
  C: Drivable,
  D: Drivable
{
  case _0(PartialKeyPathSet<A.Driver>)
  case _1(PartialKeyPathSet<B.Driver>)
  case _2(PartialKeyPathSet<C.Driver>)
  case _3(PartialKeyPathSet<D.Driver>)

  var payload: (
    _0: PartialKeyPathSet<A.Driver>?,
    _1: PartialKeyPathSet<B.Driver>?,
    _2: PartialKeyPathSet<C.Driver>?,
    _3: PartialKeyPathSet<D.Driver>?
  ) {
    switch self {
    case ._0(let set):
      return (set, .none, .none, .none)
    case ._1(let set):
      return (.none, set, .none, .none)
    case ._2(let set):
      return (.none, .none, set, .none)
    case ._3(let set):
      return (.none, .none, .none, set)
    }
  }
}

I can go one step further and generalize this into a Union enum. Right now I would have as much boilerplate as this:

enum Union<A> {
  case _0(A)

  struct Payload {
    let _0: A?
  }

  var payload: Payload {
    switch self {
    case ._0(let payload):
      return Payload(_0: payload)
    }
  }
}

extension Union: Equatable where A: Equatable {}
extension Union: Hashable where A: Hashable {}

enum Union2<A, B> {
  case _0(A)
  case _1(B)

  struct Payload {
    let _0: A?
    let _1: B?
  }

  var payload: Payload {
    switch self {
    case ._0(let payload):
      return Payload(_0: payload, _1: .none)
    case ._1(let payload):
      return Payload(_0: .none, _1: payload)
    }
  }
}

extension Union2: Equatable where A: Equatable, B: Equatable {}
extension Union2: Hashable where A: Hashable, B: Hashable {}

enum Union3<A, B, C> {
  case _0(A)
  case _1(B)
  case _2(C)

  struct Payload {
    let _0: A?
    let _1: B?
    let _2: C?
  }

  var payload: Payload {
    switch self {
    case ._0(let payload):
      return Payload(_0: payload, _1: .none, _2: .none)
    case ._1(let payload):
      return Payload(_0: .none, _1: payload, _2: .none)
    case ._2(let payload):
      return Payload(_0: .none, _1: .none, _2: payload)
    }
  }
}

extension Union3: Equatable where A: Equatable, B: Equatable, C: Equatable {}
extension Union3: Hashable where A: Hashable, B: Hashable, C: Hashable {}

enum Union4<A, B, C, D> {
  case _0(A)
  case _1(B)
  case _2(C)
  case _3(D)

  struct Payload {
    let _0: A?
    let _1: B?
    let _2: C?
    let _3: D?
  }

  var payload: Payload {
    switch self {
    case ._0(let payload):
      return Payload(_0: payload, _1: .none, _2: .none, _3: .none)
    case ._1(let payload):
      return Payload(_0: .none, _1: payload, _2: .none, _3: .none)
    case ._2(let payload):
      return Payload(_0: .none, _1: .none, _2: payload, _3: .none)
    case ._3(let payload):
      return Payload(_0: .none, _1: .none, _2: .none, _3: payload)
    }
  }
}

extension Union4: Equatable
  where A: Equatable, B: Equatable, C: Equatable, D: Equatable {}
extension Union4: Hashable
  where A: Hashable, B: Hashable, C: Hashable, D: Hashable {}

// Types for my use-case
typealias PartialDriverKeyPathSetUnion<A> = Union<
  PartialKeyPathSet<A.Driver>
> where A: Drivable

typealias PartialDriverKeyPathSetUnion2<A, B> = Union2<
  PartialKeyPathSet<A.Driver>,
  PartialKeyPathSet<B.Driver>
> where A: Drivable, B: Drivable

typealias PartialDriverKeyPathSetUnion3<A, B, C> = Union3<
  PartialKeyPathSet<A.Driver>,
  PartialKeyPathSet<B.Driver>,
  PartialKeyPathSet<C.Driver>
> where A: Drivable, B: Drivable, C: Drivable

typealias PartialDriverKeyPathSetUnion4<A, B, C, D> = Union4<
  PartialKeyPathSet<A.Driver>,
  PartialKeyPathSet<B.Driver>,
  PartialKeyPathSet<C.Driver>,
  PartialKeyPathSet<D.Driver>
> where A: Drivable, B: Drivable, C: Drivable, D: Drivable

As you can see we would also need variadic generic typealiases to wrap custom constraints around a variadic type such as Union.

enum Union<T...> {
  case $T...(T)
  var payload: (T?...) { 
    ... 
  } 
}

extension Union: Equatable where T...: Equatable {}
extension Union: Hashable where T...: Hashable {}

typealias PartialDriverKeyPathSetUnion<T...> = Union<
  PartialKeyPathSet<T.Driver>...
> where T...: Drivable

One thing which I have not seen so far here is a use-case for multiple sets of variadic generics in a single function. I have a use-case which would directly benefit from this:

I currently have a use-case which would involve two separate sets of variadic types. It's part of an data-driven Entity-Component-System implementation, and I have several functions which take a closure as a parameter which has variadic inputs and outputs. I.e:

struct TypeMask {
    var types: [Component.Type]
    func matches(_ entity: Entity) -> Bool { ... } // returns true if the entity has the given components
}

struct Entity {
    func getComponent<T: Component>() -> T {
        // returns the component of the given type on this entity
    }
    func setComponent<T: Component>(component: T) {
        // sets the component of the given type on this entity to the given value
    }
}

func forEachMatching<Input, Output>(block: (Input) -> (Output)) {
    let typeMask = TypeMask([Input.self, Output.self])
    for entity in entities {
        if typeMask.matches(entity) {
            let input: Input = entity.getComponent()
            let output: Output = block(input)
            entity.setComponent(output)
        }
    }
}

I have a function implemented for each combination of inputs and outputs up to 7. I.e:

func forEachMatching<Input1, Output1>(block: (Input1) -> (Output1))
func forEachMatching<Input1, Input2, Output1>(block: (Input1, Input2) -> (Output1))
func forEachMatching<Input1, Output1, Ouptut2>(block: (Input1) -> (Output1, Output2))
func forEachMatching<Input1, Input2, Output1, Ouptut2>(block: (Input1, Input2) -> (Output1, Output2))
...

It would be very useful to be able to generalize these all into a single function:

func forEachMatching<Inputs..., Outpus...>(block: (Inputs...) -> (Outputs...)) {
    // I'm not sure how this would work in the proposed syntax, but it would be necessary
    // to get an array of Types from the generic parameter
    let typeMask = TypeMask( Inputs.map { $0 } + Outputs.map { $0 } )
    for entity in entities {
        if typeMask.matches(entity) {
            let inputs: (Inputs...) = entity.getComponents()
            let outputs: (Output...) = block(inputs)
            entity.setComponents(outputs)
        }
    }
}

I could be wrong, but I think anything that requires synthesizing a property or case declaration would probably require some kind of macro facility in addition to variadic generics. I’m not sure exactly what it might look like but variadic generics alone don’t seem sufficient. If for no other reason, I can’t imagine how they would synthesize names for the properties / cases.

3 Likes

I think you might be right here, I had hard times thinking of a way to express the enum in an understandable way. Having a variadic enum like above does not necessarily mean that it cases should act always in the same way. You could create a variadic Optional with two cases but some would have a variadic payload (not saying that we should do, but that it would be possible with variadic generics).

I would love to see some GYB similar static system for variadic generics here, or it would be really limited what we can express with bare variadic generics.

You can also use Sourcery today to generate code using Swift instead of Python if you want. Hopefully someday both Sourcery and GYB become obsolete and replaced with macros and other static metaprogramming features built directly into the language itself though.

4 Likes

Sure, but that still would not solve the issue with variadic generics as we would not have any way in iterating over the length of the generic type parameter list.

I’m no expert, but it seems like it would be possible to expose the actual type list to a static metaprogram. I don’t know how this would work but hopefully it will be possible to support these use cases one way or another “in the fullness of time”.

2 Likes

In reality your suggestion makes perfect sense because when dealing with a tuple of unknown length you cannot access the last member by name nor index. Good catch!

func gimmeLast<T... : P>(_ values: T...) -> #someWayToExpressLastTypeOf(T) {
  let (_..., last) = values
  return last
}
1 Like

@DevAndArtist I think I understand what you are trying to do here, but like @anandabits says I also think that the support for this is beyond the power of Variadic Generics. This is because (at least in my view) VG let you express a "collection of types" of some shape, but a type they remain nonetheless :confused:

Regardless, I'll definitely try to think of something for this issue!

I had not written it expressly in the proposal even if I has already thought about this, but you are right - this seems an obvious and really useful extension to the model, and I do not think it will complicate the proposal too much!

We should think about a potential way to match the "size" of the various VGs, if this is something needed, but I'm not too sure that this being a real issue.

Well the general VG is a simple pack of types, which is totally fine and I'd argue that it's understood by most people. The issue with the above Union enum is that on top of VG we would need a way to tell the compiler how it should 'un-pack' the list so we can make more use of it. I don't think that VG should be limited by T... arguments.

Assuming we could write cases that start with a number (we totally should btw. enum Dimension { case 1D, 2D, 3D ... }):

// We would need something along these lines to be able to tell the 
// compiler what to do.
enum Union<T...> {
% for (type, index) in <T...>:
  case ${index}(${type})
% end

  ...
}