The Wayback Machine - https://web.archive.org/web/20201202154845/https://github.com/microsoft/TypeScript/pull/38305
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

Add support for Pipeline Operator #38305

Draft
wants to merge 14 commits into
base: master
from
Draft

Conversation

@Pokute
Copy link

@Pokute Pokute commented May 3, 2020

PR adding pipeline operator support to TypeScript. The proposal is currently a stage 1 proposal for EcmaScript. It "introduces a new operator |> similar to F#, OCaml, Elixir, Elm, Julia, Hack, and LiveScript, as well as UNIX pipes and Haskell's &. It's a backwards-compatible way of streamlining chained function calls in a readable, functional manner, and provides a practical alternative to extending built-in prototypes."

const square = (n: number) => Math.pow(n, 2);
4 |> square |> console.log; // 16

Currently this is a minimal implementation of pipeline operator, but I'm planning on making a F# variant support and after that a Topic/Smart pipeline variant PRs.

Partial application is a good pair for minimal/F# pipeline operator and has it's own PR at #37973. I'm planning on making a separate draft PR that has both features in them.

Code status

This works minimally. The TS transformer generates valid JS and there's working type inference. There are many bugs, inconsistencies with specs, insufficient error messages currently. Most of failing tests are not included in this PR since Playground won't get generated with any failures and having faulty baselines is shady. I'm eagerly awaiting feedback, testing and even help with this PR. Even using and talking of these features will help advance them through TC39 proposal process!

I used @rbuckton 's work on pipelineStage1 branch as a guide.

Testing

Playground link

To test it in your project, add following to your package.json:

{
    "devDependencies": {
        "typescript": "https://typescript.visualstudio.com/cf7ac146-d525-443c-b23c-0d58337efebc/_apis/build/builds/72713/artifacts?artifactName=tgz&fileId=5FDC6B0458B0184B85D790D1EB8AA47C0924EADA6FBC0575C4425A46F70973E002&fileName=/typescript-4.0.0-insiders.20200503.tgz"
    }
}

and then run npm install.

PR state

  • There is an associated issue in the Backlog milestone (required)
  • Code is up-to-date with the master branch
  • You've successfully run gulp runtests locally
  • There are new or updated unit tests validating the change

Why a draft PR for stage 1 proposal?

The TypeScript officially follows EcmaScript / stage 3 proposals. However, I'm opening this draft pull request despite it being only stage 1. The partial application and pipeline proposals now are in a stage where they want people to test the features - write it in their code, find problems, test existing codebases. The usual way these would be done is through a Babel plugin, but since a few years ago, just a plain text editor with syntax highlighting is no longer the default JS development environment.

TypeScript is no longer just a build step. It's used by a significant portion of JS developers for all their development, even when prototyping new code. If the tool chain of these developers requires TypeScript, then a simple Babel plugin won't help. This means that writing TypeScript with those features is impossible without first-class support for them in TypeScript.

But then could we ask them to write just plain JS instead? It turns out that TypeScript is more and more part of normal JavaScript development process with Language Server Protocol pointing out errors in code and providing typing information. Having people encounter red squigglies and disappearing type information as soon as they use the new language feature will not count favourably to the user experience. I believe having TypeScript support for new syntax proposals as important or even more important than having a Babel plugin.

I was considering creating and upkeeping a separate fork, but that would just mean duplication of work. There's already a great infrastructure for TypeScript with playgrounds, CI, etc. With a draft PR, new features can more easily get more eyes, testers and feedback. There will eventually be issues and questions on new features for TypeScript and github.com/microsoft/TypeScript is where anyone would first look up info on partial application.

Of course, I'm not expecting merging until the proposal reaches stage 3.

@Pokute Pokute marked this pull request as draft May 3, 2020
@orta
Copy link
Member

@orta orta commented May 3, 2020

@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

@typescript-bot typescript-bot commented May 3, 2020

