DEV Community

loading...

Destructuring all the things with Reason

johnridesabike profile image John Jackson ・3 min read

Destructuring and pattern-matching is one of the top features of Reason’s syntax. These are a few tricks I’ve picked up while learning it. Some techniques here may be well known among Reason veterans, but not all are visible in documentation or easy to figure out on your own. Enjoy!

Binding with simple destructuring

Suppose you have a variant like this:

type character =
  | Elephant(string)
  | Turtle(string);

let horton = Elephant("Horton");
let yertle = Turtle("Yertle");

You may want to extract the string from one of those values like this:

let name =
  switch (yertle) {
  | Elephant(name)
  | Turtle(name) => name
  };

Which works, but you can make it even simpler.

let Elephant(name) | Turtle(name) = yertle;

You can even do it in function arguments.

let getName = (Elephant(name) | Turtle(name)) => name;

getName(yertle);

If it’s not obvious, this doesn’t work if the value doesn’t exist on every variant.

/* WRONG. Doesn't account for `None`. */
let Some(name) = optionalName;

Pattern match records

Suppose you have a type like this:

type book = {title: string, author: string};

let book = {title: "The Cat in the Hat", author: "Dr. Seuss"};

If you want to pattern-match multiple values, you might try this at first:

let comment = 
  switch (book.title, book.author) {
  | ("The Cat in the Hat", "Dr.Seuss") => "That's my favorite book!"
  | (_, _) => "Nice book."
  };

But it makes more sense to just pattern-match the whole record.

let comment =
  switch (book) {
  | {title: "The Cat in the Hat", author: "Dr. Seuss"} =>
    "That's my favorite book!"
  | {author: "Dr. Seuss", _} => "That's my favorite author!"
  | _ => "Nice book."
  };

Re-binding record fields when destructuring

Destructuring record fields is similar to JavaScript.

let {title, author} = book;

But what if you don’t want your values named title and author? You can bind them with this syntax:

let {title: a, author: b} = book;
/* a == "The Cat in the Hat" */

Binding values with as

Suppose you want to destructure a record or variant but you still want to access the original, non-destructured, value. You can do it like this:

let logBook = ({title, author} as book) => {
  Js.log2("The title is:", title);
  Js.log2("The author is:", author);
  doSomethingWithBook(book);
};

let logCharacter = ((Elephant(name) | Turtle(name)) as character) => {
  Js.log2("The character's name is:", name);
  doSomethingWithCharacter(character);
};

fun functions

Many functions are just one giant switch statement, so Reason has a special syntax just for that.

let rec logAllItems = list =>
  switch (list) {
  | [] => Js.log("End of list")
  | [x, ..rest] =>
    Js.log(x);
    logAllItems(rest);
  };

The above is equivalent to this:

let rec logAllItems =
  fun
  | [] => Js.log("End of list")
  | [x, ...rest] => {
      Js.log(x);
      logAllItems(rest);
    };

fun is just a shorthand for when the argument of your function is immediately used in a switch. Note how we don’t need to reference the function argument anywhere when we use fun. It’s implicitly there.

Namespaces with records

Let’s put our book type into a module.

module Book = {
  type t = {title: string, author: string};
};

Record fields are namespaced to their modules, so if you try to use title or author outside of Book you may get an error.

let book = {title: "The Cat in the Hat", author: "Dr. Seuss"};
/*          ^^^^^
  Error: Unbound record field title.
*/

You can get around this with an open.

open Book;

Or open it for a one-line expression.

open Book.(/* expresion goes here. */);

But the syntax for opening with a single record or list is slightly simpler.

let book = Book.{title: "Cat in the Hat", author: "Dr. Seuss"};

let bookList =
  Book.[
    {title: "Cat in the Hat", author: "Dr. Seuss"},
    {title: "Yertle the Turtle", author: "Dr. Seuss"},
  ];

It works for destructuring as well.

let getTitle = (Book.{title, _}) => title;

And you can access a single field by putting the module name before the field.

let author = book.Book.author;

Module namespaces can feel like a point of friction for newcomers, especially when you follow the OCaml guideline of “a module for (almost) every type.” But taking advantage of this syntax makes it much easier. As the Zen of Python says: “Namespaces are one honking great idea—let's do more of those!”

Combine all of the above

module Character = {
  type t =
    | Elephant(string)
    | Turtle(string);
};

module Book = {
  type t = {
    title: string,
    author: string,
    character: Character.t,
  };
};

let rec logAllTheBooks =
  fun
  | [] => Js.log("End of list")
  | [
      Book.{
        title,
        author,
        character: Character.(Elephant(name) | Turtle(name)),
      } as book,
      ...rest,
    ] => {
      Js.log({j|Title: $title. Author: $author. Character: $name|j});
      doSomethingWithBook(book);
      logAllTheBooks(rest);
    };

Discussion

pic
Editor guide
Collapse
happylinks profile image
Michiel Westerbeek

Thanks! Learned how to destructure objects without “as”, with “:”. Always had “not used” warnings because I didn’t know I could do that😁 Also learned “fun”!

Collapse
austindd profile image
Austin

Very cool. I wasn't even aware of a few of these patterns!