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("?");
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?.("?");
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};
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!")
};
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
}
Belt.Option.flatMap
is the same as:
let flatMap = (opt, fn) =>
switch (opt) {
| Some(x) => fn(x)
| None => None
}
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!");
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!");
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 */
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);
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 */
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;
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)
};
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.
Top comments (8)
Nice writeup! If I were to nitpick, I’d add that:
>>|
formap
, so maybe it’s slightly more idiomatic in OCaml/Reason (then again, not a lot of programming fonts have ligatures for>>|
😅).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.
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.What is it to add the second type using
and doesThisExist = {orThis: option(string)};
Seems like its interchangeable with:Does the
and
style have name?Thank you, sir.
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.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...@johnridesabike are you on twitter?
I barely use it, but yes 🙂 twitter.com/johnthebikeguy