Heya @orta, I've started to run the tarball bundle task on this PR at 19fd8b1. You can monitor the build here.

@typescript-bot
Copy link
Collaborator

@typescript-bot typescript-bot commented May 3, 2020

Hey @orta, I've packed this into an installable tgz. You can install it for testing by referencing it in your package.json like so:

{
    "devDependencies": {
        "typescript": "https://typescript.visualstudio.com/cf7ac146-d525-443c-b23c-0d58337efebc/_apis/build/builds/72713/artifacts?artifactName=tgz&fileId=5FDC6B0458B0184B85D790D1EB8AA47C0924EADA6FBC0575C4425A46F70973E002&fileName=/typescript-4.0.0-insiders.20200503.tgz"
    }
}

and then running npm install.


There is also a playground for this build.

@markusjohnsson
Copy link
Contributor

@markusjohnsson markusjohnsson commented May 3, 2020

Nice! I have wondered myself how pipeline operators would play with typescript, especially how the types would flow. You could have went the way of only parsing and type checking, and letting e.g. babel do translation, but since you have also done translation this is really easy to try out.

I did test it out and ran in to a type inference limitation which I hope can be fixed or it will be hard to use with complex types:

const ns = [1, 2, 3, 4, 5];

function map<T, R>(f: (t: T) => R) {
    return (ts: T[]): R[] => ts.map(f);
}

const result = ns |> map(x => x * x);
index.ts:8:16 - error TS2571: Object is of type 'unknown'.

8 ns |> map(x => x * x);

Here T must be inferred from the type of ns.

@rbuckton
Copy link
Member

@rbuckton rbuckton commented May 3, 2020

@Pokute: If it helps at all I have two old branches where I started to look into implementing pipeline as well:

@Pokute
Copy link
Author

@Pokute Pokute commented May 4, 2020

Nice! I have wondered myself how pipeline operators would play with typescript, especially how the types would flow. You could have went the way of only parsing and type checking, and letting e.g. babel do translation, but since you have also done translation this is really easy to try out.

Writing the transformer was actually pretty simple as soon as I had the scanning & parsing in place.

I did test it out and ran in to a type inference limitation which I hope can be fixed or it will be hard to use with complex types:
Here T must be inferred from the type of ns.

I think that the form that TypeScript seems to internally handle it is map(x => x * x)(ns).
This is an interesting case where the type of a function would be resolved from it's usage and it's not directly either. I'm interested whether there are existing cases of this happening within TypeScript.

In the simplest cases, it feels like it should be possible to resolve the type. Beyond that is out of my expertise. :-)

Either way, thank you for an interesting test case!

@phaux
Copy link

@phaux phaux commented May 4, 2020

Is there a way to try pipeline operator and partial application together?

@dustinlacewell
Copy link

@dustinlacewell dustinlacewell commented May 8, 2020

When I found this PR I nearly cried. Thank you.

@lukewestby
Copy link

@lukewestby lukewestby commented May 15, 2020

Hey folks, I stumbled upon this PR from https://github.com/tc39/proposal-pipeline-operator#build-tools. I'm not able to access the tarball mentioned here #38305 (comment) and I'm just wondering if it's been removed intentionally.

@Pokute
Copy link
Author

@Pokute Pokute commented May 15, 2020

Hey folks, I stumbled upon this PR from https://github.com/tc39/proposal-pipeline-operator#build-tools. I'm not able to access the tarball mentioned here #38305 (comment) and I'm just wondering if it's been removed intentionally.

At least I haven't removed the tarball. I wonder if those have limited lifetimes. Any ideas @orta?

@orta
Copy link
Member

@orta orta commented May 15, 2020

Yep, looks like they timeout - we're having a chat about what to do in the long term (normally this is not a problem for us because they are more active PRs), but for now I'll give it another 10-12 days.

@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

@typescript-bot typescript-bot commented May 15, 2020

Heya @orta, I've started to run the tarball bundle task on this PR at 19fd8b1. You can monitor the build here.

@typescript-bot
Copy link
Collaborator

@typescript-bot typescript-bot commented May 15, 2020

Hey @orta, I've packed this into an installable tgz. You can install it for testing by referencing it in your package.json like so:

{
    "devDependencies": {
        "typescript": "https://typescript.visualstudio.com/cf7ac146-d525-443c-b23c-0d58337efebc/_apis/build/builds/74014/artifacts?artifactName=tgz&fileId=53B4D99407A93B4AF6FAEF8437B02126A8C7FEC62F1C3307DAA098F75D2CE5D602&fileName=/typescript-4.0.0-insiders.20200515.tgz"
    }
}

and then running npm install.


There is also a playground for this build.

@mikearnaldi
Copy link

@mikearnaldi mikearnaldi commented May 29, 2020

Nice! I have wondered myself how pipeline operators would play with typescript, especially how the types would flow. You could have went the way of only parsing and type checking, and letting e.g. babel do translation, but since you have also done translation this is really easy to try out.

I did test it out and ran in to a type inference limitation which I hope can be fixed or it will be hard to use with complex types:

const ns = [1, 2, 3, 4, 5];

function map<T, R>(f: (t: T) => R) {
    return (ts: T[]): R[] => ts.map(f);
}

const result = ns |> map(x => x * x);
index.ts:8:16 - error TS2571: Object is of type 'unknown'.

8 ns |> map(x => x * x);

Here T must be inferred from the type of ns.

Running in the same issue, without this sort of inference the usage of the operator is very limited, not sure if this can help but currently the only place I sow proper inference with a pipe implementation is this (not the operator a simple pipe function):
https://github.com/gcanti/fp-ts/blob/master/src/pipeable.ts#L97

@xixixao
Copy link

@xixixao xixixao commented Jun 16, 2020

This is great, I might take a stab at adding the Hack-style proposal (see tc39/proposal-pipeline-operator#167 (comment) - it's the Smart proposal without the minimal proposal) to Flow.

@stken2050
Copy link

@stken2050 stken2050 commented Jun 19, 2020

One of the greatest updates.
Just in case, here's the working:

 "devDependencies": {
    "@types/node": "*",
    "typescript": "https://typescript.visualstudio.com/cf7ac146-d525-443c-b23c-0d58337efebc/_apis/build/builds/74014/artifacts?artifactName=tgz&fileId=53B4D99407A93B4AF6FAEF8437B02126A8C7FEC62F1C3307DAA098F75D2CE5D602&fileName=/typescript-4.0.0-insiders.20200515.tgz"
  }
@sgioia9
Copy link

@sgioia9 sgioia9 commented Jul 14, 2020

YESSSS

@chenglou
Copy link

@chenglou chenglou commented Jul 14, 2020

Funny that this is coming back full circle. I've made a comment a while ago regarding the type inference/tooling problem with the pipe, and how to solve it: tc39/proposal-pipeline-operator#143 (comment)

@jamiebuilds jamiebuilds mentioned this pull request Jul 14, 2020
@Jopie64
Copy link

@Jopie64 Jopie64 commented Jul 15, 2020

About that backward type inference problem:

In RxJS, TypeScript works just fine for a situation like this:

const int$ = of('hello').pipe(map(x => x.length));
// type of int$ is inferred to be Observable<number>

This would be equivalent to:

const int$ = of('hello') |> map(x => x.length);

Why would it be impossible to infer int$ in the latter case while it is inferrable in the former? In both cases map is something of type:

type Map = <T, U>(f: T => U) => Observable<T> => Observable<U>

And hence the receiver is the last curried argument. So it feels like it should be inferrable in both cases..?

@benlesh
Copy link

@benlesh benlesh commented Jul 17, 2020

FYI: It appears the type inference isn't working that great for more advanced cases like RxJS see this playground example

Copied here for convenience:

interface Subscription {
  unsubscribe(): void;
}

interface Observer<T> {
  next(value: T): void;
  error(err: any): void;
  complete(): void;
}

interface Observable<T> {
  subscribe(observer: Partial<Observer<T>>): Subscription;
}

type Op<T, R> = (source: Observable<T>) => Observable<R>;

declare function map<T, R>(fn: (value: T, index: number) => R): Op<T, R>;

declare function filter<T>(fn: (value: T, index: number) => boolean): Op<T, T>;

declare const source: Observable<number>;

const result = source
  |> map(x => x + x) // <-- x is `unknown`
  |> map(x => x + '!!!') // <-- x is `unknown`
  |> filter(x => x.length < 20); // <-- x is `unknown`

This is a real shame, because it would be a killer language feature for libraries like RxJS.

@lozandier
Copy link

@lozandier lozandier commented Jul 22, 2020

@benlesh @markusjohnsson Seems to be a pattern of type inference needing to be added for its use w/ generics; generic use cases don't seem to be covered yet in cases/conformance nor baselines/reference yet by @Pokute.

@stken2050
Copy link

@stken2050 stken2050 commented Aug 8, 2020

@typescript-bot pack this

1 similar comment
@orta
Copy link
Member

@orta orta commented Aug 17, 2020

@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

@typescript-bot typescript-bot commented Aug 17, 2020

Heya @orta, I've started to run the tarball bundle task on this PR at 19fd8b1. You can monitor the build here.

@kiprasmel
Copy link

@kiprasmel kiprasmel commented Aug 18, 2020

Testing features like this out in a project and having to manually update the packed bundle because it expires in about 10 days is very annoying - could we get a feature to tell typescript bot to pack things indefinitely? Thanks!

/cc @orta

@kasperpeulen
Copy link

@kasperpeulen kasperpeulen commented Sep 29, 2020

Actually, this PR convinces me that the pipeline operator in its current form is not a good match for typescript.
The pipeline operator feels the most natural if we live in a js world with curried, data last, functions.
Of course, Haskell and other FP languages have moved to this pattern for similar reasons. It makes function much easier composable.

However:

  1. This is not the current js world where we live in. I think any javascript programmer with no FP background, will use the data as the first argument. This is the standard for most JS programmers, codebases and I also think still the standard in most open source APIs. Do we really want to change this whole paradigm?
  2. Curried functions are quite a bit uglier in js, especially if you want to use function, function* or async function.
  3. If you are only going to use the curried function by itself, it looks more confusing. For example, what is the output of this function: concat([1])([2, 3])? [1,2,3] or [2,3,1]
  4. TS can not really infer curried, data last, functions well. This has nothing to do with this PR, but more in general that TS infers from left to right, (data first), and not right to left (data last).

For example, this also get not inferred well:

interface Observable<T> {}
type Op<T, R> = (source: Observable<T>) => Observable<R>;
declare function map<T, R>(fn: (value: T, index: number) => R): Op<T, R>;
declare function filter<T>(fn: (value: T, index: number) => boolean): Op<T, T>;
declare const source: Observable<number>;
const result = filter(x => x.length < 20)(map(x => x + '!!!')(map(x => x + x)(source)));

Libraries like RxJS partially has solved this problem, by having a sick pipe method, that basically allows curried functions to be inferred left to right again. For example, it could be typed like this:

export function pipe<X1, X2, X3, R>(
  value: X1,
  f1: (x: X1) => X2,
  f2: (x: X2) => X3,
  f3: (x: X3) => R,
): R;

const result = pipe(
  source,
  map(x => x + x),
  map(x => x + '!!!'),
  filter(x => x.length < 20),
);

But if typescript can not infer curried functions well using the pipeline "natively", TS users will still prefer a custom pipe method above the js operator.

It sounds like me, that we may need to reach the same conclusion as rescript did (used to be called reasonml), and move to a pipeline operator that is optimized to work with data first functions:
https://www.javierchavarri.com/data-first-and-data-last-a-comparison/

Elixir also uses a pipeline operator that works on data first functions

@lozandier
Copy link

@lozandier lozandier commented Sep 29, 2020

@kasperpeulen Your mention of RxJS in your last comment is kind of confusing. The main contributors of RxJS have also campaigned for this version pipeline operator for some time; the version of pipeline operator they've supported that this PR is related to is in fact a drop-in replacement for their implementation.

A good example of that can be found here: https://reactive.how/rxjs/explorer

Accordingly it can be inferred just fine by TypeScript; it been pointed out by people like @benlesh that this current attempt of realizing that version is not acting as expected well within the capabilities of TypeScript today.

Similarly, pipeline operator is also a drop-in replacement for the equivalent in lodash, ramda, & other popular functional programming libraries that also have significant traction in the JS community overall. The way this variant of the pipeline operator works also aligns with how POSIX-compliant unix shells work & so on. Overall, how this pipeline operator behaves is not an uncommon way data pipelining works to most developers..

Accordingly, I respectfully disagree we need to prioritize an alternate pipeline operator that "works on data first functions" that isn't as idiomatic or have been as historically leveraged by functional programmers; not opposed to that being pursued as a separate initiative/standard proposed nonetheless.

@kasperpeulen
Copy link

@kasperpeulen kasperpeulen commented Sep 29, 2020

@lozandier My point is that the curried function of Rx.js only infer in combination with the pipe method. They don't infer if you would use the curried function directly. For example try this:

import { from } from "rxjs";
import { map, filter } from "rxjs/operators";

const result = filter((x) => x.length < 20)(map((x) => x + "!!!")(map((x) => x + x)(from("bla"))));

result.subscribe(console.log);

x is inferred as unknown in all callbacks

That is I think the reason why the pipeline operator implemented in this PR, can not infer them.
It could be, that we can change the pipeline operator implementation in TS so that this will get inferred as good as the .pipe method of RxJS.
However, isn't a bit silly that we are about to change the whole paradigm of how to write a function in javascript, while we can only leverage those functions using a pipeline operator, and not be able to call those functions directly?

So my point is that I don't think that curried, data last, functions are on itself a good fit for typescript, but that there are other ways to compose functions. The way elixir and rescript tackle function composition using a data-first pipeline is more in line with how functions in javascript/typescript are written today. In that way, you can both call those functions directly, for example:

const result = map(from("bla"), (x) => x + x);

But also using a pipeline:

const result = from("bla") |> map((x) => x + x);

In other words, you have multiple ways to call the same kind of function.
Especially with binary function such as power(n, exponent), this is often an advantage.

@lozandier
Copy link

@lozandier lozandier commented Sep 29, 2020

@kasperpeulen I’m not sure what you mean by “change the paradigm of how functions are written in JS” when partial application capabilities & the pipeline operator enable the use of existing functions that JS developers have written just fine.

This is particularly true of referential transparent functions that have been augmented by the equivalent of both a variety of FP libraries provide FP devs today while they wait for these two proposals to be supported by the language properly usually  through ramda, lodash, or from scratch via call/apply.

These functions are extremely common, particularly when writing optimally testable JS anyway; I’m not sure what functions would have to be majorly re-written for use w/ the pipeline operator when combined w/ the partial application proposal.

The pipeline operator on its own elegantly supports single arg (unary) referential transparent functions while combined w/ the partial application proposal enables it to handle all typical functions JS write today just fine.

Without the latter being available to the language for a long time is where I can see your concern having merit forcing point-free programming all the time when using the pipeline operator; that said, all steps point to the pipeline operator & partial application capabilities being combined or ratified simultaneously to alleviate your concern.

Currently the F#-style, split mix, & smart mix are proposal variants of the pipeline operator that explicitly do so.

The latter 2 are explicitly a florian's compromise of accepting some of the ideas of a recently proposed new style (Hack-style) from a perspective similar to yours being cautious of forcing point-free programming w/ the use of pipeline-operator.

Split mix & F#-style I find the most promising variants on the matter. Both are iterative approaches to enabling the minimal proposal to be what it is prior to them augmenting it further with concerns that are beyond the uses approximated today by rxjs, lodash, ramda, & other popular JS libraries

@kasperpeulen
Copy link

@kasperpeulen kasperpeulen commented Sep 29, 2020

With the partial application proposal also in, you indeed dont have to rewrite existing javascript, but it is still more elegant to use the curried variant of the same function (to avoid the ?).

I guess the js ecosystem will move more to curried, data last functions even with the partial application syntax.

The question is, do we want that?

When data last, curried functions, when called directly as shown above, infer very poorly the generic parameters, which seems to be a core design limitation of typescript.

Or are there better solution for function composition in typescript?

@lozandier
Copy link

@lozandier lozandier commented Sep 29, 2020

@kasperpeulen While I prefer point-free programming, it's purely subjective that a curried variant of the same function is "still more elegant".

As far as the two capabilities "moving the JS ecosystem" in a particular direction, that's up for the JS community to decide; not exactly sure who is "we" in your original comment & why their wants should outweigh the JS community.

The existence of these proposals won't force the JS community in a particular direction; these capabilities being prevalent in codebases after being ratified would merely signal & verify people wanted to go this direction and them being available natively finally more easily enabled that. The existence of the pipeline operator & partial application proposal natively unblocks barrier of entry selling & communicating longly-leveraged patterns of writing declarative, functional code involving left-to-right composition, clearer mix-in patterns, & so on in environments where every byte counts compared to importing & writing ad hoc code to approximate their behavior with limitations a native solution doesn't.

With these capabilities in the language soon, it will be marketed that there are "new" ways to write declarative, functional code in JS as a result. That's fine; similar to how futures (Promises) in JS was similarly marketed (though that went a little extreme when it was shoehorned in places it's not a good fit for like async HTTP requests, but I digress).

