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

Add a left-to-right composition function (flow?) and re-work or remove compose #5

Closed
Avaq opened this issue Oct 3, 2021 · 13 comments
Closed
Labels
enhancement New feature or request question Further information is requested

Comments

@Avaq
Copy link

Avaq commented Oct 3, 2021

Introduction

I think the variadic compose function as implemented in Ramda and proposed here occupies a weird space in between two problems, where it doesn't fully solve either. My suggestion is to split the function into two, to address each problem directly.

Problem 1: Point-free code linearization / function creation

For example, all statements below are equivalent:

x => JSON.stringify(Math.round(Math.sin(x)));

x => Function.pipe(x, Math.sin, Math.round, JSON.stringify);

Function.compose(JSON.stringify, Math.round, Math.sin);

The Function.pipe helper allowed the code to be linearized. On top of this, the Function.compose helper allowed the point (x) to be removed. However, the user was asked to reverse the order of their functions for no apparent benefit. Why? Because this compose function carries legacy from solving "problem 2", which we'll get to.

From what I can tell (please, someone correct me if I'm wrong), the sole reason for the compose function being right-to-left is because the binary function composition combinator does it that way. Doing right-to-left composition of a pair of unary functions makes a lot of sense (I'll expand on that), but doing right-to-left composition in a setting where any number of functions can be given is nothing but impractical.

In conclusion, the current compose helper is not as ergonomic as it could be for solving the point-free code linearization / function creation problem.

Problem 2: Function composition

The curried composition function on two unary functions from combinatory logic, with signature (b -> c) -> (a -> b) -> a -> c, can be very useful outside of the context of code linearization. In this scenario, doing the composition right-to-left makes sense because a partially applied composition is a useful tool.

In the following example, I use compose to define a binary function in terms of another binary function:

const add = a => b => a + b;

// Don't forget to read bottom-to-top
const addThenSqrt = Function.compose(
  Function.compose.bind(Function, Math.sqrt)
  add
);

addThenSqrt (140) (4) // 12, because `Math.sqrt (140 + 4)`

Here I've partially applied compose to make it so it takes one more function to compose. Then when the outer compose is supplied its argument, the partially applied add function is baked into the composition with Math.sqrt. This is just one example of how partial application of compose can serve some kind of purpose. This is the space where the curried unary compose function shines.

Using the variadic compose function for this was uncomfortable, because, while its argument order (right-to-left) suggests that this is the purpose it serves, I had to use .bind to partially apply it.

In conclusion, the current compose helper is not as ergonomic as it could be for solving the function composition problem.

Solution: Addressing these two problems separately

I'm proposing addressing each of the problems that are now partially addressed by the variadic compose function as two separate problems. These problems can both be addressed more directly to provide a better developer experience.

Problem Solution
Point-free code linearization / function creation

Instead of the variadic compose with reversed function order, I would propose solving this problem more directly with a composition function that applies functions left to right. In fp-ts this function was dubbed flow. But in Ramda and Sanctuary, this function is the one called pipe.

The code example under Problem 1 would now be:

Function.flow(Math.sin, Math.round, JSON.stringify);

This is both easier to read, and easier to refactor from and to x => Function.pipe(x, Math.sin, Math.round, JSON.stringify)

Function composition

Instead of the variadic compose function, this problem is best addressed by a unary curried function composition combinator, exactly as implemented (named B) in my Gist.

The code example under Problem 2 would now be:

const addThenSqrt = Function.flow(
  add
  Function.compose(Math.sqrt)
);

In my ideal world, both of these functions would make it into this proposal. But I feel less strongly about including the B combinator, than I feel about fixing (the argument order in) compose to better address problem 1. I'd also expect more pushback against the B combinator.

@mmkal
Copy link

mmkal commented Oct 3, 2021

Note that this is called flow in lodash too: https://lodash.com/docs/4.17.15#flow

I think it would be used much more than compose. For example, you could implement a lambda function handler for AWS kinesis event record:

