Bind curried data last function as data first

I have been playing around with the pipe operator and RxJS, but I quickly ran into issues
on the binding side. As you might know an operator in RxJS is just a function that takes an observable and returns an observable:

let operator: observable<'a> => observable<'b>

But since we often need extra parameters to know how to build these observable, all RxJS operators
are wrapped with an extra function taking additional parameters:

let map: ('a => 'b) => observable<'a> => observable<'b>

If we were to uncurry that, it looks just like a data last function:

let map: ('a => 'b, observable<'a>) => observable<'b>

Having the data last makes a lot of sense in the RxJS library, since it is used with the .pipe
function in JS (having the data first wouldn’t work with this pattern):

from([1, 2, 3])
  .pipe(map(number => number + 1))
  .subscribe(number => console.log(number));

This brings me to the actual question. Is it possible to make bindings to these operators so they
can be used with the data first pipe operator? Currently I’m doing something like this, which works
fine, but generates a lot of extra code:

type observable<'a>
type operator<'a, 'b> = observable<'a> => observable<'b>;

@module("rxjs/operators")
external _mapOperator: ('a => 'b) => operator<'a, 'b> = "map"
let map = (source, project) => _mapOperator(project)(source)

And then I only use the map function. Complete example:

type observable<'a>
type operator<'a, 'b> = observable<'a> => observable<'b>;
type subscription

@send
external subscribe: (observable<'a>, 'a => unit) => subscription = "subscribe"

@module("rxjs")
external fromArray: array<'a> => observable<'a> = "from"

@module("rxjs/operators")
external _mapOperator: ('a => 'b) => operator<'a, 'b> = "map"
let map = (source, project) => _mapOperator(project)(source)

let sub = fromArray([1, 2, 3])
  ->map(number => number + 1)
  ->subscribe(number => Js.log(number)) // Logs 2, 3, 4

This also works and generates less code, but IMO looks pretty bad with the placeholder syntax:

@module("rxjs/operators")
external map: ('a => 'b, . observable<'a>) => observable<'b> = "map"

let obs = fromArray([1, 2, 3])
  ->map(number => number + 1)(. _)
  ->subscribe(number => Js.log(number))

@ManBearTM

I don’t think you can change the order of inputs while binding. Maybe you can use some higher order functions like flip.

let flip = (f, x, y) => f(y, x)
let flipC = (f, x, y) => f(y)(. x)

Now you can just say

let map = _mapOperator->flipC

This can be used as somewhat cleaner workaround.

1 Like

If the JS library itself is designed to worked data-last, wouldn’t it be simpler to bind it that way and use the pipe-last operator (|>)?

1 Like

Yeah I feared as much, the use case is probably too advanced for the bindings api. A higher order function like that would indeed be cleaner, thanks for the suggestion. I might just go with that.

1 Like

It makes sense, but I didn’t want to go down that road since the pipe-last operator is deprecated. It would be nice if it was there as an alternative, but I can understand why they don’t want to mantain two different pipe operators.

That’s true but I doubt it will be removed. There’s a ton of code out there using it and it wouldn’t be an automatic migration from that to pipe-first. And even if it was removed, it’s easy enough to define it in the project. Plus I think it’s a good idea to keep bindings to JS libraries as close to the original library as possible to prevent confusion for the user. If the JS library itself was using data-first or OOP style, then pipe-first would make sense. But since it’s using data-last style, pipe-last makes more sense imho.

1 Like

FWIW, you can still use data-last functions with pipe-first and the _ placeholder: a->f(b, _).

3 Likes

It is an interesting take! Personally I’d love the option to use both pipe operators, but IMO it feels wrong to use something that is marked as deprecated. I wonder what other people think about this.

And even if it was removed, it’s easy enough to define it in the project.

I don’t understand? You can redefine operators? :open_mouth:

Plus I think it’s a good idea to keep bindings to JS libraries as close to the original library as possible to prevent confusion for the user.

It’s a good point! These bindings would just be for me, but I can see why it might be confusing to reorder parameters like that (and perhaps that is one of the reasons why it is not possible to do on the binding side).

:+1: You’re right, though not in the binding itself. I just tried with the placeholder again and managed to make it work like this:

let sub = fromArray([1, 2, 3])
   ->map(number => number + 1)(_)
   ->subscribe(number => Js.log(number))

Which looks okay, but gets kinda tedious with RxJS, since you tend to have a lot of operators (and even though it makes sense logically, it is arguably more confusing to people that are used to the JS api). I’d rather just go with the .pipe api over this.

1 Like

In that specific example, wouldn’t it be cleaner to use Belt.Array.map first and fromArray afterwards?

Like so:

let sub =
  [1, 2, 3]
  ->Belt.Array.map(number => number + 1)
  ->fromArray
  ->subscribe(number => Js.log(number))

My example code is completely useless, it was only to keep it as simple as possible :grinning_face_with_smiling_eyes: But you are right, might as well use the regular array functions here.