As far as limitations w/ TypeScript's type system: TypeScript core maintainers are a brilliant bunch; at the end of the day JS doesn't revolve around TypeScript; as a superset of JS they'll refactor TypeScript as needed to continue to be that in good faith.

It can be argued w/ the pipeline operator types flow much easier, not the other way around.

@kasperpeulen
Copy link

@kasperpeulen kasperpeulen commented Sep 29, 2020

With these capabilities in the language soon, it will be marketed that there are "new" ways to write declarative, functional code in JS as a result. That's fine.

That is fine, if you believe this is the best way to write declarative functional code in JS using curried data last functions. That is what I doubt.

Your argument are not really argumenting why data last curried functions with say a F# pipeline are a better fit for javascript, than say a data first “normal” function with an elixir like pipeline.

This article goes in detail about the pros and cons about those approaches:
https://www.javierchavarri.com/data-first-and-data-last-a-comparison/

Probably better to move this discussion to the pipeline repo though.

If we indeed can fix:

  • all typescript inference issues for this PR
  • curried functions inference in general
  • the autocompletion issues mentioned in the blog post
  • the confusing error message that are even prevelant in many FP languages according to the blog (as the “parameters” are inferred earlier than the “data”)

than it wouldnt matter that much to me which variant of the pipeline proposal is chosen.

But if those issues are too hard to fix, it may be another argument to take the elixer pipeline proposal more serious.

@stken2050
Copy link

@stken2050 stken2050 commented Oct 1, 2020

Heya @orta, I've started to run the tarball bundle task on this PR at 19fd8b1. You can monitor the build here.

@orta The pack build seems failed?
Build not found.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet

You can’t perform that action at this time.