Skip to content

Prior difficulty persuading TC39 about F# pipes and PFA #221

Open
@js-choi

Description

@js-choi
  • F#-style pipes, partial function application (PFA), unary function composition (Function.pipe / flow), and currying all are techniques of a paradigm called tacit / point-free programming.
    • Tacit / point-free programming is associated with functional programming on unary functions that accommodate extra arguments by currying.
    • JavaScript is an n-ary functional programming language that lacks built-in automatic currying.
    • Tacit / point-free programming is not equivalent to functional programming.
    • Functional programming can be either tacit / point free or pointful.
    • Examples of pointful functional programming include all function/lambda literals (the vast majority), Haskell’s functor/monad do-notation, and F#’s computation expressions.
  • Many people in the Committee remain strongly against encouraging widespread tacit / point-free programming in JavaScript in syntax.
    • Many of the pipe champions are not these people and are fans of tacit / point-free programming in JavaScript. However, we have to work with the ones who are not.
    • The strongest opposition to tacit / point-free programming comes from the JavaScript engine implementors, concerning performance.
      • More widespread tacit / point-free programming, especially widespread curried functions, would encourage extensive allocation and garbage collection of onetime-use unary functions.
      • The engine vendors are concerned that such changes would be difficult and complex for them to optimize for relatively little gain from their perspective.
        • This includes concerns about significantly increasing the allocation/deallocation of many onetime-use functions closures in program hot paths.
        • The engine implementers have said that they would have much difficulty with optimizing these closures by inlining.
        • This is especially difficult when the closures were created by a named function and must appear in error stack traces.
        • For example, an engine might easily inline [1, 2, 3].map(x => x + 1).
        • But given a curried add function, [1, 2, 3].map(add(1)) may be difficult to inline—especially if add can throw an error.
        • Error stack traces are observable by the developer, and any function that may throw an error must be preserved for the error stack.
      • These performance concerns are obviated if every step of a pipeline is a cached variable.
      • This difficulty is not present in programming languages that use function-based pipes.
        • F# & Haskell are both auto-curried, compiled, and statically typed.
          • These three traits provide a compiler with a significant amount of ahead-of-time information that can be used to optimize the program around their unary-function-based pipes.
          • JavaScript does not have these three traits and cannot optimize unary-function-based pipes nearly as well. If JavaScript adopted an F# pipe, it would be, as far as we are aware, the first language that would require the VM to allocate a new closure to evaluate the RHS of the pipe in the point-free case.
          • Notably, even F#’s guidance cautions that tacit programming should be used carefully and judiciously in F#, and it should not be used in public F# APIs.
        • Elixir is dynamically typed but inserts the pipeline value as the first argument, which obviates the requirement to allocate a new closure.
          • However, Elixir-style pipes are also limited in which parameter into which can insert—the entire language must choose to pipe through first or last parameters.
          • Elixir-style pipes are ironically confusable with calls to higher-order functions: x |> f(y) being f(x, y) or f(y, x) and not f(x)(y).
      • Furthermore, many “pipe” functions as implemented by many JavaScript functional-programming libraries like Ramda and Sanctuary avoid this concern by actually being composition functions.
        • For example, pipe(a(1), b(2), c(3)) returns a new function, which can then be called multiple times without allocating new closures, because they were allocated up front at the point the new function is created.
        • While x => x |> a(1) |> b(2) |> c(3) with F# pipes looks functionally similar, the closures need to be allocated every time that function is invoked.
        • This is avoidable if you extract a(1) or wrap it in an arrow function like |> x => a(x, 1), but now this is more verbose than Hack-style / topic-style pipes.
      • This difficulty is unlikely to change just because hardware may get faster.
        • The engine vendors care about low-end devices and they care about performance regressions.
      • The engine implementers have stressed repeatedly to us that people generally overestimate how much optimization the engines can do, including in this case.
      • This is the most serious barrier. Just like with HTML in WHATWG, the implementors have the most power.
        • The engine vendors will simply not implement what they do not agree should be implemented, and we cannot force them to.
        • The most power—perhaps rightfully—lies in the standard implementors, because they are the ones doing the work to make it shippable and real. Likewise, they are the ones who get the blame when it goes wrong.
        • If a language change makes the average website’s performance worse, then the browsers and engines will be blamed for it.
        • The browser vendors have a strong interest in being conservative with the language to avoid performance regression. The recent JSSugar proposal from one delegate from Chromium/V8 lays out some of this explicitly.
        • We cannot force browser vendors or engine implementors to do things that they disagree with or don’t want to do.
        • Such attempted compulsion to implement is why the WHATWG–W3C schism over HTML occurred in the first place, as Ian Hixie saw from the frontlines.
        • Oftentimes, the desires of many standards users (the greater developer community) have concord with the desires of standards (the browser-engine implementers).
        • At other times, they clash. This is just one of those cases. (Though note that many users also have voiced support for Hack-style / topic-style pipes.)
    • Other concerns have included that tacit / point-free programming poorly fits JavaScript.
      • Tacit / point-free programming best fits unary functional programming languages that have currying, but JavaScript is an n-ary functional programming language that lacks auto-currying.
      • Some people in the Committee believe that this mismatch also makes widespread tacit / point-free programming undesirable in JavaScript.
      • Some of them also are concerned that encouraging widespread tacit / point-free programming would cause an ecosystem schism.
  • Because of this:
    • F#-style pipes and Elixir-style pipes still do not have any chance to succeed.
    • Partial function application (PFA) remains in stasis.
      • It has been proposed at least three times (2017, 2018, and 2021); see HISTORY.md.
      • Each time, it received much pushback, especially from those who are against widespread tacit / point-free programming in JavaScript.
    • My Function.pipe / flow (unary function composition) proposal also received much skepticism and has been withdrawn.
  • Thus, Hack-style / topic-style pipes (the current state of this proposal) remain the only viable path forward for standardized piping on the language level.
    • We have given up on trying to standardize language features in the tacit / point-free programming paradigm.
    • There is no way around the fundamental anti-tacit barrier, and we are not interested in re-litigating it.
    • We care most about being able to linearize deeply nested expressions into fluent pipelines with linear dataflow, without resorting to temporary variables.
    • Hack-style / topic-style pipes work fine with function-based pipelines or any other API design.
    • Hack-style / topic-style pipes are also a zero-cost abstraction that sidesteps the engines’ concerns about the difficulty of optimizing hot-path onetime-use closures.
    • Hack-style / topic-style pipes’ only downside is that they aren’t tacit / point-free.
  • As I said in the original 2021 version of this post, I will update the explainer with an FAQ with this explanation. I plan to close this documentation issue once I do so.
Old 2021 version

The explainer does not currently explain why the pipe champion group believes Hack pipes to be the best chance of TC39 agreeing to any pipe operator.
(See HISTORY.md for a lot of detailed background.)

During 2017, 2018, and 2021, whenever they were presented to TC39, F# pipes and partial function application (PFA) have run into pushback from multiple other TC39 representatives due to memory performance concerns (e.g., from Google V8’s team), syntax concerns about await, concerns about encouraging ecosystem bifurcation/forking, etc. This pushback has occurred from outside the pipe champion group. (See HISTORY.md for specific examples.)

It is the pipe champion group’s belief that any pipe operator is better than none (in order to easily linearize deeply nested expressions without resorting to named variables). Many members of the champion group believe that Hack pipes are slightly better than F# pipes, and some members of the champion group believe that F# pipes are slightly better than Hack pipes. But everyone in the champion group agrees that F# pipes have met with far too much resistance to be able to pass TC39 in the foreseeable future (or even ever—though I hope not).

To emphasize, it is likely than an attempt to switch from Hack pipes to F# pipes will result in TC39 never agreeing to any pipes at all; PFA syntax is similarly facing an uphill battle in TC39 (see HISTORY.md). I personally think this is unfortunate, and I am willing to fight again for F# pipes and PFA syntax, later—see #202 (comment). But there are quite a few representatives (including browser-engine implementers; see HISTORY.md about this again) outside of the Pipe Champion Group who are against encouraging tacit programming (and PFA syntax) in general, regardless of Hack pipes.

In other words, the explainer currently does not adequately explain the situation regarding the strong pushback that F# pipes and PFA syntax, since 2017, have run into from various TC39 representatives. We need to fix this sometime.

This issue tracks the fixing of this deficiency in the explainer (lack of discussion regarding the pipe champion group’s decision making, after the pushback that F# pipes and PFA syntax encountered in TC39 from outside the champion group). 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 it to tc39-conduct-reports@googlegroups.com). Please also try to read CONTRIBUTING.md and How to Give Helpful Feedback. Thank you!

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentation

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions