Open briancavalier opened 10 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.
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?
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.
I'd love to have all the statics functions curried as well ! :sparkles:
@jgoux Cool. We are going to curry everything in the new a la carte packages, and @most/prelude
now includes currying helpers. :smile:
Could you please provide an example of point-free code with most.js?
@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.
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.
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.
@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!
@dypsilon Take a look at the following benchmarks —
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.
@tusharmath We actually already have curry defined in @most/prelude
that is quite fast and already past of the build actually
@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);
};
}
@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?
@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 :)
i see what you mean @tusharmath was just curious on your take
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.
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.
@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.
FWIW, Function.prototype.bind
is getting much faster in V8: https://codereview.chromium.org/1542963002
@jgrund not fast enough. https://gist.github.com/tusharmath/eaabea0bee3ad2e3b7ef16ca50f6ac14
@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.
@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?
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 :)
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.