DEV Community

loading...

Optional chaining in Reason

johnridesabike profile image John Jackson Updated on ・5 min read

May 2021 update: This article was written for ReasonML and BuckleScript, which has since been replaced by ReScript for web development. Most of these concepts are the same, although the syntax and naming is different.

There’s a JavaScript proposal to add syntax for “optional chaining”, which would be a solution to the problem where you’re trying to access deeply into a nested object but properties may be missing.

what.happens.when.one.of.these.doesnt.exist("?");
Enter fullscreen mode Exit fullscreen mode

If any of those properties are undefined, you get a type error. Optional chaining looks like this:

what?.happens?.when?.one?.of?.these?.doesnt?.exist?.("?");
Enter fullscreen mode Exit fullscreen mode

If any property in the “chain” is undefined or null, then the expression returns undefined without an error.

Doing it in Reason

Reason doesn’t have the exact same problem JavaScript has. Strict typing guarantees that records will always have the fields you expect. But chaining optional values and functions can still be an issue, and thankfully Reason has tools to make it manageable.

Setting up your types

As always with Reason, first you have to make sure your types are set up correctly. With external sources, especially, this can be tricky but even more important.

type unreliableObject = {doesThisExist: option(doesThisExist)}
and doesThisExist = {orThis: option(string)};

let everythingExists = {doesThisExist: Some({orThis: Some("Horray!")})};
let doesntExist = {doesThisExist: None};
Enter fullscreen mode Exit fullscreen mode

And now you can access this with an old-fashioned switch statement:

let result =
  switch (everythingExists) {
  | {doesThisExist: None}
  | {doesThisExist: Some({orThis: None})} => None
  | {doesThisExist: Some({orThis: Some(x)})} => Some(x ++ " it exists!")
  };
Enter fullscreen mode Exit fullscreen mode

It works, but can’t we do better?

Helper functions

Since you probably don’t want to write these switch blocks everywhere, we can use some helper functions to make life easier. BuckleScript’s Belt library includes the useful Option module out of the box for us.

Belt.Option.map is the same as:

let map = (opt, fn) =>
  switch (opt) {
  | Some(x) => Some(fn(x))
  | None => None
  }
Enter fullscreen mode Exit fullscreen mode

Belt.Option.flatMap is the same as:

let flatMap = (opt, fn) =>
  switch (opt) {
  | Some(x) => fn(x)
  | None => None
  }
Enter fullscreen mode Exit fullscreen mode

They look very similar, and they are often confused. The difference is that the function passed to flatMap returns a value wrapped in option. The function passed to map returns a non-optional value, and map will automatically wrap it in option for you.

(Note: If you’re coming from other functional languages, then keep in mind that Belt’s functions have the arguments flipped. The optional value comes first, and the function comes last.)

Using the incorrect function may give you a type error, or it may give you a value with an extra option wrapper, e.g.: Some(Some("Hello")), which will compile but also cause headaches later.

Our example above can be rewritten like this:

open Belt;
let result = everythingExists.doesThisExist->Option.flatMap(x => x.orThis)->Option.map(x => x ++ " it exists!");
Enter fullscreen mode Exit fullscreen mode

Much nicer, but we’re not done yet.

Making it prettier with custom infix operators

It’s still a little verbose. We can shorten it more by defining our own custom infix operators. (Remember that Reason’s infix operators are just ordinary functions. You can even re-define existing infix operators with your own versions.)

let (<$>) = Belt.Option.map;
let (>>=) = Belt.Option.flatMap;

let result = everythingExists.doesThisExist >>= (x => x.orThis) <$> (x => x ++ " it exists!");
Enter fullscreen mode Exit fullscreen mode

Using accessor functions

If you find yourself doing this a lot, you can define your own “accessor” functions for record fields:

let doesThisExist = x => x.doesThisExist
let orThis = x => x.orThis;

let result = everythingExists |> doesThisExist >>= orThis <$> (x => x ++ " it exists!");
/* returns Some("Horray! it exists!") */
let result = doesntExist |> doesThisExist >>= orThis <$> (x => x ++ " it exists!");
/* returns None */
Enter fullscreen mode Exit fullscreen mode

It makes your code more readable, plus it optimizes it by reducing the number of inline functions.

BuckleScript tip

Do you wish you didn’t have to keep defining functions to access record fields? This is such a common pattern that BuckleScript can automatically generate them for you. Just add [@bs.deriving accessors] to a record. See more details here: “Generate first-class accessors for record types.

Nullable types

Keep in mind that if you’re dealing with values coming from JavaScript that may be null, then Belt.Option won’t be enough. (None in Reason is the same as undefined, but not null.) You’ll need to convert it with Js.Nullable.toOption:

let (<$>) = (nullable, f) => Belt.Option.map(Js.Nullable.toOption(nullable), f);
Enter fullscreen mode Exit fullscreen mode

Beyond records

Optional record fields isn’t always an issue in Reason, but there are many other situations where you’d want to chain optional functions.

Something a bit more common in Reason is nested variants:

module Covering = {
  type t =
    | Fur(string)
    | Feathers(string);
  let toColor = (Fur(color) | Feathers(color)) => color;
};
module Species = {
  type t =
    | Dog(Covering.t)
    | Fish;
  let toCovering =
    fun
    | Dog(fur) => Some(fur)
    | Fish => None;
};
module Thing = {
  type t =
    | Animal(Species.t)
    | Machine;
  let toSpecies =
    fun
    | Animal(species) => Some(species)
    | Machine => None;
};
let toto = Thing.Animal(Species.Dog(Covering.Fur("black")));
let totoFurColor =
  toto |> Thing.toSpecies >>= Species.toCovering <$> Covering.toColor;
/* totoFurColor = Some("black") */
let nemo = Thing.Animal(Species.Fish);
let nemoFurColor =
  nemo |> Thing.toSpecies >>= Species.toCovering <$> Covering.toColor;
/* nemoFurColor = None */
Enter fullscreen mode Exit fullscreen mode

For another example, consider if you had several maps or hashmaps with related data. You need to look up the data from one map (returning an option) and then use the result to look up data from another map (also returning an option).

let streetName = Map.get(personMap, id) >>= Map.get(addressMap) <$> streetNameOfAddress;
Enter fullscreen mode Exit fullscreen mode

A note about infixes

Why do these examples use <$> and >>= as our infix functions? There’s no good reason, other than an old convention. The option type is a monad, and monads in functional programming conventionally use those infixes for their map and flatMap functions. If you’re using other modules with their own monadic types and functions, then these infixes will feel more consistent.

You can call your own infixes anything you want. If you think something like <?> looks better because it’s closer to the JavaScript ?., then that’s perfectly fine.

The future: let+ or bs-let bindings

The community is currently working on making this process even easier by adding "monadic let" bindings to the language. You can view the status of the project on the bs-let repository and on this pull request.

It allows you to write statements the same way you would write async and await in JavaScript. You can see their example:

type address = {
    street: option(string)
};
type personalInfo = {
    address: option(address)
};
type user = {
    info: option(personalInfo)
};
// Get the user's street name from a bunch of nested options. If anything is
// None, return None.
let getStreet = (maybeUser: option(user)): option(string) => {
    let%Option user = maybeUser;
    // Notice that info isn't an option anymore once we use let%Option!
    let%Option info = user.info;
    let%Option address = info.address;
    let%Option street = address.street;
    Some(street->Js.String.toUpperCase)
};
Enter fullscreen mode Exit fullscreen mode

It's not officially ready for production yet, but still available for you to test out.

Conclusion

Once you get the hang of the Reason-able way of chaining options, you may start to see opportunities to use it throughout your code. It’s a little more complex compared to JavaScript optional chaining, but it’s far more expressive and useful in a wider variety of situations. It’s a great way to make your code less verbose and also more readable.

Discussion (8)

Collapse
hoichi profile image
Sergey Samokhov

Nice writeup! If I were to nitpick, I’d add that:

  1. Jane Street Base uses >>| for map, so maybe it’s slightly more idiomatic in OCaml/Reason (then again, not a lot of programming fonts have ligatures for >>| 😅).
  2. Nested optional values can be a code smell the same way optics are: they can help you to be lax with abstraction boundaries.
Collapse
johnridesabike profile image
John Jackson Author

Good points. As far as >>= goes, I picked it as my example because it’s used by bs-abstract and Relude (which uses bs-abstract). github.com/Risto-Stevcev/bs-abstra...

Although, if you bind them to Belt’s functions, this may still feel “wrong” to people used to data-last infixes. There are tradeoffs no matter what infix you choose.

Collapse
hoichi profile image
Sergey Samokhov • Edited

I’ve also heard (actually, read) Cheng Lou say they’d like to avoid infix operators in general, so probably monadic let is the way to go.

Collapse
idkjs profile image
Alain

What is it to add the second type using and doesThisExist = {orThis: option(string)}; Seems like its interchangeable with:


type doesThisExist = {orThis: option(string)};
type unreliableObject = {doesThisExist: option(doesThisExist)};

Does the and style have name?

Thank you, sir.

Collapse
johnridesabike profile image
John Jackson Author • Edited

You are correct that either way would work. and is used for mutually recursive types. The only thing it does here is let us write our types in reverse order (so in this case, they’re not really mutually recursive).

reasonml.github.io/docs/en/more-on...

Being able use and to write nested type definitions “backwards” can look nice (IMO) when nesting a very large number of types.

Collapse
yawaramin profile image
Yawar Amin

In case of interest in the 'let'-syntax: this article has a great overview and also uses option examples jobjo.github.io/2019/04/24/ocaml-h...

Collapse
idkjs profile image
Alain

@johnridesabike are you on twitter?

Collapse
johnridesabike profile image
Forem Open with the Forem app