Skip to content
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 · 20 comments
Open

Support variadic generics #1251

ooflorent opened this issue Jan 7, 2016 · 20 comments

Comments

@ooflorent
Copy link

@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
Copy link
Member

@samwgoldman samwgoldman commented Jan 7, 2016

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

@ooflorent
Copy link
Author

@ooflorent 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
Copy link
Member

@samwgoldman samwgoldman commented Jan 7, 2016

What would the implementation of createEntity be?

@ooflorent
Copy link
Author

@ooflorent 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
Copy link

@rjbailey 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
Copy link
Member

@samwgoldman 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
Copy link
Author

@ooflorent ooflorent commented Jan 8, 2016

@samwgoldman Yes it is similar.

@ooflorent
Copy link
Author

@ooflorent 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 mentioned this issue Aug 28, 2016
2 of 2 tasks complete
@Macil
Copy link
Contributor

@Macil 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
Copy link
Contributor

@vkurchatkin 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
Copy link

@dchambers 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
Copy link

@dchambers 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
Copy link

@dszakallas 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
Copy link
Contributor

@nmn 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
Copy link
Contributor

@nmn 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
Copy link

@cameron-martin cameron-martin commented Aug 14, 2017

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

microsoft/TypeScript#5453

@sibelius
Copy link

@sibelius sibelius commented Jul 18, 2018

this has landed on typescript 3

@goodmind
Copy link
Contributor

@goodmind goodmind commented Mar 23, 2019

@sibelius it isn't, microsoft/TypeScript#5453 is still open

@kevinbarabash
Copy link

@kevinbarabash kevinbarabash commented Aug 4, 2019

It would be nice if whatever we end up with worked with $Pred. Currently $Pred is typed in the following way:

$Pred<1> => (x_0: any) => mixed
$Pred<2> => (x_0: any, x_1) => mixed
...

It would be nice if $Pred could act more like:

$Pred<...Types> => (...args: Types) => mixed

Where Types is a tuple corresponding to the parameter types.

@asazernik
Copy link

@asazernik asazernik commented Feb 5, 2020

It would be nice if $Pred could act more like:

$Pred<...Types> => (...args: Types) => mixed

Where Types is a tuple corresponding to the parameter types.

More generally, I've got a use case where I want to statically type case here is having generics for functions, where the the function can have any number of arguments. e.g.

function logFunction<Args: $Tuple, RetVal>(
    name: string, f: (...Args) => RetVal
): (...Args) => RetVal {
    return (...(args: Args)) => {
        console.log(`calling ${name} with arguments: ${args.toString}`);
        return f(args);
    }
}

function promisifyfunction<Args: $Tuple, RetVal>(
    f: (...Args) => RetVal
): (...Args) => Promise<RetVal> {
    return (...(args: Args)) => Promise(f(...args));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet