cujojs / most

Ultra-high performance reactive programming
MIT License
3.5k stars 231 forks source link

Curry "statics" #20

Open briancavalier opened 10 years ago

briancavalier commented 10 years ago

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 commented 9 years ago

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 commented 9 years ago

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 commented 8 years ago

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 commented 8 years ago

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

briancavalier commented 8 years ago

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

dypsilon commented 8 years ago

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

briancavalier commented 8 years ago

@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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

@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 commented 7 years ago

@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 commented 7 years ago

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

tusharmath commented 7 years ago

@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 commented 7 years ago

@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 commented 7 years ago

@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 commented 7 years ago

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

TylorS commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

@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 commented 7 years ago

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

tusharmath commented 7 years ago

@jgrund not fast enough. https://gist.github.com/tusharmath/eaabea0bee3ad2e3b7ef16ca50f6ac14

briancavalier commented 7 years ago

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

@tusharmath your curry function is quite nice :+1: 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 commented 7 years ago

@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 commented 7 years ago

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 :)