facebook / flow

Adds static typing to JavaScript to improve developer productivity and code quality.
https://flow.org/
MIT License
22.09k stars 1.86k forks source link

Support variadic generics #1251

Open ooflorent opened 8 years ago

ooflorent commented 8 years ago

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 commented 8 years ago

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

ooflorent commented 8 years ago

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 commented 8 years ago

What would the implementation of createEntity be?

ooflorent commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

@samwgoldman Yes it is similar.

ooflorent commented 8 years ago

@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 commented 8 years ago

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 commented 8 years ago

@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 commented 8 years ago

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 8 years ago

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 :smile:

dszakallas commented 7 years ago

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

const prepend = (arg, fn) => (...rest) => fn(arg, ...rest)
nmn commented 7 years ago

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 commented 7 years ago

@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 commented 7 years ago

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

https://github.com/Microsoft/TypeScript/issues/5453

sibelius commented 6 years ago

this has landed on typescript 3

goodmind commented 5 years ago

@sibelius it isn't, https://github.com/Microsoft/TypeScript/issues/5453 is still open

kevinbarabash commented 5 years ago

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 commented 4 years ago

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 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));
}