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

Curry "statics" #20

Open
briancavalier opened this issue Aug 3, 2014 · 24 comments
Open

Curry "statics" #20

briancavalier opened this issue Aug 3, 2014 · 24 comments

Comments

@briancavalier
Copy link
Member

Currently, most provides both prototype and "static" versions of each combinator. The static versions should be curried to make them nicely composable. We can curry at the public API boundary (ie in the main most.js module) to avoid using curried functions internally.

@briancavalier
Copy link
Member Author

I think currying and its benefits have made enough headway in JS (for example, Ramda has become quite popular, and lodash-fp is actively developed) that it's time to do this.

@briancavalier briancavalier mentioned this issue Apr 16, 2015
4 tasks
@briancavalier
Copy link
Member Author

Another angle on this is: What value are the static functions, like most.map, providing at the moment? I would argue that the answer is "not much". So maybe we should either deprecate and remove them, going strictly with instance methods, or we should do something to make them more useful/attractive. Maybe currying + composition is that something?

@ivan-kleshnin
Copy link

In theory, I'd like to see purely functional reactive library in JS. Currying turns out to be extremely useful in practice. I use Ramda extensively and one of the main reason for it is currying.

Streaming libraries generally does not provide static counterparts for Promises. And mixed mess of methods and functions is anything but pretty. It shouldn't be hard to implement though.

import {curry, pipe} from "ramda";

let M = {
  then: curry((handler, promise) => {
    return promise.then(handler);
  }),

  catch: curry((handler, promise) => {
    return promise.catch(handler);
  }),
};

let x$ = pipe(
  M.then(x => x * 2),
  M.then(x => x * 2),
  M.then(x => x * 2),
  M.catch(err => console.error(err))
)(Promise.reject(new Error("oh no!")));

I'm also interested how you solve laziness mismatch. Rx Observables are lazy and Promises are not.
So their interaction are somehow crippled from the start. It would be great to have laziness aspect described in the docs.

@jgoux
Copy link

jgoux commented Apr 13, 2016

I'd love to have all the statics functions curried as well ! ✨

@briancavalier
Copy link
Member Author

@jgoux Cool. We are going to curry everything in the new a la carte packages, and @most/prelude now includes currying helpers. 😄

@dypsilon
Copy link

Could you please provide an example of point-free code with most.js?

@briancavalier
Copy link
Member Author

@dypsilon Just to be clear: most.js core (ie.. npm install most) doesn't yet support partial application and point free out of the box. We're working on it. However, you can certainly use your favorite curry helper (such as those in Ramda, lodash, @most/prelude, etc) with most.js functions. Also, the a la carte packages, such as @most/sample, are curried by default.

Here are a few examples of doing pointfree with most.js (if you use a curry helper):

import { curry2, compose } from `@most/prelude`
import { map, filter, from } from `most`

const add1 = x => x + 1
const even = x => x % 2 === 0

// Imagine map and filter are curried
const mapc = curry2(map)
const filterc = curry2(filter)

// pointfree declaration of a function that adds 1 to every item in a stream
// and another that keeps only even-valued events
const add1s = mapc(add1)
const evens = filterc(even)

// compose (right to left), also pointfree
// add1Evens is now a function that takes a stream,
// keeps the even-valued events, and adds 1 to them
const add1Evens = compose(add1s, evens)

const numbers = from([1,2,3,4])

add1Evens(numbers).observe(x => console.log(x)) //=> 3, 5

Of course, the real benefit of currying and composition is that once partially-applied and composed, those functions can be reused over and over.

@dypsilon
Copy link

Thank you for this lengthy example. My initial though was, that it's already possible with native most tooling, I just don't know how. Currying the functions myself works fine for me, but I definitely think autocurrying would improve most for this style of programming. Off course it's important to test if this feature will handicap performance.

Here is a tiny hint for fellow pointfree programmers: you could just use Ramda functions like map and filter directly on the stream, since they dispatch to stream.map(x) internally. This way you avoid currying for most functions.

@briancavalier
Copy link
Member Author

No problem @dypsilon. Auto currying may happen after 1.0.0 (which is very close, see the roadmap).

you could just use Ramda functions like map and filter directly on the stream, since they dispatch to stream.map(x) internally

Yes! Thanks for mentioning this. Ramda + most.js is quite a convenient setup since most.js implements a good bit of fantasyland.

@dypsilon
Copy link

@briancavalier those are some good news. Cudos to the team behind most.js. I worked with RxJS, Highland and node.js Streams in object mode. Most.js is the easiest to grasp and work with, while providing some power features like monadic composition, promise composition and now pointfree style!

@tusharmath
Copy link

tusharmath commented Oct 14, 2016

@dypsilon Take a look at the following benchmarks —
screen shot 2016-10-14 at 6 54 14 pm

Pretty much no change in performance. I think this is going to be an awesome addition to mostjs.

I used ramda.curry

const [
  mReduce,
  mMap,
  mFilter,
  mFrom
] = [
    R.curry(most.reduce),
    R.curry(most.map),
    R.curry(most.filter),
    R.curry(most.from)
  ]

const fileMapReduce = R.compose(
  mReduce(sum, 0),
  mMap(add1),
  mFilter(even),
  mFrom
)
 .add('most-curried', function (deferred) {
    runners.runMost(deferred, fileMapReduce(a));
  }, options)

My concern is how big the library would get if we simply import an external curry function.

@TylorS
Copy link
Collaborator

TylorS commented Oct 15, 2016

@tusharmath We actually already have curry defined in @most/prelude that is quite fast and already past of the build actually

@tusharmath
Copy link

tusharmath commented Oct 15, 2016

@TylorS I went thru the code for currying there — https://github.com/mostjs/prelude/blob/master/src/function.js#L13

it looks like it essentially hard coded for functions or arity 2 & 3 only. How about a more generic approach —

function Curry(f) {
    return function curried(...t) {
        if (t.length === 0) return curried;
        if (t.length === f.length) return f(...t);
        return curried.bind(this, ...t);
    };
}

@davidchase
Copy link
Collaborator

@tusharmath I think thats a interesting approach but why do you really need a curry for more than 3 arity? majority of the static method in this lib are binary and some have an arity 3 but i feel like going over 3 would be a code smell, no? plus with es6 if you want to take a "variadic" approach you can do fn(x, y, ...z) which at that point you are really dealing with arrays and its fixed at 3 again

thoughts?

@tusharmath
Copy link

tusharmath commented Oct 15, 2016

@davidchase

have an arity 3 but i feel like going over 3 would be a code smell, no

Yeah agree it would be.

but the code above is actually generic+concise enough for arity = 1, 2, 3 also. That's all :)

@davidchase
Copy link
Collaborator

i see what you mean @tusharmath was just curious on your take

@TylorS
Copy link
Collaborator

TylorS commented Oct 15, 2016

The reason for the hard coding is performance. We avoid the need to use .bind or the spread operator or many other tricks which in plain ES6 in the browser are not yet optimized.

Also, there are no parts of the public API which take more than 3 arguments (currently), which is why we only wrote the curry2 and curry3 functions to begin with.

@davidchase
Copy link
Collaborator

yeah @TylorS that was my next point i would think the engines would prefer to deal with fixed or known arities vs ones that are decided at run time based on the arguments passed to the function.

though i wasn't sure exactly on the performance implications.

@tusharmath
Copy link

@TylorS

We avoid the need to use .bind or the spread operator or many other tricks which in plain ES6 in the browser are not yet optimized.

Totally true! But have you considered that in a real world use case these functions are going to be polymorphic and would anyways get de-optimized.

@jgrund
Copy link

jgrund commented Nov 20, 2016

FWIW, Function.prototype.bind is getting much faster in V8: https://codereview.chromium.org/1542963002

@tusharmath
Copy link

@briancavalier
Copy link
Member Author

@jgrund Yeah, that's good progress. It's only v8, but hopefully other engine will follow suit.

@tusharmath your curry function is quite nice 👍 Once VMs optimize both bind and rest/spread, it'd be nice to switch to a simpler impl like yours. If you're up for doing a performance comparison of different approaches, I'd certainly be willing to switch sooner if such a comparison shows that a simpler implementation is as performant as the existing curry2 and curry3 across the current set of popular VMs.

@tusharmath
Copy link

@briancavalier I would suggest bind is the way to go. The creation is more than 5x faster than creating a new function but execution is 0.5x slower. The issue for this has been filed here — https://bugs.chromium.org/p/v8/issues/detail?id=5605 and is most probably going to get fixed sometime.

How can I help mostjs in this regards?

@briancavalier
Copy link
Member Author

We have to be careful not to leave other VMs in a bad spot. Does anyone have any data on bind performance in other VMs (current/recent versions)?

How can I help mostjs in this regards?

Thanks for the offer! I think if we get data on other VMs, and we see that bind is a good implementation mechanism for currying, then a PR to mostjs/prelude to use bind would be super helpful :)

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

Successfully merging a pull request may close this issue.

8 participants