The only optimisation I know of in ReScript so far is filterMap. And AFIAK, that’s about it for OCaml too. Nothing fancy unlike, say, Haskell.
As for transducers you mentioned, this obviously can be done in userland. No idea if they’re going to be popular with ReScript crowd though. There must me a reason why they’re not mainstream.
let fn1 = Js.Array2.map(a => a + 1);
let fn2 = Js.Array2.map(a => a + 2);
let result1 = [1, 2, 3]
-> fn1
-> fn2
Or having just one map? That would be more efficient. But depending on the user writing efficient code, usually trading for readability, is what the compiling should help with. Google “transducers” for some more use cases.
This is an interesting question. I think the main reason transducers aren’t used as much in javascript (and so rescript too) is that they are super slow (at least in my experience). Now what you show don’t look like transducers, but rather you wanting some sort of iter/gen/stream fusion thing.
A couple of months ago I benchmarked some of the ways of doing this in rescript/javascript. (Unfortunately the code is not available publicly right now, as I’m a bit busy to clean it up, but at some point I would like to.)
at the time I did the benchmark it was in his extras library, so it may have changed since then
this one is a generator (lazy/pull-based/thunk), eg:
type rec t<'a> = (. unit) => node<'a>
and node<'a> =
| End
| Next('a, t<'a>)
a quick port of c-cube’s OCaml iter library to rescript that I did
this one is a push-based iterator: type t<+'a> = ('a => unit) => unit
in OCaml it is very fast and with the certain compiler options can basically compile down to for loop
different levels of “chaining” Array map/reduce vs “in-lining” the steps into a single fold function
The last point I mean something like this:
// All inlined
ary->Array.filterMap(x => {
let y = x->f1->f2->f3
if predicate(y) {
Some(y->f4->f5)
} else {
None
}
})
// Sort of inlined, sort of broken up
ary->map(x => x->f1->f2->f3)->filter(predicate)->map(x => x->f4->f5)
// All broken up
ary->map(f1)->map(f2)->map(f3)->filter(predicate)->map(f4)->map(f5)
The overall results in terms of speed were (fastest to slowest)
array ops all inlined
array ops sort of inlined
array ops broken up
transducer
iterator
generator
Anyway, maybe that will be interesting for you.
Edit: It really is sort of a shame that the chaining methods aren’t more efficient in javascript, as I do think it helps with readability. I wonder if there is a way to do them efficiently, as it is a nice style I think.
A bit off topic, but OCaml has a lot of different ways of doing this sort of thing. You might find this benchmark interesting.
I have always thought this was a big gain for FP to be able to do this without writing it explicitly. Though maybe the compilers under Rescript do it just as well?
Nice, thanks for posting. Just an FYI, that link is broken for me. …not that I can complain, as I didn’t even post my code
I saw a similar behavior as well. I guess it’s just something the javascript engine doesn’t optimize well…
Btw, were you running your benchmarks on node, in the browser? I saw ops/sec differences up to three orders of magnitude between the generator and the “everything in one fold function” way. …I should really post my code, but it was actually pretty similar to that example I gave above: map a couple functions, filter, then map a couple more (all pretty simple math functions), with the array size of 1,000 items and 10,000 items.
The main purpose of transducer is to abstract away input/output types of the collection, and they only get performance gain where lazy evaluation is by default like Haskell and Clojure, because transducers behave as eager.
I’m dubious that transducers will be used well in ReScript.