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

Annotation for compose function on flowtype: compose(m1, m2, m3)(...args) -> m1(m2(m3(...args))) #1950

Closed
nodkz opened this issue Jun 15, 2016 · 7 comments

Comments

@nodkz
Copy link
Contributor

nodkz commented Jun 15, 2016

How to annotate compose function, that will be covered on 100%. This function is polymorphic (it can accept different function types and different arguments).

/* @flow */

/**
 * Composes single-argument functions from right to left. The rightmost
 * function can take multiple arguments as it provides the signature for
 * the resulting composite function.
 *
 * @param {...Function} funcs The functions to compose.
 * @returns {Function} A function obtained by composing the argument functions
 * from right to left. For example, compose(f, g, h) is identical to doing
 * (...args) => f(g(h(...args))).
 */
export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg;
  }

  const last = funcs[funcs.length - 1];
  const rest = funcs.slice(0, -1);
  return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args));
}
@ryyppy
Copy link
Contributor

ryyppy commented Jun 15, 2016

Hey @nodkz ...
This question is asked quite frequently, but quite tricky... we have an existing compose declaration for our flow-typed ramda libdef, maybe you get some inspiration here:

https://github.com/flowtype/flow-typed/blob/master/definitions/npm/ramda_v0.21.x/flow_%3E%3Dv0.23.x/ramda_v0.21.x.js#L36

Hope this helps!

@ryyppy
Copy link
Contributor

ryyppy commented Jun 15, 2016

The gist of it: It is only possible to type this by manually writing compose declarations for a function call with 1, 2, 3,... n parameters... assuming that your users don't use a higher number than n.

@nodkz
Copy link
Contributor Author

nodkz commented Jun 15, 2016

@ryyppy you are crazy magician! ;)
I'm blind. I have not seen such assembler-code a long time.

PS. I want use it for graphql-compose. There must be 3 compositions (resolve methods, fields args and object types), which all works via this compose function. This compositions will be assemble via middlewares, so their number can be quite big.

Yesterday with tears in my eyes ;) I splice my lib with graphql-js definitions. And I found a lot of new things with graphql internals, that I didn't know. Thanks to flow and @leebyron type annotations.

@nodkz
Copy link
Contributor Author

nodkz commented Jun 15, 2016

May be somebody suggest better solution?

@ryyppy
Copy link
Contributor

ryyppy commented Jun 16, 2016

@nodkz For clarification, I did not write this code, I am just vary of its existence :-)
Sadly I don't use GraphQL so I can't give a qualified answer on that one...

@nodkz
Copy link
Contributor Author

nodkz commented Jun 16, 2016

@ryyppy no problem! Thanks again.

Gabe Levi provide his solution: (provide it here for future travelers from search engines):

/* 
 CAUTION 
      this solution has wrong order annotation. 
      It works for `reduce`, but in this compose function using `reduceRight`.
      Proper solution can be founв on next comment. 
*/
/* @flow */

function composeImpl(...funcs) {
  if (funcs.length === 0) {
    return arg => arg;
  }

  const last = funcs[funcs.length - 1];
  const rest = funcs.slice(0, -1);
  return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args));
}

var compose:
  ((end: void) => (<T>(x: T) => T)) &
  (<Ta, Tb>(f: (x: Ta) => Tb, end: void) => ((x: Ta) => Tb)) &
  (<Ta, Tb, Tc>(f1: (x: Ta) => Tb, f2: (x: Tb) => Tc, end: void) => ((x: Ta) => Tc)) &
  (<Ta, Tb, Tc, Td>(f1: (x: Ta) => Tb, f2: (x: Tb) => Tc, f3: (x: Tc) => Td, end: void) => ((x: Ta) => Td)) &
  (<T>(...funcs: Array<(x: T) => T>) => ((x: T) => T))
  = (composeImpl: any);

type ERROR = boolean

