gilbert / es-papp

A proposal for adding partial application support to JavaScript.
359 stars 12 forks source link

`<|` operator instead of prototype function? #9

Open azz opened 7 years ago

azz commented 7 years ago

Idea

A partial application operator, <|, designed to work with the proposed pipeline operator, |>.

Instead of fn.papp(foo), you would write fn <| foo.

If you need to partially apply multiple arguments, just repeat the operator. Instead of fn.papp(foo, bar), you would write fn <| foo <| bar.

The operator would have a precedence higher than |>.

Comparison

import { partial } from 'somewhere';

const clamp = (lower, upper, value) =>
  Math.max(lower, Math.min(upper, value))

const things = [-0.5, 0, 0.5, 1, 1.5]

// All of the below output: [0, 0, 0.5, 1, 1]

things.map(clamp.bind(null, 0, 1))
things.map(partial(clamp, 0, 1))
things.map(_ => clamp(0, 1, _))
things.map(clamp.papp(0, 1))
things.map(clamp <| 0 <| 1)

Example

import { map, filter, reduce } from 'somewhere'

// Before pipeline or partial application
reduce(
  (a, b) => a + b,
  0,
  map(
      _ => clamp(0, 1, _), 
      filter(
          x => x >= 0.5, 
          map(Math.abs, things)
      )
  )
) // ==> 3

// With pipeline, without partials
things
  |> _ => map(Math.abs, _)
  |> _ => filter(x => x >= 0.5, _)
  |> a => map(b => clamp(0, 1, b), a)
  |> _ => reduce((a, b) => a + b, 0)

// Pipeline with Function#papp
things
  |> map.papp(Math.abs)
  |> filter.papp(x => x >= 0.5)
  |> map.papp(clamp.papp(0, 1))
  |> reduce.papp((a, b) => a + b, 0)

// Pipeline with <|
things
  |> map <| Math.abs
  |> filter <| (x => x >= 0.5)
  |> map <| (clamp <| 0 <| 1)
  |> reduce <| ((a, b) => a + b) <| 0

Pros

Cons

I imagine it is more difficult to get an operator through TC39 versus a prototype function. Feel free to shoot this down, but I thought it was worth at least discussing.

alexandradeas commented 7 years ago

The primary argument against this, as I think you pointed out in the last sentence is that papp is easy to polyfill. However introducing an operator requires transpilation.

For TC39 it would appear too cryptic and doesn't really flow from the current ECMA syntax. A better option IMO would be to push for operator overloading (and allowing us to define operators). After that's introduced it would become much easier to introduce new operators by simply defining them in a library, and creating proposals from whether the operators are adopted or not.

Jamesernator commented 7 years ago

Personally I like the idea of syntax, although I'd prefer to see a more generic syntax which can work in any position not just left:

// ? used for exposition
things
    |> map(Math.abs, ?)
    |> filter(even, ?)
    ...

// Still works if map takes arguments in opposite order
things
    |> map(?, Math.abs)
    |> filter(?, isEven)
    ...

// Or in any position actually which means no flip(func) stuff is required
things
   |> reduce(seed, ?, sum)

// Even works for things like quickly promisifying arbitrary functions:
new Promise(setTimeout(?, 1000))
amoerie commented 7 years ago

In my humble opinion, the version without partial apply is still, by far, the easiest to read and understand:

// With pipeline, without partials
things
  |> _ => map(Math.abs, _)
  |> _ => filter(x => x >= 0.5, _)
  |> a => map(b => clamp(0, 1, b), a)
  |> _ => reduce((a, b) => a + b, 0)

To avoid confusion and making the language too cryptic, I would consider it best to only go for the pipeline operator (which is well defined in other languages too) and leave the partial application out of the language itself.

There's already plenty of libraries which offer such a function (e.g. lodash#curry comes to mind) so I wonder if this really needs to be language feature at all.