Search code examples
flowtypetypecheckingstatic-typing

How to specify a list of generics of unknown/arbitrary size


Note: I started a discussion on Github about this subject.

I have a zip function, for now it is typed for iterables of the same type T. I would like to have this typed for arbitrary mixed input type but still conserving the matching output type, for example, if the input type [Iterable<T>, Iterable<U>] I want the output type to be Iterable<[T, U]>. Is it possible to have this for arbitrary input size? I basically want to say, if you have this list of type as input you'll have them as output.

Here is the current version of my zip:

export function *zip<T>(...iterables:Array<Iterable<T>>): Iterable<Array<T>> {
   const iterators = iterables.map(iterable => iter(iterable));
   while(true){
      const items = iterators.map(iterator => iterator.next());
      if (items.some(item => item.done)){
         return;
      }
      yield ((items.map(item => { return item.value }): Array<any>): Array<T>);
  }
}

export function *iter<T>(iterable:Iterable<T>): Iterator<T> {
   yield* iterable;
}

Current best solution by AndrewSouthpaw:

declare function zip<A, B>(Iterable<A>, Iterable<B>): Iterable<[A, B]>;
declare function zip<A, B, C>(Iterable<A>, Iterable<B>, Iterable<C>): Iterable<[A, B, C]>;
declare function zip<A, B, C, D>(Iterable<A>, Iterable<B>, Iterable<C>, Iterable<D>): Iterable<[A, B, C, D]>;
export function *zip<T>(...iterables:Array<Iterable<T>>): Iterable<Array<T>> {
   const iterators = iterables.map(iterable => iter(iterable));
   while(true){
      const items = iterators.map(iterator => iterator.next());
      if (items.some(item => item.done)){
         return;
      }
      yield ((items.map(item => { return item.value }): Array<any>): Array<T>);
  }
}

It works as expected when called with 4, 3 or 2 iterables, when called with 5 or more arguments flow will simply say that zip can only be called with 4 or less arguments. Of course we could add as many function signature as we like to get it to work for 5, 6 or any number N of arguments, but that would require to declare N distinct signatures (which is a bit ugly). On the other hand this strategy does not allow to have an unbounded number of arguments (like the spread operator does). I'm still looking for that.


This raised a more general question, is there any language in which this exists?

I really have the feeling that this can be done in theory (not necessarily in flow), on the other hand I can't recall of a statically typed language in which I've done/seen that (I would also be interested in seeing this kind of type checking in any language).

To be a bit more specific, my feeling is that if you have a type checking system in which (by definition) all types are statically known (any variable has a known type x) then function f: Array<Iterable<x>> -> Iterable<Array<x>> is always called on a known type x. Therefore we should be able to statically decide what type f will return given x (whether x is a single generic type or a list of generic types).

The same goes for the function itself, if you have a type x as input, then you only need to check that your function preserve type x.

Maybe this needs to be defined recursively in some languages, that would also be interesting to see.


Solution

  • Here is the working solution. All credit goes to jbrown215 from Flow team, he found the idea of using $ReadOnlyArray<mixed> here:

    export function *zip<T: $ReadOnlyArray<mixed>>(...iterables:Array<Iterable<T>>): Iterable<Array<T>> {
       const iterators = iterables.map(iterable => iter(iterable));
       while(true){
          const items = iterators.map(iterator => iterator.next());
          if (items.some(item => item.done)){
             return;
          }
          yield ((items.map(item => { return item.value }): Array<any>): Array<T>);
      }
    }
    
    export function *iter<T>(iterable:Iterable<T>): Iterator<T> {
       yield* iterable;
    }