// Explicitly handled cases work      
(compose()(123) : ERROR); // number ~> boolean
(compose(String)(123) : ERROR); // string ~> boolean
(compose(String, Number)(123) : ERROR); // number ~> boolean
(compose(String, Number, String)(123) : ERROR); // string ~> boolean

// And fallback requires all the functions to take and return the same type
// which is in this case string | number
(compose(String, Number, String, Number)(123) : string | number);

@nodkz nodkz closed this as completed Jun 16, 2016
@nodkz
Copy link
Contributor Author

nodkz commented Jun 16, 2016

Correct fully tested solution (combined from @ryyppy and @gabelevi)

/* @flow */
/* eslint-disable */

/**
 * Composes single-argument functions from right to left. The rightmost
 * function can take multiple arguments as it provides the signature for
 * the resulting composite function.
 *
 * @param {...Function} funcs The functions to compose.
 * @returns {Function} A function obtained by composing the argument functions
 * from right to left. For example, compose(f, g, h) is identical to doing
 * (...args) => f(g(h(...args))).
 */

function composeImpl(...funcs) {
  if (funcs.length === 0) {
    return arg => arg;
  }

  const last = funcs[funcs.length - 1];
  const rest = funcs.slice(0, -1);
  return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args));
}

type FN<A,R> = (a: A) => R;
const compose:
  ((end: void) => (<T>(x: T) => T)) &
  (<A,B>(m1: FN<A,B>, end: void) => FN<A,B>) &
  (<A,B,C>(m1: FN<B,C>, m2: FN<A,B>, end: void) => FN<A,C>) &
  (<A,B,C,D>(m1: FN<C,D>, m2: FN<B,C>, m3: FN<A,B>, end: void) => FN<A,D>) &
  (<A,B,C,D,E>(m1: FN<D,E>, m2: FN<C,D>, m3: FN<B,C>, m4: FN<A,B>, end: void) => FN<A,E>) &
  (<A,B,C,D,E,F>(m1: FN<E,F>, m2: FN<D,E>, m3: FN<C,D>, m4: FN<B,C>, m5: FN<A,B>, end: void) => FN<A,F>) &
  (<A,B,C,D,E,F,G>(m1: FN<F,G>, m2: FN<E,F>, m3: FN<D,E>, m4: FN<C,D>, m5: FN<B,C>, m6: FN<A,B>, end: void) => FN<A,G>) &
  (<A,B,C,D,E,F,G,H>(m1: FN<G,H>, m2: FN<F,G>, m3: FN<E,F>, m4: FN<D,E>, m5: FN<C,D>, m6: FN<B,C>, m7: FN<A,B>, end: void) => FN<A,H>) &
  (<A,B,C,D,E,F,G,H,J>(m1: FN<G,J>, m2: FN<G,H>, m3: FN<F,G>, m4: FN<E,F>, m5: FN<D,E>, m6: FN<C,D>, m7: FN<B,C>, m8: FN<A,B>, end: void) => FN<A,J>) &
  (<A,B,C,D,E,F,G,H,J,K>(m1: FN<J,K>, m2: FN<G,J>, m3: FN<G,H>, m4: FN<F,G>, m5: FN<E,F>, m6: FN<D,E>, m7: FN<C,D>, m8: FN<B,C>, m9: FN<A,B>, end: void) => FN<A,K>) &
  (<A,B,C,D,E,F,G,H,J,K,L>(m1: FN<K,L>, m2: FN<J,K>, m3: FN<G,J>, m4: FN<G,H>, m5: FN<F,G>, m6: FN<E,F>, m7: FN<D,E>, m8: FN<C,D>, m9: FN<B,C>, m10: FN<A,B>, end: void) => FN<A,L>) &
  (<A,R>(...funcs: Array<FN<A,R>>) => FN<A,R>)
  = (composeImpl: any);

export default compose;

@nodkz nodkz changed the title An exercise for flow's gurus. Also this is a good check of strength of the flow engine. Annotation for compose function on flowtype: compose(m1, m2, m3)(...args) -> m1(m2(m3(...args))) Jun 16, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants