Skip to content
This repository has been archived by the owner on Jan 26, 2022. It is now read-only.

Concerns regarding type inference and data-last functions #18

Closed
OliverJAsh opened this issue Sep 3, 2021 · 4 comments
Closed

Concerns regarding type inference and data-last functions #18

OliverJAsh opened this issue Sep 3, 2021 · 4 comments

Comments

@OliverJAsh
Copy link

OliverJAsh commented Sep 3, 2021

Unlike the F#-style, the Hack-style is incompatible with the majority of pipeable APIs that already exist—data-last functions in widely used libraries such as RxJS and fp-ts.

In TypeScript, type inference works left to right. In this example the type of the user argument can't be inferred because the data comes afterwards (i.e. map and filter are data-last functions).

userOption
  |> map(user => user.name)(%)
  |> filter(user => user.age >= 18)(%)

This is not a problem with the pipe function (currently used in RxJS/fp-ts) because of contextual type inference, so this compiles:

pipe(
  userOption,
  map(user => user.name),
  filter(user => user.age >= 18),
)

To make type inference work, pipeable APIs will probably need to be rewritten so they are data-first instead of data-last:

userOption
  |> map(%)(user => user.name)
  |> filter(%)(user => user.age >= 18)

However this makes partial application more awkward. With data-last functions we can partially apply them so the data is provided later on:

const add1ToList = List.map(add1)
add1ToList([1,2,3]) // [2,3,4]

Partial application with data-first is much more verbose because we need to include a function signature:

const add1ToList = (xs: number[]) => List.map(xs)(add1)

add1ToList([1,2,3]) // [2,3,4]

Given that the data-last approach makes sense regardless of pipelines (e.g. for partial application), and given that the data-last approach has been widely adopted in popular libraries such as RxJS and fp-ts, I think it's important that we consider how this pipeline proposal can provide interoperability. After all, these FP libraries represent the majority of the pipe function usage that the pipeline operator is supposed to replace.

The |>> operator ("tacit unary function application") does not suffer from these problems—it works with data-last functions. For this reason I would like to suggest that we bring the |>> operator forward so it is part of the initial specification. Example usage with data-last functions:

userOption
  |>> map(user => user.name)
  |>> filter(user => user.age >= 18)

My personal preference would be for the F#-style operator but given the Hack-style operator seems to be progressing I hope we can bring the |>> operator into the specification sooner rather than later. Failing this I worry that many libraries will continue to use the pipe function.

@js-choi
Copy link
Collaborator

js-choi commented Sep 3, 2021

Thanks for the well-written issue! Your points are good and well taken.

There’s two things to address here.

TypeScript unification’s limitations

One is a general problem with TypeScript not unifying types in map(user => user.name)(userOption). Even that version without pipes raises a type error; here’s an isolated example (thanks @devsnek). My understanding is that this is due how type inference works left to right. See microsoft/TypeScript#15680…which I see you had opened yourself, ah. Also microsoft/TypeScript#22081, microsoft/TypeScript#25826, microsoft/TypeScript#29904, microsoft/TypeScript#30134.

It’s interesting how pipe functions allow people using data-last / curried functions to avoid this problem, although it also seems a little brittle (as your own TypeScript issues, linked above, make clear).

I’m hopeful that TypeScript will improve its type unification so that it retains free types (microsoft/TypeScript#30134), but of course this means that we can’t do map(user => user.name)(userOption) today, and we therefore can’t do userOption |> map(user => user.name)(^) 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.

(CC @tabatkins: This is may be a problem with our “we can just tack on (^) to the end of unary function calls” argument that Hack pipes and F# pipes + await are functionally equivalent…even if the problem is simply due to an artifact of TypeScript’s limited left-to-right type unification, and which might be obviated by future changes to TypeScript in microsoft/TypeScript#30134.)

Whether to reintroduce split mix (|>>) and whether to do it now or later

The second thing to address is whether and when we should try to reintroduce what we have called “split mix”: a separate pipe operator |>> for tacit unary function calls. This would allow people using data-last / curried-function styles to omit (^).

I think @tabatkins has said before that also having |>> might be a hard sell to the Committee, because from JavaScript’s perspective x |> f(^) is equivalent to x |>> f under this paradigm (TypeScript’s limited type unification notwithstanding). But |>> is something that I would be happy to work on…

…It’s just that the Committee may well balk if we do too much at once. It’s already having a hard time swallowing any pipe operator. It generally prefers to do things piecemeal, while keeping forward compatibility with future extensions. And Hack pipes are forward compatible with |>>.

My inclination (@tabatkins, @ljharb, @mAAdhaTTah, others are free to insert their own opinions) therefore is to keep this first pipe proposal focused on Hack |>, and to leave |>> to a later proposal (if |> ever even gets accepted, as we hope).

(Aside: I would like to push back at “these FP libraries represent the majority of the pipe function usage that the pipeline operator is supposed to replace”. Although unary function calls are a significant use case, from this proposal’s perspective, unary function calls are not the most common use case. The pipe operator is not supposed to replace only unary function calls; it is supposed to replace deeply nested expressions in general, which occur in all APIs. In the worst case, people using Ramda/RxJS/etc. can keep using pipe functions for curried-function calls…while still finding Hack pipes useful for interoperation with other APIs’ data-first function calls, function calls with trailing option objects, Web APIs, and so on.)

Anyways, I’m personally enthusiastic about |>> for tacit unary function calls and, in fact, I’d be eager to eventually write a proposal for |>> myself. (Clojure is one of my homes, and Clojure has a ton of threading macros, so I’d be used to having two pipe operators.) It’s just that the Committee may recoil from too big a bolus of syntax at once…It’s already been hesitant at even one pipe operator. If there’s enough of a use case for |>> (e.g., working with TypeScript’s limited type unification before microsoft/TypeScript#30134 lands), then it can fight for it on its own merits. I would be happy to try to make that fight for it, later.

@tabatkins
Copy link
Collaborator

(CC @tabatkins: This is may be a problem with our “we can just tack on (^) to the end of unary function calls” argument that Hack pipes and F# pipes + await are functionally equivalent…even if the problem is simply due to an artifact of TypeScript’s limited left-to-right type unification, and which might be obviated by future changes to TypeScript in microsoft/TypeScript#30134.)

I don't understand why val |> map(user=>user.name)(^) in Hack-style is problematic but val |> map(user=>user.name) in F#-style isn't, given that they are literally identical in both execution and semantics. I presume it's just an oddity of fragile/limited unification semantics?

I think @tabatkins has said before that also having |>> might be a hard sell to the Committee, because from JavaScript’s perspective x |> f(^) is equivalent to x |>> f under this paradigm (TypeScript’s limited type unification notwithstanding).

Yes, adding it to this version of the proposal would almost certainly sink things. I'm happy to explore more syntax after the dust has settled on the base proposal and we see how it works in practice and how the ecosystem adapts to it.

@tabatkins
Copy link
Collaborator

(Aside: I would like to push back at “these FP libraries represent the majority of the pipe function usage that the pipeline operator is supposed to replace”. Although unary function calls are a significant use case, from this proposal’s perspective, unary function calls are not the most common use case. The pipe operator is not supposed to replace only unary function calls; it is supposed to replace deeply nested expressions in general, which are useful for all APIs. In the worst case, people using Ramda/RxJS/etc. can keep using pipe functions for curried-function calls…while still finding Hack pipes useful for interoperation with other APIs’ data-first function calls, function calls with trailing option objects, Web APIs, and so on.)

Yes, the vast majority of JS is not written in curried, data-last style; that's limited to a relatively small subset of users of certain libraries (popular libraries, to be sure, but nowhere near universal). Most JS (importantly, any JS written using web platform APIs) is written with non-curried data-first (or at least data-early; argument order isn't always well thought-out...) functions, and the nesting problem is just as prevalent there. (In particular, variadic functions, and final optional argument-bag functions, are very common on the web platform and in JS as a whole, and they're wholly unsuitable for auto-currying; that style requires a certain discipline and design style that JS isn't particularly built to encourage or enforce.)

Making sure that the pipe operator addresses the large majority of code as well as it possibly can is more important than optimizing it for the curried-data-last paradigm (at least in the first iteration of the syntax), particularly since that paradigm already has a working and syntax-light library solution in the form of pipe(); the F#-style pipe operator is only a few characters of savings over pipe() anyway.

@js-choi
Copy link
Collaborator

js-choi commented Sep 9, 2021

In anticipation of the archival of this repository and switching back to https://github.com/tc39/proposal-pipeline-operator, I’ll close this issue. Thanks again for raising it; hopefully microsoft/TypeScript#30134 gets resolved soon.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants