What are the practical differences between andThen and flatMap?

First post here, so forgive me if this question has an obvious answer!

I was looking at the Js.Option and the Belt.Option in the API documentation and was wondering what are the differences between the two. The Js.Option seems to be “data-last” in the sense that the option you pass in comes last, as compared to Belt.Option “data-first” approach.

The data first seems nicer to use when chaining together operations eg:

// This is a bit cleaner...
Some(thing)->flatMap(doSomething)->flatMap(doSomethingElse)

// Than this
Some(thing)->andThen(doSomething, _)->andThen(doSomethingElse, _)

Other than data-first arguably looking a bit nicer, are there any other differences that are important (practically speaking?)

Example

Here is a little example with the compiled JS for illustration:

let reciprocal = (. x) => x == 0. ? None : Some(1. /. x)
let jsVersion = Js.Option.andThen(reciprocal, Some(2.))
let beltVersion = Belt.Option.flatMapU(Some(2.), reciprocal)

Here is the compiled JS output:

function reciprocal(x) {
  if (x === 0) {
    return ;
  } else {
    return 1 / x;
  }
}

var jsVersion = Js_option.andThen(reciprocal, 2);

var beltVersion = Belt_Option.flatMapU(2, reciprocal);

So the JS functions are different, but will it really make a difference which to use? Should one be preferred over the other?

TL;DR

Are there any important differences between Js.Option.andThen and Belt.Option.flatMapU (uncurried version) other than the order of their arguments?

2 Likes

This is a good question and stems from a greater question that still trips me up.

The answer is: they are exactly the same. The implementations of the two methods in the standard library are:

function flatMapU(opt, f) {
  if (opt !== undefined) {
    return f(Caml_option.valFromOption(opt));
  }

}

and

function andThen(f, x) {
  if (x !== undefined) {
    return f(Caml_option.valFromOption(x));
  }
  
}

The bigger question is why are there two and how do you choose?

I believe Js.Option is an older syntax intended to work with the older Reason and OCAML ecosystems, which favour pipe-last (|>) syntax. Belt.Option is more suitable to the Rescript ecosystem which favours pipe-first (->). There are a few libraries in the Js module that support both syntaxes (e.g: Array and Array2), but there is no Js.Option2, at least, not yet.

Belt and Js have a lot of overlap. I generally choose Belt whenever possible, but if I am working with objects that are going to be sent into a Javascript binding, I’ll sometimes switch to the JS module.

5 Likes

Thanks for the reply. Guess I should get mode comfortable looking through the ReScript compiler source code!

I had a question about this:

I generally choose Belt whenever possible, but if I am working with objects that are going to be sent into a Javascript binding, I’ll sometimes switch to the JS module.

That’s interesting that you mention choosing Belt when possible. The API docs seem to suggest picking Js if possible:

If you can find your API in this module, prefer this over an equivalent Belt helper. For example, prefer Js.Array2 over Belt.Array

Do you think the prefer Js suggestion is for an older version of rescript maybe?

1 Like

More likely that I am mistaken. :smiley: Hopefully one of the Rescript devs can chime in on Best practices.

1 Like

Well, it’s a matter of zero-cost vs. safety. So Belt methods are usually safer and mostly immutable, while the Js module exposes only what’s already available in JavaScript anyway. If you write a library, you may not want to bloat your bundle with Belt.

It’s comparable to using lodash in the JS world.

4 Likes

Thanks, that clears a lot of things up for me.