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

Hack pipes and TypeScript inference of curried functions #219

Open
js-choi opened this issue Sep 18, 2021 · 3 comments
Open

Hack pipes and TypeScript inference of curried functions #219

js-choi opened this issue Sep 18, 2021 · 3 comments
Labels
documentation Improvements or additions to documentation

Comments

@js-choi
Copy link
Collaborator

js-choi commented Sep 18, 2021

Spinning this out of tc39/proposal-hack-pipes#18 (thanks @OliverJAsh). He raised an important point: that rx.pipe, etc. currently work with but would not work with Hack pipes (or F# pipes?).

We need to document this in the explainer in an FAQ.

To be clear, this is not due to any inherent mathematical property of Hack pipes.

This is a manifestation of a limitation of TypeScript’s current inference algorithm: it can only unify types from left to right, and it cannot unify types in expressions like map(i => i + 1)(items).

This is an incidental TypeScript limitation, about which @OliverJAsh has been raising issues for several years (see microsoft/TypeScript#15680, as well as the related microsoft/TypeScript#22081, microsoft/TypeScript#25826, microsoft/TypeScript#29904, microsoft/TypeScript#30134).

Indeed, this limitation is why RxJS and Ramda must use a brittle workaround for their pipe functions: they manually overload the arity of their pipe functions up to a finite number. For example, RxJS manually overloads the arity of rx.pipe ten times:

export function pipe(): typeof identity;
export function pipe<T, A>(fn1: UnaryFunction<T, A>): UnaryFunction<T, A>;
export function pipe<T, A, B>(fn1: UnaryFunction<T, A>, fn2: UnaryFunction<A, B>): UnaryFunction<T, B>;
export function pipe<T, A, B, C>(fn1: UnaryFunction<T, A>, fn2: UnaryFunction<A, B>, fn3: UnaryFunction<B, C>): UnaryFunction<T, C>;
export function pipe<T, A, B, C, D>(
  fn1: UnaryFunction<T, A>,
  fn2: UnaryFunction<A, B>,
  fn3: UnaryFunction<B, C>,
  fn4: UnaryFunction<C, D>
): UnaryFunction<T, D>;
export function pipe<T, A, B, C, D, E>(
  fn1: UnaryFunction<T, A>,
  fn2: UnaryFunction<A, B>,
  fn3: UnaryFunction<B, C>,
  fn4: UnaryFunction<C, D>,
  fn5: UnaryFunction<D, E>
): UnaryFunction<T, E>;
export function pipe<T, A, B, C, D, E, F>(
  fn1: UnaryFunction<T, A>,
  fn2: UnaryFunction<A, B>,
  fn3: UnaryFunction<B, C>,
  fn4: UnaryFunction<C, D>,
  fn5: UnaryFunction<D, E>,
  fn6: UnaryFunction<E, F>
): UnaryFunction<T, F>;
export function pipe<T, A, B, C, D, E, F, G>(
  fn1: UnaryFunction<T, A>,
  fn2: UnaryFunction<A, B>,
  fn3: UnaryFunction<B, C>,
  fn4: UnaryFunction<C, D>,
  fn5: UnaryFunction<D, E>,
  fn6: UnaryFunction<E, F>,
  fn7: UnaryFunction<F, G>
): UnaryFunction<T, G>;
export function pipe<T, A, B, C, D, E, F, G, H>(
  fn1: UnaryFunction<T, A>,
  fn2: UnaryFunction<A, B>,
  fn3: UnaryFunction<B, C>,
  fn4: UnaryFunction<C, D>,
  fn5: UnaryFunction<D, E>,
  fn6: UnaryFunction<E, F>,
  fn7: UnaryFunction<F, G>,
  fn8: UnaryFunction<G, H>
): UnaryFunction<T, H>;
export function pipe<T, A, B, C, D, E, F, G, H, I>(
  fn1: UnaryFunction<T, A>,
  fn2: UnaryFunction<A, B>,
  fn3: UnaryFunction<B, C>,
  fn4: UnaryFunction<C, D>,
  fn5: UnaryFunction<D, E>,
  fn6: UnaryFunction<E, F>,
  fn7: UnaryFunction<F, G>,
  fn8: UnaryFunction<G, H>,
  fn9: UnaryFunction<H, I>
): UnaryFunction<T, I>;
export function pipe<T, A, B, C, D, E, F, G, H, I>(
  fn1: UnaryFunction<T, A>,
  fn2: UnaryFunction<A, B>,
  fn3: UnaryFunction<B, C>,
  fn4: UnaryFunction<C, D>,
  fn5: UnaryFunction<D, E>,
  fn6: UnaryFunction<E, F>,
  fn7: UnaryFunction<F, G>,
  fn8: UnaryFunction<G, H>,
  fn9: UnaryFunction<H, I>,
  ...fns: UnaryFunction<any, any>[]
): UnaryFunction<T, unknown>;

As can be seen, this means that rx.pipe only supports automatic TypeScript inference when piping a value through less than nine functions; more than ten functions would require manual typing.

Again, this is not due to any inherent mathematical property of Hack pipes. In fact, this probably would not work with F# pipes either. It is the result of not being able override the arity of a pipe operator and to specify the type of each arity, as RxJS does above (and as Ramda, etc. do too).

The amazing TypeScript team has been working hard on a new, more robust type inferencer: one that improves its type unification so that it retains free types (microsoft/TypeScript#30134). But of course this means that we can’t do map(i => i + 1)(items) today, and we therefore can’t do items |> map(i => i + 1)(^) today. (I’m sorry that Hack pipes don’t give a good answer to this now—other than to manually annotate types as needed (which TypeScript developers often already have to do) and hope for microsoft/TypeScript#30134 to land.)

In any case, the explainer does not talk about this. This is a deficiency of the explainer. We need to fix this sometime.

This issue tracks the fixing of this deficiency in the explainer (lack of discussion regarding TypeScript’s current limitations). Please try to keep the issue on topic (e.g., comments about the importance of tacit programming would be off topic), and please try to follow the code of conduct (and report violations of others’ conduct that violates it to tc39-conduct-reports@googlegroups.com). Please also try to read CONTRIBUTING.md and How to Give Helpful Feedback. Thank you!

@acutmore
Copy link

acutmore commented Sep 18, 2021

If it's useful to anyone. The following utility function can wrap a function so that the receiver can be passed as the first argument, which currently works better for TypeScript inference.

function uncurryReceiver<Args extends any[], R, O>(
    op: (...args: Args) => (receiver: R) => O
) {
    return function(receiver: R, ...args: Args): O {
        return op(...args)(receiver);
    }
}

Example usage:

function map<V, R>(f: (v: V) => R) {
    return function(arr: Array<V>): Array<R> {
        return arr.map(f);
    }
}

map(v => v + 1)([1, 2, 3]);   // no inference, 'v' is 'unknown' ❌

const _map = uncurryReceiver(map);

_map([1, 2, 3], v => v + 1);  // 'v' is inferred as 'number' ✅

https://www.typescriptlang.org/play?#code/GYVwdgxgLglg9mABOCIBOaCeAlAphXGAN1zQB4BBNAcwGdFcAPKXMAE3oEMxMBtAXQA0ibMIDyAPgAUAWABQASDgAHAFyIpAOm2catdVToBKRAF4JGtPkIk067CfOIx8kwG95Cq1HRJQkWAQpKwJiUnthbU1dOgM9I3UxRA9FL1wfNCQVLR144OswtCMAbk8AX3kKuXl-aHgkAFtOZTIANWFsaWB1KSJ1VscLB2TPb19EWsCwXri0Tkw2iQTEKjmFzpHUsczEIk0m5SlgEvLK+XkD3rMLIkQAakQARiMpXkfhACZhAGZ+EsQAQB6QGIMBwRAwMDAUgwyC4YQAciICIh9AR4AA1mCAO5gFGIQAy5Oc5BAELQoIgAPoHMzISDoLB4UK2KQHE7VOTU5qvd6IL6IX7CW5OW4PZ7FIEgpEomD0SHQjC4NiIThosAgBoAI1IKMAoOTyIA

@js-choi js-choi added the documentation Improvements or additions to documentation label Sep 19, 2021
@ken-okabe

This comment has been minimized.

@js-choi js-choi changed the title Hack pipes and TypeScript inference Hack pipes and TypeScript inference of curried functions Sep 24, 2021
@js-choi

This comment has been minimized.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
None yet
Development

No branches or pull requests

3 participants