export const recordParser = Function.flow(
  base64Decode,
  JSON.parse,
  SomeSchema.validate,
  ev => sendMessage('hello ' + ev.user.name)
)

Reading left-to-right makes it intuitive vs compose, and the naming is more human and less mathematical. Data "flows" into function 1, then function 2, etc.

@Avaq
Copy link
Author

Avaq commented Oct 3, 2021

Thanks @mmkal. I've added _.flow to the table I created for #6. See it here.

@js-choi js-choi added the question Further information is requested label Oct 3, 2021
@Avaq
Copy link
Author

Avaq commented Oct 3, 2021

To bring it full circle (and hopefully minimize any unclarity before I start my work week), I'll rephrase my suggestion in terms of the the aforementioned "needs" table. I am arguing the following.

That, based on my own experience, the need to define a function in terms of a series of function calls in left-to-right order, and maybe even the need to (partially) compose two unary functions in right-to-left order are both greater than the need to define a function in terms of a series of function calls in right-to-left order. Schematically:

1 to define a function in terms of a series of function calls in left-to-right order

>

3 to define a function in terms of a series of function calls in right-to-left order
2 to (partially) compose two unary functions in right-to-left order

The current proposal addresses 3 foremost (via Function.compose).

(And I think that's because this version of compose has evolved as an attempt to make the B combinator that addresses 2 useful for 1 as well, but got stuck in the middle where it addresses the (less severe) need of 3 without fully addressing 1 nor 2.)

One statistic which might support part of this argument is that the lodash.flow package (which addresses 1) has almost 14 times as many downloads as the lodash.flowright package, which addresses 3.

@js-choi
Copy link
Collaborator

js-choi commented Oct 4, 2021

I appreciate your points. @rbuckton, @tabatkins, @mAAdhaTTah, and I have been talking about whether to include a function-composition helper function and whether to make it flow right-to-left (RTL) or left-to-right (LTR). In mathematics and in many functional-programming languages, the composition operation flows RTL. So some of us had anticipated that FP users would be unhappy if Function.compose flowed LTR. (@rbuckton has also pointed out that decorators are applied in RTL order.)

But you have a good point that Lodash’s popular flow function goes LTR, and that lodash.flow is much more popular than the (rather confusingly named) lodash.flowRight.

So I am inclined to changing the current RTL Function.compose to a LTR Function.flow. (I expect to get some pushback from TC39 unless we are able to convincingly demonstrate that Function.flow(a, b, c) is worth standardizing in addition to Function.pipe(x, a, b, c), a similar problem to what identity and constant face.)

As for your curried partial-composition function idea…unfortunately, I am reluctant to include any curried function in this proposal. Programming styles involving currying have been quite controversial in TC39; see tc39/proposal-pipeline-operator#221. I wish to avoid such controversy and increase the probability that this initial proposal be accepted at all. So I’m going to avoid including anything that TC39 would perceive as benefiting or encouraging currying-heavy styles. Sorry if that’s disappointing, and hopefully it’s understandable.

@js-choi js-choi added the enhancement New feature or request label Oct 4, 2021
@tabatkins
Copy link

(Given that built-ins are relatively cheap, especially compared to syntax, I don't think we'll have a particular problem with flow vs pipe competing. The mechanics are similar, but you can't meaningfully use one in place of the other.)

@samhh
Copy link

samhh commented Oct 5, 2021

Some more anecdata to consider.

The fp-ts ecosystem uses flow, which is LTR, however I'm unsure if this is because of an actual preference or due to type system constraints (either now or in the past).

The PureScript ecosystem, very much derived from Haskell, has both >>> and <<< operators, where my impression is that the community leans towards LTR.

Haskell likewise has >>> however . remains very much the norm.


I can only speak for myself, but as someone who wants to see FP thrive in JS I'd be happy either way (in stark contrast to my feelings on the pipe operator...). Whatever makes it most likely to get through committee.

@Avaq
Copy link
Author

Avaq commented Oct 5, 2021

I am reluctant to include any curried function in this proposal.

I figured, and understand the reasoning. As I mentioned, I'm happy for that part of my suggestion to be ignored. In this case, I think this issue and #9 are equivalent.

@Jopie64
Copy link

Jopie64 commented Oct 5, 2021

In this case, I think this issue and #9 are equivalent.

Yes they are indeed. I strangly didn't find this issue before I created #9. So I think it can be closed as it is a duplicate :)

@Avaq Avaq changed the title Splitting the compose function into two to better address the problems it solves Add a left-to-right composition function (flow?) and re-work or remove compose Oct 5, 2021
@Avaq
Copy link
Author

Avaq commented Oct 5, 2021

I strangly didn't find this issue before I created #9.

I came into it from a bit of an odd angle. I renamed the issue to hopefully aid with discoverability.

@js-choi js-choi closed this as completed in d0b704b Oct 5, 2021
@theqp theqp mentioned this issue Oct 9, 2021
@shuckster
Copy link

I've noticed that it is not necessarily the case that choosing LTR pipe or RTL compose results in code that reads in a "natural order". I pulled-out a couple of examples that demonstrate this:

Transducers

The composition below reads naturally from top-to-bottom (or LTR) and makes sense when compared with its corresponding output ([3, 5, 7]), despite being assembled with an RTL compose:

const calc = arr => arr.reduce(
  Function.compose(
    onlyIntegers,
    double,
    addOne
  )(append),
  []
)

calc([1, 2, undefined, 3])
// result = [ 3, 5, 7 ]

Swapping double and addOne in this composition results in [ 4, 6, 8 ].

The rest of the code for this use-case if it's useful:

const map = (xform) => (build) => (acc, x) => build(acc, xform(x))
const filter = (pred) => (build) => (acc, x) => pred(x) ? build(acc, x) : acc
const append = (acc, x) => [...acc, x]

const onlyIntegers = filter(Number.isInteger)
const double = map((x) => x * 2)
const addOne = map((x) => x + 1)

HTML/VDOM

If you find yourself using compose to build an HTML/VDOM, you might notice that this also results in a nice top-to-bottom reading order that fits with how HTML elements appear in the DOM:

const renderBoard = Function.compose(
  (tbody) => `
    <table class="board">
    <tbody>
      ${tbody.join('')}
    </tbody>
    </table>
  `,
  (rows) => rows.map((col) => `<tr>${col.join('')}</tr>`),
  (board) =>
    board.map((row, py) =>
      row.map((col, px) => `
        <td
          data-action="grid-click"
          data-xy="${px},${py}"
        >
          ${cellAppearance[col]}
        </td>
    `))
)

As you can see, table is first, followed by tr then td.

Not sure how convincing these are, but hopefully they're enough to show that it's at least possible to have RTL compose give a more "natural" reading order for certain use-cases.

This is not an argument against flow(...fns) by the way, which I'd welcome as a compliment to pipe(x, ...fns).

@js-choi
Copy link
Collaborator

js-choi commented Oct 14, 2021

Transducers are funny because—due to their funny wrapping behavior—they seemingly reverse flow direction. Clojure happened to be one of my first FP languages (I truly got into JavaScript via ClojureScript), and, when Rich Hickey introduced transducers, my use of comp greatly increased. Having said that, I can’t really think of any other time when the directionality of comp was important in Clojure either.

I’m reluctant to include both a RTL and LTR composition function in this initial proposal because several TC39 representatives would probably push back at including both without clear use cases for both. (In fact, I still fear them pushing back at both flow and pipe being included, but I think the problem there is less severe.) Hopefully that makes sense.

@shuckster
Copy link

Thanks for the reply and work on this @js-choi , the explanation makes sense.

@mAAdhaTTah
Copy link

The other thing is the transducer protocol is implemented in userland, so unless we were to standardize transducers as a whole, you can't use a typical curried map function (e.g. arr => pred => arr.map(pred)) to compose up a transducer, which makes compose as a built-in a lot less useful.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement New feature or request question Further information is requested
Projects
None yet
Development

No branches or pull requests

8 participants