New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support variadic generics #1251

Open
ooflorent opened this Issue Jan 7, 2016 · 17 comments

Comments

Projects
None yet
@ooflorent

ooflorent commented Jan 7, 2016

I was wondering if Flow supports variadic generics or would support them. I'm trying to achieve the this but I can't figure out how to do it.

type Components<...Cs> = {[_: string]: Class<Cs...>}

The main idea is to declare a function that accepts {[_: string]: Class<Cs...>} and returns {[_: string]: Cs...>. I tried using different flow types but it seems currently impossible to achieve it.

Edit: Using an union type could probably solve this but it raises another problem.

declare class Entity {}

declare class EntityManager<Cs> {
  create(): Entity & {[_: string]: Cs};
}

declare function createManager<Cs>(components: {[_:string]: Class<Cs>}): EntityManager<Cs>

type Component = A | B | C;

declare class A {}
declare class B {}
declare class C {}

var em = createManager({
  a: A,    // <-- Here is the issue.
  b: B,    // <-- Since we cannot do `createManager<Component>( ... )` flow raises an
  c: C,    // <-- incompatible type error.
})
@samwgoldman

This comment has been minimized.

Show comment
Hide comment
@samwgoldman

samwgoldman Jan 7, 2016

Member

How is the {[_: string]: Cs...} type used? I'm unclear about what you're asking for here.

Member

samwgoldman commented Jan 7, 2016

How is the {[_: string]: Cs...} type used? I'm unclear about what you're asking for here.

@ooflorent

This comment has been minimized.

Show comment
Hide comment
@ooflorent

ooflorent Jan 7, 2016

Well, the following is pseudo code to highlight the main idea:

class Foo {}
class Bar {}
class Baz {}
class Qux {}

var entity1 = createEntity({foo: Foo, bar: Bar, baz: Baz})
var entity2 = createEntity({foo: Foo, qux: Qux})

Basically createEntity accepts an object where each value is a Class.
The resulting entity types would be:

type Entity1 = {
  foo: ?Foo;
  bar: ?Bar;
  baz: ?Baz;
}

type Entity2 = {
  foo: ?Foo;
  qux: ?Qux;
}

I think createEntity signature would be something like:

function createEntity<...Cs>(cs: {[_: string]: Class<Cs...>}): {[_: string]: ?Cs...}

I want to avoid any to keep the code strictly typed but would like it to be generic.
Any thoughts on how to achieve it?

ooflorent commented Jan 7, 2016

Well, the following is pseudo code to highlight the main idea:

class Foo {}
class Bar {}
class Baz {}
class Qux {}

var entity1 = createEntity({foo: Foo, bar: Bar, baz: Baz})
var entity2 = createEntity({foo: Foo, qux: Qux})

Basically createEntity accepts an object where each value is a Class.
The resulting entity types would be:

type Entity1 = {
  foo: ?Foo;
  bar: ?Bar;
  baz: ?Baz;
}

type Entity2 = {
  foo: ?Foo;
  qux: ?Qux;
}

I think createEntity signature would be something like:

function createEntity<...Cs>(cs: {[_: string]: Class<Cs...>}): {[_: string]: ?Cs...}

I want to avoid any to keep the code strictly typed but would like it to be generic.
Any thoughts on how to achieve it?

@samwgoldman

This comment has been minimized.

Show comment
Hide comment
@samwgoldman

samwgoldman Jan 7, 2016

Member

What would the implementation of createEntity be?

Member

samwgoldman commented Jan 7, 2016

What would the implementation of createEntity be?

@ooflorent

This comment has been minimized.

Show comment
Hide comment
@ooflorent

ooflorent Jan 7, 2016

I've created of what I'm trying to achieve.
https://gist.github.com/ooflorent/84260ef9aa8498fb63b1

Example usage:

const em = createManager({transform: Transform2D, body: Body, sprite: Sprite})
const entity = em.create()

In the above example, entity type would be defined as:

declare class Entity {
  transform: Transform2D;
  body: Body;
  sprite: Sprite;
}

Calling createManager with another object shape would recompile an Entity class and shape it according createManager argument.

ooflorent commented Jan 7, 2016

I've created of what I'm trying to achieve.
https://gist.github.com/ooflorent/84260ef9aa8498fb63b1

Example usage:

const em = createManager({transform: Transform2D, body: Body, sprite: Sprite})
const entity = em.create()

In the above example, entity type would be defined as:

declare class Entity {
  transform: Transform2D;
  body: Body;
  sprite: Sprite;
}

Calling createManager with another object shape would recompile an Entity class and shape it according createManager argument.

@rjbailey

This comment has been minimized.

Show comment
Hide comment
@rjbailey

rjbailey Jan 8, 2016

I have also wanted variadic generics, when writing a function that behaves similarly to Promise.all. I ended up writing non-variadic type-safe versions:

  static all2<A, B, U>(
    resultA: Result<A>,
    resultB: Result<B>,
    func: (a: A, b: B) => U
  ): Result<U> {
    return Result.all([resultA, resultB])
      .map(a => func(a[0], a[1]));
  }

  static all3<A, B, C, U>(
    resultA: Result<A>,
    resultB: Result<B>,
    resultC: Result<C>,
    func: (a: A, b: B, c: C) => U
  ): Result<U> {
    return Result.all([resultA, resultB, resultC])
      .map(a => func(a[0], a[1], a[2]));
  }

  // ...etc

rjbailey commented Jan 8, 2016

I have also wanted variadic generics, when writing a function that behaves similarly to Promise.all. I ended up writing non-variadic type-safe versions:

  static all2<A, B, U>(
    resultA: Result<A>,
    resultB: Result<B>,
    func: (a: A, b: B) => U
  ): Result<U> {
    return Result.all([resultA, resultB])
      .map(a => func(a[0], a[1]));
  }

  static all3<A, B, C, U>(
    resultA: Result<A>,
    resultB: Result<B>,
    resultC: Result<C>,
    func: (a: A, b: B, c: C) => U
  ): Result<U> {
    return Result.all([resultA, resultB, resultC])
      .map(a => func(a[0], a[1], a[2]));
  }

  // ...etc
@samwgoldman

This comment has been minimized.

Show comment
Hide comment
@samwgoldman

samwgoldman Jan 8, 2016

Member

Thanks @rjbailey. The Promise.all example is a bit easier to wrap my head around. We are actually kicking around some ideas to make typing these kinds of APIs easier, but it's still very much in the primordial phase.

@ooflorent, do you think Promise.all is similar to your issue? I haven't spent a lot of time trying to grok the gist you shared.

Member

samwgoldman commented Jan 8, 2016

Thanks @rjbailey. The Promise.all example is a bit easier to wrap my head around. We are actually kicking around some ideas to make typing these kinds of APIs easier, but it's still very much in the primordial phase.

@ooflorent, do you think Promise.all is similar to your issue? I haven't spent a lot of time trying to grok the gist you shared.

@ooflorent

This comment has been minimized.

Show comment
Hide comment
@ooflorent

ooflorent Jan 8, 2016

@samwgoldman Yes it is similar.

ooflorent commented Jan 8, 2016

@samwgoldman Yes it is similar.

@ooflorent

This comment has been minimized.

Show comment
Hide comment
@ooflorent

ooflorent Jan 14, 2016

@samwgoldman I've found a way more descriptive use case.

How would you write ES7 typed objects type definitions?
Here is an example using StructType:

const Point2D = new StructType({ x: uint32, y: uint32 })
let p2 = Point2D({x: 10, y: 20})
let x = p2.x // ok
let z = p2.z // TypeError

const Point3D = new StructType({ x: uint32, y: uint32, z: uint32 })
let p3 = Point3D({x: 10, y: 20}) // TypeError

ooflorent commented Jan 14, 2016

@samwgoldman I've found a way more descriptive use case.

How would you write ES7 typed objects type definitions?
Here is an example using StructType:

const Point2D = new StructType({ x: uint32, y: uint32 })
let p2 = Point2D({x: 10, y: 20})
let x = p2.x // ok
let z = p2.z // TypeError

const Point3D = new StructType({ x: uint32, y: uint32, z: uint32 })
let p3 = Point3D({x: 10, y: 20}) // TypeError

@Macil Macil referenced this issue Aug 28, 2016

Merged

Add Flow type declarations #217

2 of 2 tasks complete
@Macil

This comment has been minimized.

Show comment
Hide comment
@Macil

Macil Sep 2, 2016

Contributor

I've been thinking about variadic generics a lot lately. One problem I ran into is that Kefir.combine has a very similar type signature to Promise.all, but Flow's support for Promise.all is hard-coded, so Kefir.combine couldn't be properly typed. These functions' type signatures share some similarities to createEntity too. I think I found a solution that looks nice, though I don't know if it really aligns with how Flow works internally.

// $Wrapped<{a: number, b: string}, Foo> refers to the type {a: Foo<number>, b: Foo<string>}
declare function createEntity<T: Object>(classes: $Wrapped<T, Class>): T;
// (Partial) Bluebird Promise.props: http://bluebirdjs.com/docs/api/promise.props.html
declare function props<T: Object>(obj: $Wrapped<T, Promise>): Promise<T>;

// $Tuple refers to some specific tuple of types like [number, string, boolean].
// $Wrapped<[number, string, boolean], Foo> refers to the type [Foo<number>, Foo<string>, Foo<boolean>].
// Promise.all:
declare function all<T: $Tuple>(arr: $Wrapped<T, Promise>): Promise<T>;

// ~ is an operator where
// type NumberStringTyple = [number, string];
// type NumberStringBooleanDateTuple = NumberStringTyple~[boolean, Date];

// Kefir.combine:
declare function combine<O: $Tuple>(obss: $Wrapped<O, Observable>): Observable<O>;
declare function combine<O: $Tuple, C>(obss: $Wrapped<O, Observable>, combinator: (values: O) => C): Observable<C>;
declare function combine<P: $Tuple, P: $Tuple>(obss: $Wrapped<O, Observable>, passiveObss: $Wrapped<P, Observable>): Observable<O~P>;
declare function combine<O: $Tuple, P: $Tuple, C>(obss: $Wrapped<O, Observable>, passiveObss: $Wrapped<P, Observable>, combinator: (values: O~P) => C): Observable<C>;

I've also been thinking of the type of the compose function as implemented by ramda, lodash, multiple transducer libs, etc, which can take any number of functions, and returns a function that takes the input type of the last function and returns the output type of the first function. I didn't really come up with a generic solution for it, but it seems like everyone implements it about the same way (okay, there are variants where the first called function is allowed to take 1 parameter and some where it can take any number of parameters) and without binding it to specific types (promises, observables, etc), so maybe it deserves its own special type like Promise.all currently has:

declare var compose: $Compose;

Well okay, I also came up with this solution to the variadic generics problem. It's ... not fully developed, but possibly a lot more generic, but it's also asking a lot more out of Flow and not as readable. Maybe someone will see this and realize it's exactly what Flow needs, or it's exactly what Flow doesn't need.

declare function all<T>(arr: T): Promise<$Reduce<T, (C,N) => C~[N], []>>;
declare function compose<T>(...args: T): $Reduce<T, ((a: MID) => OUT, (a: IN) => MID) => (a: IN) => OUT>;
Contributor

Macil commented Sep 2, 2016

I've been thinking about variadic generics a lot lately. One problem I ran into is that Kefir.combine has a very similar type signature to Promise.all, but Flow's support for Promise.all is hard-coded, so Kefir.combine couldn't be properly typed. These functions' type signatures share some similarities to createEntity too. I think I found a solution that looks nice, though I don't know if it really aligns with how Flow works internally.

// $Wrapped<{a: number, b: string}, Foo> refers to the type {a: Foo<number>, b: Foo<string>}
declare function createEntity<T: Object>(classes: $Wrapped<T, Class>): T;
// (Partial) Bluebird Promise.props: http://bluebirdjs.com/docs/api/promise.props.html
declare function props<T: Object>(obj: $Wrapped<T, Promise>): Promise<T>;

// $Tuple refers to some specific tuple of types like [number, string, boolean].
// $Wrapped<[number, string, boolean], Foo> refers to the type [Foo<number>, Foo<string>, Foo<boolean>].
// Promise.all:
declare function all<T: $Tuple>(arr: $Wrapped<T, Promise>): Promise<T>;

// ~ is an operator where
// type NumberStringTyple = [number, string];
// type NumberStringBooleanDateTuple = NumberStringTyple~[boolean, Date];

// Kefir.combine:
declare function combine<O: $Tuple>(obss: $Wrapped<O, Observable>): Observable<O>;
declare function combine<O: $Tuple, C>(obss: $Wrapped<O, Observable>, combinator: (values: O) => C): Observable<C>;
declare function combine<P: $Tuple, P: $Tuple>(obss: $Wrapped<O, Observable>, passiveObss: $Wrapped<P, Observable>): Observable<O~P>;
declare function combine<O: $Tuple, P: $Tuple, C>(obss: $Wrapped<O, Observable>, passiveObss: $Wrapped<P, Observable>, combinator: (values: O~P) => C): Observable<C>;

I've also been thinking of the type of the compose function as implemented by ramda, lodash, multiple transducer libs, etc, which can take any number of functions, and returns a function that takes the input type of the last function and returns the output type of the first function. I didn't really come up with a generic solution for it, but it seems like everyone implements it about the same way (okay, there are variants where the first called function is allowed to take 1 parameter and some where it can take any number of parameters) and without binding it to specific types (promises, observables, etc), so maybe it deserves its own special type like Promise.all currently has:

declare var compose: $Compose;

Well okay, I also came up with this solution to the variadic generics problem. It's ... not fully developed, but possibly a lot more generic, but it's also asking a lot more out of Flow and not as readable. Maybe someone will see this and realize it's exactly what Flow needs, or it's exactly what Flow doesn't need.

declare function all<T>(arr: T): Promise<$Reduce<T, (C,N) => C~[N], []>>;
declare function compose<T>(...args: T): $Reduce<T, ((a: MID) => OUT, (a: IN) => MID) => (a: IN) => OUT>;
@vkurchatkin

This comment has been minimized.

Show comment
Hide comment
@vkurchatkin

vkurchatkin Sep 3, 2016

Collaborator

@AgentME I have a proof of concept implementation, that makes this possible, looks like this:

declare function all<T>(arr: T): Promise<$TupleMap<T, <T>(t: Promise<T>) => T>>;
Collaborator

vkurchatkin commented Sep 3, 2016

@AgentME I have a proof of concept implementation, that makes this possible, looks like this:

declare function all<T>(arr: T): Promise<$TupleMap<T, <T>(t: Promise<T>) => T>>;
@dchambers

This comment has been minimized.

Show comment
Hide comment
@dchambers

dchambers Sep 18, 2016

I think I have another use-case for variadic generics, as I need to type a function so that it has a covariant input parameter, but the Function type doesn't currently support generics -- presumably because there's no support for variadic generics.

Here's some code that highlights the need for this:

class C<Type> {
  funcs: Array<(t: any) => Type>; // we should be using `Type` instead of `any` here

  m<T: Type>(t: T, f: (t: T) => T): T {
    this.funcs.push(f);
    return f(t);
  }
}

type X = {type: 'X', x: number};
type Y = {type: 'Y', y: number};
const x: X = {type: 'X', x: 1};

const c: C<X | Y> = new C();
const f = (v: X): X => v;
c.m(x, f);

If I change the definition of funcs from Array<(t: any) => Type> to Array<(t: Type) => Type> then I get an error because function input parameters are contravariant, yet my functions require various sub-types of Type.

At present, the funcs member variable is effectively typed like this (if the Function type supported generics):

Array<Function<-Type, +Type>>;

whereas I need it to be typed as:

Array<Function<+Type, +Type>>;

dchambers commented Sep 18, 2016

I think I have another use-case for variadic generics, as I need to type a function so that it has a covariant input parameter, but the Function type doesn't currently support generics -- presumably because there's no support for variadic generics.

Here's some code that highlights the need for this:

class C<Type> {
  funcs: Array<(t: any) => Type>; // we should be using `Type` instead of `any` here

  m<T: Type>(t: T, f: (t: T) => T): T {
    this.funcs.push(f);
    return f(t);
  }
}

type X = {type: 'X', x: number};
type Y = {type: 'Y', y: number};
const x: X = {type: 'X', x: 1};

const c: C<X | Y> = new C();
const f = (v: X): X => v;
c.m(x, f);

If I change the definition of funcs from Array<(t: any) => Type> to Array<(t: Type) => Type> then I get an error because function input parameters are contravariant, yet my functions require various sub-types of Type.

At present, the funcs member variable is effectively typed like this (if the Function type supported generics):

Array<Function<-Type, +Type>>;

whereas I need it to be typed as:

Array<Function<+Type, +Type>>;
@dchambers

This comment has been minimized.

Show comment
Hide comment
@dchambers

dchambers Sep 21, 2016

I was able to solve my own particular issue by using the (undocumented) $Subtype<T> type, so that I simply changed this line:

funcs: Array<(t: any) => Type>; // we should be using `Type` instead of `any` here

to this:

funcs: Array<(t: $Subtype<Type>) => Type>;

and all was well in the world again 😄

dchambers commented Sep 21, 2016

I was able to solve my own particular issue by using the (undocumented) $Subtype<T> type, so that I simply changed this line:

funcs: Array<(t: any) => Type>; // we should be using `Type` instead of `any` here

to this:

funcs: Array<(t: $Subtype<Type>) => Type>;

and all was well in the world again 😄

@dszakallas

This comment has been minimized.

Show comment
Hide comment
@dszakallas

dszakallas Mar 25, 2017

I need this too, a stripped down version of my use case is:

const prepend = (arg, fn) => (...rest) => fn(arg, ...rest)

dszakallas commented Mar 25, 2017

I need this too, a stripped down version of my use case is:

const prepend = (arg, fn) => (...rest) => fn(arg, ...rest)
@nmn

This comment has been minimized.

Show comment
Hide comment
@nmn

nmn Mar 28, 2017

Contributor

It's relatively straightforward if you use $ObjMap.

declare function createEntity<T: {}>(obj: T): $ObjMap<T, <X>(klass: Class<X>) => X>;

See working example here.

Contributor

nmn commented Mar 28, 2017

It's relatively straightforward if you use $ObjMap.

declare function createEntity<T: {}>(obj: T): $ObjMap<T, <X>(klass: Class<X>) => X>;

See working example here.

@nmn

This comment has been minimized.

Show comment
Hide comment
@nmn

nmn Mar 28, 2017

Contributor

@szdavid92 Here's something that seems to work in your case:

const prepend = <T, Rest: $ReadOnlyArray<mixed>, R>(
  arg: T,
  fn: (first: T, ...rest: Rest) => R
): ((...rest: Rest) => R) => (...rest: Rest) => fn(arg, ...rest);

Code here

Flow technically has some weak incomplete support for variadic generics through Tuple types.
Tuple types are a subType of $ReadOnlyArray and we can use that to our advantage some of the times.

Contributor

nmn commented Mar 28, 2017

@szdavid92 Here's something that seems to work in your case:

const prepend = <T, Rest: $ReadOnlyArray<mixed>, R>(
  arg: T,
  fn: (first: T, ...rest: Rest) => R
): ((...rest: Rest) => R) => (...rest: Rest) => fn(arg, ...rest);

Code here

Flow technically has some weak incomplete support for variadic generics through Tuple types.
Tuple types are a subType of $ReadOnlyArray and we can use that to our advantage some of the times.

@cameron-martin

This comment has been minimized.

Show comment
Hide comment
@cameron-martin

cameron-martin Aug 14, 2017

For reference, here is typescript's proposal for the same thing:

Microsoft/TypeScript#5453

cameron-martin commented Aug 14, 2017

For reference, here is typescript's proposal for the same thing:

Microsoft/TypeScript#5453

@sibelius

This comment has been minimized.

Show comment
Hide comment
@sibelius

sibelius Jul 18, 2018

this has landed on typescript 3

sibelius commented Jul 18, 2018

this has landed on typescript 3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment