[Completing Generics] Variadic generics


(Jonathan Tang) #1

The application of this to function types is most interesting to me. Right
now, when I need client code to register callbacks of potentially different
arities, I make the function signature take an Args structure with a method
to unpack it by position:

struct Args {
  func arg<T>(i: Int) -> T {
    // Bounds-checking and dynamic typecasting/marshalling
    guard let result = myValue as? T else {
      throw TypeConversionError()
    }
    return result
  }
}

func addHandler(name: String, handler: Args throws -> AnyObject) {
  ....
}

func foo(x: String) { ... }
func bar(y: Int, z: String) { ... }

addHandler("foo", { args in try foo(args.arg(0)) })
addHandler("bar", { args in try bar(args.arg(0), args.arg(1)) })

If variadics could let the client just do

addHandler("foo", foo)
addHandler("bar", bar)

and then preserve the type signature of foo & bar as they're stored in a
dictionary, that'd be great for my library's users. Repeated across the
whole library ecosystem, it'd make it much easier for one library's
functions to play nice with another library's callbacks, because you
wouldn't need proprietary adapter classes that force a dependency on
another library just so that client code can use the two together easily.

But this only works if the type is not erased, i.e. if I can define a
dictionary of type [String : (...ParameterType throws -> AnyObject)] and
then call a function with apply(handlers["foo"], ["myParam"]). This
requires at least three features:

1. Variadic generics - and not just in function signatures, but also as
part of the function *type*, and as structure members, and as associated
Element types for Dictionary etc.
2. An array-splat operator which lets me apply these functions to a data
structure, checking the types dynamically as it converts them, *or*
3. A tuple-splat operator to apply functions to a tuple, *plus* an
array-to-tuple conversion.

The actual data that gets passed to the functions is dynamic and subject to
error-checking, so it doesn't help if it only works with tuples and not
arrays etc.

Not sure how likely the combination of these happening is, but I think it'd
be a big win for Swift's library ecosystem. It's hard to pass callbacks
across third-party libraries otherwise, because the type signatures depend
upon proprietary types that won't be in the receiving library.

···

On Wed, Mar 2, 2016 at 5:22 PM, Douglas Gregor via swift-evolution < swift-evolution@swift.org> wrote:

*Variadic generics*

Currently, a generic parameter list contains a fixed number of generic
parameters. If one has a type that could generalize to any number of
generic parameters, the only real way to deal with it today involves
creating a set of types. For example, consider the standard library’s “zip”
function. It returns one of these when provided with two arguments to zip
together:

public struct Zip2Sequence<Sequence1 : Sequence,
                           Sequence2 : Sequence> : Sequence { … }

public func zip<Sequence1 : Sequence, Sequence2 : Sequence>(
              sequence1: Sequence1, _ sequence2: Sequence2)
            -> Zip2Sequence<Sequence1, Sequence2> { … }

Supporting three arguments would require copy-paste of those of those:

public struct Zip3Sequence<Sequence1 : Sequence,
                           Sequence2 : Sequence,
                           Sequence3 : Sequence> : Sequence { … }

public func zip<Sequence1 : Sequence, Sequence2 : Sequence, Sequence3 :
>(
              sequence1: Sequence1, _ sequence2: Sequence2, _ sequence3:
sequence3)
            -> Zip3Sequence<Sequence1, Sequence2, Sequence3> { … }

Variadic generics would allow us to abstract over a set of generic
parameters. The syntax below is hopelessly influenced by C++11 variadic
templates <http://www.jot.fm/issues/issue_2008_02/article2/> (sorry),
where putting an ellipsis (“…”) to the left of a declaration makes it a
“parameter pack” containing zero or more parameters and putting an ellipsis
to the right of a type/expression/etc. expands the parameter packs within
that type/expression into separate arguments. The important part is that we
be able to meaningfully abstract over zero or more generic parameters, e.g.:

public struct ZipIterator<... *Iterators* : IteratorProtocol> : Iterator
{ *// zero or more type parameters, each of which conforms to
IteratorProtocol*
  public typealias Element = (*Iterators.Element...*)
  *// a tuple containing the element types of each iterator in Iterators*

  var (*...iterators*): (*Iterators...*) *// zero or more stored
properties, one for each type in Iterators*
  var reachedEnd: Bool = false

  public mutating func next() -> Element? {

    if reachedEnd { return nil }

    guard let values = (*iterators.next()...*) { *// call “next” on
each of the iterators, put the results into a tuple named “values"*

      reachedEnd = true

      return nil

    }

    return values

  }
}

public struct ZipSequence<*...Sequences* : Sequence> : Sequence {
  public typealias Iterator = ZipIterator<*Sequences.Iterator...*> *//
get the zip iterator with the iterator types of our Sequences*

  var (...*sequences*): (*Sequences**...*) *// zero or more stored
properties, one for each type in Sequences*

  *// details ...*
}

Such a design could also work for function parameters, so we can pack
together multiple function arguments with different types, e.g.,

public func zip<*... Sequences : SequenceType*>(*... sequences:
Sequences...*)
            -> ZipSequence<*Sequences...*> {
  return ZipSequence(*sequences...*)
}

Finally, this could tie into the discussions about a tuple “splat”
operator. For example:

func apply<... Args, Result>(fn: (Args...) -> Result, *// function
taking some number of arguments and producing Result*
                           args: (Args...)) -> Result { *// tuple of
arguments*
  return fn(*args...*) // expand the
arguments in the tuple “args” into separate arguments
}


(Jonathan Tang) #2

*Variadic generics*

Currently, a generic parameter list contains a fixed number of generic
parameters. If one has a type that could generalize to any number of
generic parameters, the only real way to deal with it today involves
creating a set of types. For example, consider the standard library’s “zip”
function. It returns one of these when provided with two arguments to zip
together:

public struct Zip2Sequence<Sequence1 : Sequence,
                           Sequence2 : Sequence> : Sequence { … }

public func zip<Sequence1 : Sequence, Sequence2 : Sequence>(
              sequence1: Sequence1, _ sequence2: Sequence2)
            -> Zip2Sequence<Sequence1, Sequence2> { … }

Supporting three arguments would require copy-paste of those of those:

public struct Zip3Sequence<Sequence1 : Sequence,
                           Sequence2 : Sequence,
                           Sequence3 : Sequence> : Sequence { … }

public func zip<Sequence1 : Sequence, Sequence2 : Sequence, Sequence3 :
>(
              sequence1: Sequence1, _ sequence2: Sequence2, _ sequence3:
sequence3)
            -> Zip3Sequence<Sequence1, Sequence2, Sequence3> { … }

Variadic generics would allow us to abstract over a set of generic
parameters. The syntax below is hopelessly influenced by C++11 variadic
templates <http://www.jot.fm/issues/issue_2008_02/article2/> (sorry),
where putting an ellipsis (“…”) to the left of a declaration makes it a
“parameter pack” containing zero or more parameters and putting an ellipsis
to the right of a type/expression/etc. expands the parameter packs within
that type/expression into separate arguments. The important part is that we
be able to meaningfully abstract over zero or more generic parameters, e.g.:

public struct ZipIterator<... *Iterators* : IteratorProtocol> : Iterator
{ *// zero or more type parameters, each of which conforms to
IteratorProtocol*
  public typealias Element = (*Iterators.Element...*)
    *// a tuple containing the element types of each iterator in
Iterators*

  var (*...iterators*): (*Iterators...*) *// zero or more stored
properties, one for each type in Iterators*
  var reachedEnd: Bool = false

  public mutating func next() -> Element? {

    if reachedEnd { return nil }

    guard let values = (*iterators.next()...*) { *// call “next” on
each of the iterators, put the results into a tuple named “values"*

      reachedEnd = true

      return nil

    }

    return values

  }
}

public struct ZipSequence<*...Sequences* : Sequence> : Sequence {
  public typealias Iterator = ZipIterator<*Sequences.Iterator...*> *//
get the zip iterator with the iterator types of our Sequences*

  var (...*sequences*): (*Sequences**...*) *// zero or more stored
properties, one for each type in Sequences*

  *// details ...*
}

Such a design could also work for function parameters, so we can pack
together multiple function arguments with different types, e.g.,

public func zip<*... Sequences : SequenceType*>(*... sequences:
Sequences...*)
            -> ZipSequence<*Sequences...*> {
  return ZipSequence(*sequences...*)
}

Finally, this could tie into the discussions about a tuple “splat”
operator. For example:

func apply<... Args, Result>(fn: (Args...) -> Result, *// function
taking some number of arguments and producing Result*
                           args: (Args...)) -> Result { *// tuple of
arguments*
  return fn(*args...*) // expand the
arguments in the tuple “args” into separate arguments
}

The application of this to function types is most interesting to me.
Right now, when I need client code to register callbacks of potentially
different arities, I make the function signature take an Args structure
with a method to unpack it by position:

struct Args {
  func arg<T>(i: Int) -> T {
    // Bounds-checking and dynamic typecasting/marshalling
    guard let result = myValue as? T else {
      throw TypeConversionError()
    }
    return result
  }
}

func addHandler(name: String, handler: Args throws -> AnyObject) {
  ....
}

func foo(x: String) { ... }
func bar(y: Int, z: String) { ... }

addHandler("foo", { args in try foo(args.arg(0)) })
addHandler("bar", { args in try bar(args.arg(0), args.arg(1)) })

If variadics could let the client just do

addHandler("foo", foo)
addHandler("bar", bar)

and then preserve the type signature of foo & bar as they're stored in a
dictionary, that'd be great for my library's users. Repeated across the
whole library ecosystem, it'd make it much easier for one library's
functions to play nice with another library's callbacks, because you
wouldn't need proprietary adapter classes that force a dependency on
another library just so that client code can use the two together easily.

But this only works if the type is not erased, i.e. if I can define a
dictionary of type [String : (...ParameterType throws -> AnyObject)] and
then call a function with apply(handlers["foo"], ["myParam"]). This
requires at least three features:

1. Variadic generics - and not just in function signatures, but also as
part of the function *type*, and as structure members, and as associated
Element types for Dictionary etc.
2. An array-splat operator which lets me apply these functions to a data
structure, checking the types dynamically as it converts them, *or*
3. A tuple-splat operator to apply functions to a tuple, *plus* an
array-to-tuple conversion.

4. Probably "Opening existentials" too, to make the array-to-tuple or
array-to-function conversion work. The data in the array is heterogenous,
as are the variadic generic parameters. It could work if you could loop
over variadic parameters in a type and attempt to unwrap the existential in
the array as that type, reporting an error if not.

···

On Thu, Mar 10, 2016 at 12:43 AM, Jonathan Tang <jonathan.d.tang@gmail.com> wrote:

On Wed, Mar 2, 2016 at 5:22 PM, Douglas Gregor via swift-evolution < > swift-evolution@swift.org> wrote:

The actual data that gets passed to the functions is dynamic and subject
to error-checking, so it doesn't help if it only works with tuples and not
arrays etc.

Not sure how likely the combination of these happening is, but I think
it'd be a big win for Swift's library ecosystem. It's hard to pass
callbacks across third-party libraries otherwise, because the type
signatures depend upon proprietary types that won't be in the receiving
library.