Skip to content

Better Burgers

Cafe Emoji is still the hottest restaurant in town, despite the fact that all burgers are exactly the same. Madame Jellobutter isn’t one to rest on her laurels, though, so she decides to kick it up a notch by allowing customers to choose the toppings on their burgers.

Currently, your Item.t variant type looks something like this:

re
type sandwich =
  | Portabello
  | Ham
  | Unicorn
  | Turducken;

type t =
  | Sandwich(sandwich)
  | Burger
  | Hotdog;
type sandwich =
  | Portabello
  | Ham
  | Unicorn
  | Turducken;

type t =
  | Sandwich(sandwich)
  | Burger
  | Hotdog;

burger record type

Add a new burger record type and then add that new type as an argument for the Burger constructor of Item.t:

re
type burger = {
  lettuce: bool,
  onions: int,
  cheese: int,
};

type t =
  | Sandwich(sandwich)
  | Burger(burger)
  | Hotdog;
type burger = {
  lettuce: bool,
  onions: int,
  cheese: int,
};

type t =
  | Sandwich(sandwich)
  | Burger(burger)
  | Hotdog;

The fields in the burger record are:

NameTypeMeaning
lettuceboolif true, include lettuce
onionsintthe number of onion slices
cheeseintthe number of cheese slices

Records are similar to the Js.t objects we’ve seen before, in that they both group a collection of values into a single object with named fields. However, there are a number of syntactic and practical differences between them:

  • Record fields must be predefined
  • Records use . to access fields, while Js.t objects use ##
  • Records can be destructured and pattern matched
  • Records use nominal typing, while Js.t objects use structural typing. This means that two record types with exactly the same fields are still considered different types.

The runtime representation of a record is a plain JavaScript object, the same as for a Js.t object.

INFO

It might be better to give more semantic names to the record fields, such as hasLettuce and onionCount, but we’ve kept the names short in order to make the code listings shorter.

Update Item.toEmoji function

Depending on what toppings were chosen and how many of toppings there are, we want to show different emoji:

itemItem.toEmoji(item)
Burger({lettuce: true, onions: 1, cheese: 2})🍔{🥬,🧅×1,🧀×2}
Burger({lettuce: false, onions: 0, cheese: 0})🍔{🧅×0,🧀×0}

To support this logic, we need to add a | Burger(burger) branch to the fun expression inside of Item.toEmoji:

re
let toEmoji =
  fun
  | Hotdog => {js|🌭|js}
  | Sandwich(sandwich) =>
    Printf.sprintf(
      {js|🥪(%s)|js},
      switch (sandwich) {
      | Portabello => {js|🍄|js}
      | Ham => {js|🐷|js}
      | Unicorn => {js|🦄|js}
      | Turducken => {js|🦃🦆🐓|js}
      },
    )
  | Burger(burger) =>
    Printf.sprintf(
      {js|🍔{%s}|js},
      [|
        burger.lettuce ? {js|🥬|js} : "",
        {js|🧅×|js} ++ string_of_int(burger.onions),
        {js|🧀×|js} ++ string_of_int(burger.cheese),
      |]
      |> Js.Array.filter(~f=str => str != "")
      |> Js.Array.join(~sep=","),
    );
let toEmoji =
  fun
  | Hotdog => {js|🌭|js}
  | Sandwich(sandwich) =>
    Printf.sprintf(
      {js|🥪(%s)|js},
      switch (sandwich) {
      | Portabello => {js|🍄|js}
      | Ham => {js|🐷|js}
      | Unicorn => {js|🦄|js}
      | Turducken => {js|🦃🦆🐓|js}
      },
    )
  | Burger(burger) =>
    Printf.sprintf(
      {js|🍔{%s}|js},
      [|
        burger.lettuce ? {js|🥬|js} : "",
        {js|🧅×|js} ++ string_of_int(burger.onions),
        {js|🧀×|js} ++ string_of_int(burger.cheese),
      |]
      |> Js.Array.filter(~f=str => str != "")
      |> Js.Array.join(~sep=","),
    );

Note the use of Js.Array.filter to filter out empty strings in the array.

Destructuring records

It’s not necessary to write burger.* repeatedly, OCaml gives us a nice syntax for destructuring records:

re
| Burger(burger) => {
    let {lettuce, onions, cheese} = burger;
    Printf.sprintf(
      {js|🍔{%s}|js},
      [|
        lettuce ? {js|🥬|js} : "",
        {js|🧅×|js} ++ string_of_int(onions),
        {js|🧀×|js} ++ string_of_int(cheese),
      |]
      |> Js.Array.filter(~f=str => str != "")
      |> Js.Array.join(~sep=","),
    );
  };
| Burger(burger) => {
    let {lettuce, onions, cheese} = burger;
    Printf.sprintf(
      {js|🍔{%s}|js},
      [|
        lettuce ? {js|🥬|js} : "",
        {js|🧅×|js} ++ string_of_int(onions),
        {js|🧀×|js} ++ string_of_int(cheese),
      |]
      |> Js.Array.filter(~f=str => str != "")
      |> Js.Array.join(~sep=","),
    );
  };

Even better would be to do the destructuring directly inside the | Burger(_) pattern match:

re
| Burger({lettuce, onions, cheese}) =>
  Printf.sprintf(
    {js|🍔{%s}|js},
    [|
      lettuce ? {js|🥬|js} : "",
      {js|🧅×|js} ++ string_of_int(onions),
      {js|🧀×|js} ++ string_of_int(cheese),
    |]
    |> Js.Array.filter(~f=str => str != "")
    |> Js.Array.join(~sep=","),
  );
| Burger({lettuce, onions, cheese}) =>
  Printf.sprintf(
    {js|🍔{%s}|js},
    [|
      lettuce ? {js|🥬|js} : "",
      {js|🧅×|js} ++ string_of_int(onions),
      {js|🧀×|js} ++ string_of_int(cheese),
    |]
    |> Js.Array.filter(~f=str => str != "")
    |> Js.Array.join(~sep=","),
  );

There is a little bit of redundancy to the logic for displaying onion and cheese toppings, which we can remedy by adding a small helper function called multiple:

re
| Burger({lettuce, onions, cheese}) => {
    let multiple = (emoji, count) =>
      Printf.sprintf({js|%s×%d|js}, emoji, count);

    Printf.sprintf(
      {js|🍔{%s}|js},
      [|
        lettuce ? {js|🥬|js} : "",
        multiple({js|🧅|js}, onions),
        multiple({js|🧀|js}, cheese),
      |]
      |> Js.Array.filter(~f=str => str != "")
      |> Js.Array.join(~sep=","),
    );
  };
| Burger({lettuce, onions, cheese}) => {
    let multiple = (emoji, count) =>
      Printf.sprintf({js|%s×%d|js}, emoji, count);

    Printf.sprintf(
      {js|🍔{%s}|js},
      [|
        lettuce ? {js|🥬|js} : "",
        multiple({js|🧅|js}, onions),
        multiple({js|🧀|js}, cheese),
      |]
      |> Js.Array.filter(~f=str => str != "")
      |> Js.Array.join(~sep=","),
    );
  };

Burger submodule

The Item.toEmoji function is arguably getting a bit large and could stand to be broken up into smaller functions. The most straightforward way to do it is to add a new function called Item.toBurgerEmoji and call it from within Item.toEmoji. However, in OCaml, we have another choice: create a new submodule called Burger and put all the types and functions related to burgers inside of it:

re
module Burger = {
  type t = {
    lettuce: bool,
    onions: int,
    cheese: int,
  };

  let toEmoji = ({lettuce, onions, cheese}) => {
    let multiple = (emoji, count) =>
      Printf.sprintf({js|%s×%d|js}, emoji, count);

    Printf.sprintf(
      {js|🍔{%s}|js},
      [|
        lettuce ? {js|🥬|js} : "",
        multiple({js|🧅|js}, onions),
        multiple({js|🧀|js}, cheese),
      |]
      |> Js.Array.filter(~f=str => str != "")
      |> Js.Array.join(~sep=","),
    );
  };

  let toPrice = _burger => 15.;
};
module Burger = {
  type t = {
    lettuce: bool,
    onions: int,
    cheese: int,
  };

  let toEmoji = ({lettuce, onions, cheese}) => {
    let multiple = (emoji, count) =>
      Printf.sprintf({js|%s×%d|js}, emoji, count);

    Printf.sprintf(
      {js|🍔{%s}|js},
      [|
        lettuce ? {js|🥬|js} : "",
        multiple({js|🧅|js}, onions),
        multiple({js|🧀|js}, cheese),
      |]
      |> Js.Array.filter(~f=str => str != "")
      |> Js.Array.join(~sep=","),
    );
  };

  let toPrice = _burger => 15.;
};

Note that we renamed the burger type to t since, by convention, the primary type of a module is called t. Also, we put the destructuring of the burger record in the argument list of the Burger.toEmoji function.

In order to get everything to compile again, we’ll need to also update the definitions for Item.t, Item.toEmoji, and Item.toPrice:

re
type t =
  | Sandwich(sandwich)
  | Burger(Burger.t)
  | Hotdog;

let toEmoji =
  fun
  | Hotdog => {js|🌭|js}
  | Burger(burger) => Burger.toEmoji(burger)
  | Sandwich(sandwich) =>
    Printf.sprintf(
      {js|🥪(%s)|js},
      switch (sandwich) {
      | Portabello => {js|🍄|js}
      | Ham => {js|🐷|js}
      | Unicorn => {js|🦄|js}
      | Turducken => {js|🦃🦆🐓|js}
      },
    );

let toPrice = t => {
  let day = Js.Date.make() |> Js.Date.getDay |> int_of_float;

  switch (t) {
  | Sandwich(Portabello | Ham) => 10.
  | Sandwich(Unicorn) => 80.
  | Sandwich(Turducken) when day == 2 => 10.
  | Sandwich(Turducken) => 20.
  | Burger(burger) => Burger.toPrice(burger)
  | Hotdog => 5.
  };
};
type t =
  | Sandwich(sandwich)
  | Burger(Burger.t)
  | Hotdog;

let toEmoji =
  fun
  | Hotdog => {js|🌭|js}
  | Burger(burger) => Burger.toEmoji(burger)
  | Sandwich(sandwich) =>
    Printf.sprintf(
      {js|🥪(%s)|js},
      switch (sandwich) {
      | Portabello => {js|🍄|js}
      | Ham => {js|🐷|js}
      | Unicorn => {js|🦄|js}
      | Turducken => {js|🦃🦆🐓|js}
      },
    );

let toPrice = t => {
  let day = Js.Date.make() |> Js.Date.getDay |> int_of_float;

  switch (t) {
  | Sandwich(Portabello | Ham) => 10.
  | Sandwich(Unicorn) => 80.
  | Sandwich(Turducken) when day == 2 => 10.
  | Sandwich(Turducken) => 20.
  | Burger(burger) => Burger.toPrice(burger)
  | Hotdog => 5.
  };
};

Update Item.Burger.toPrice

Let’s update Item.Burger.toPrice so that burgers with more toppings cost more. Specifically, each onion slice costs 20 cents and each cheese slice costs 10 cents:

reason
let toPrice = ({onions, cheese, lettuce}) => {
  15.  // base cost
  +. float_of_int(onions)
  *. 0.2
  +. float_of_int(cheese)
  *. 0.1;
};
let toPrice = ({onions, cheese, lettuce}) => {
  15.  // base cost
  +. float_of_int(onions)
  *. 0.2
  +. float_of_int(cheese)
  *. 0.1;
};

You get an error from Melange:

File "src/order-confirmation/Item.re", line 24, characters 34-41:
24 |   let toPrice = ({onions, cheese, lettuce}) => {
                                       ^^^^^^^
Error (warning 27 [unused-var-strict]): unused variable lettuce.
File "src/order-confirmation/Item.re", line 24, characters 34-41:
24 |   let toPrice = ({onions, cheese, lettuce}) => {
                                       ^^^^^^^
Error (warning 27 [unused-var-strict]): unused variable lettuce.

Since Madame Jellobutter doesn’t want to charge for lettuce, we don’t need the value of the lettuce variable and can remove it:

reason
let toPrice = ({onions, cheese}) => {
  15.  // base cost
  +. float_of_int(onions)
  *. 0.2
  +. float_of_int(cheese)
  *. 0.1;
};
let toPrice = ({onions, cheese}) => {
  15.  // base cost
  +. float_of_int(onions)
  *. 0.2
  +. float_of_int(cheese)
  *. 0.1;
};

However, this results in a different error:

File "src/order-confirmation/Item.re", line 24, characters 17-33:
24 |   let toPrice = ({onions, cheese}) => {
                      ^^^^^^^^^^^^^^^^
Error (warning 9 [missing-record-field-pattern]): the following labels are not bound in this record pattern:
lettuce
Either bind these labels explicitly or add '; _' to the pattern.
File "src/order-confirmation/Item.re", line 24, characters 17-33:
24 |   let toPrice = ({onions, cheese}) => {
                      ^^^^^^^^^^^^^^^^
Error (warning 9 [missing-record-field-pattern]): the following labels are not bound in this record pattern:
lettuce
Either bind these labels explicitly or add '; _' to the pattern.

OCaml doesn’t like that you left the lettuce field out of the pattern match. We could take the second piece of advice from the error message and add a wildcard to the end of the record pattern match:

re
let toPrice = ({onions, cheese, _}) =>
  15.  // base cost
  +. float_of_int(onions)
  *. 0.2
  +. float_of_int(cheese)
  *. 0.1;
let toPrice = ({onions, cheese, _}) =>
  15.  // base cost
  +. float_of_int(onions)
  *. 0.2
  +. float_of_int(cheese)
  *. 0.1;

But this makes our code less future-proof. For example, what if you added a bacon field to the Burger.t record and it was expected that adding bacon increases the price? With the wildcard in place, the compiler would not tell you to update the Burger.toPrice function. A better way would be to match on lettuce but put a wildcard for its value:

re
let toPrice = ({onions, cheese, lettuce: _}) =>
  15.  // base cost
  +. float_of_int(onions)
  *. 0.2
  +. float_of_int(cheese)
  *. 0.1;
let toPrice = ({onions, cheese, lettuce: _}) =>
  15.  // base cost
  +. float_of_int(onions)
  *. 0.2
  +. float_of_int(cheese)
  *. 0.1;

This tells the compiler that you expect a lettuce field but you don’t intend to use its value inside the function. Similarly to how it’s preferable to match on all constructors of a variant type, it’s also a good idea to match on all fields of a record type.

Pattern matching records

Right now, if a customer doesn’t add any toppings to a burger, Item.Burger.toPrice will return 🍔{🧅×0,🧀×0}. It would be better if it just returned 🍔 to make it clear that it’s a burger without any embellishments. One way to handle this is to use a ternary expression:

re
let toEmoji = ({lettuce, onions, cheese}) => {
  let multiple = (emoji, count) =>
    Printf.sprintf({js|%s×%d|js}, emoji, count);

  !lettuce && onions == 0 && cheese == 0
    ? {js|🍔|js}
    : Printf.sprintf(
        {js|🍔{%s}|js},
        [|
          lettuce ? {js|🥬|js} : "",
          multiple({js|🧅|js}, onions),
          multiple({js|🧀|js}, cheese),
        |]
        |> Js.Array.filter(~f=str => str != "")
        |> Js.Array.join(~sep=","),
      );
};
let toEmoji = ({lettuce, onions, cheese}) => {
  let multiple = (emoji, count) =>
    Printf.sprintf({js|%s×%d|js}, emoji, count);

  !lettuce && onions == 0 && cheese == 0
    ? {js|🍔|js}
    : Printf.sprintf(
        {js|🍔{%s}|js},
        [|
          lettuce ? {js|🥬|js} : "",
          multiple({js|🧅|js}, onions),
          multiple({js|🧀|js}, cheese),
        |]
        |> Js.Array.filter(~f=str => str != "")
        |> Js.Array.join(~sep=","),
      );
};

Another approach is to pattern match on the tuple of (lettuce, onions, cheese):

re
let toEmoji = ({lettuce, onions, cheese}) => {
  let multiple = (emoji, count) =>
    Printf.sprintf({js|%s×%d|js}, emoji, count);

  switch (lettuce, onions, cheese) {
  | (false, 0, 0) => {js|🍔|js}
  | (lettuce, onions, cheese) =>
    Printf.sprintf(
      {js|🍔{%s}|js},
      [|
        lettuce ? {js|🥬|js} : "",
        multiple({js|🧅|js}, onions),
        multiple({js|🧀|js}, cheese),
      |]
      |> Js.Array.filter(~f=str => str != "")
      |> Js.Array.join(~sep=","),
    )
  };
};
let toEmoji = ({lettuce, onions, cheese}) => {
  let multiple = (emoji, count) =>
    Printf.sprintf({js|%s×%d|js}, emoji, count);

  switch (lettuce, onions, cheese) {
  | (false, 0, 0) => {js|🍔|js}
  | (lettuce, onions, cheese) =>
    Printf.sprintf(
      {js|🍔{%s}|js},
      [|
        lettuce ? {js|🥬|js} : "",
        multiple({js|🧅|js}, onions),
        multiple({js|🧀|js}, cheese),
      |]
      |> Js.Array.filter(~f=str => str != "")
      |> Js.Array.join(~sep=","),
    )
  };
};

However, pattern matching on tuples of more than 2 elements tends to be hard to read; it can even be error-prone when some of the elements of the tuple are of the same type. For example, what if you accidentally changed the positions of the onions and cheese variables in the second branch?

reason
switch (lettuce, onions, cheese) {
| (false, 0, 0) => {js|🍔|js}
| (lettuce, cheese, onions) =>
switch (lettuce, onions, cheese) {
| (false, 0, 0) => {js|🍔|js}
| (lettuce, cheese, onions) =>

The compiler wouldn’t complain but the ensuing logic would likely be wrong.

The best approach here is to use the record itself as the input to the switch expression:

re
let toEmoji = t => {
  let multiple = (emoji, count) =>
    Printf.sprintf({js|%s×%d|js}, emoji, count);

  switch (t) {
  | {lettuce: false, onions: 0, cheese: 0} => {js|🍔|js}
  | {lettuce, onions, cheese} =>
    Printf.sprintf(
      {js|🍔{%s}|js},
      [|
        lettuce ? {js|🥬|js} : "",
        multiple({js|🧅|js}, onions),
        multiple({js|🧀|js}, cheese),
      |]
      |> Js.Array.filter(~f=str => str != "")
      |> Js.Array.join(~sep=","),
    )
  };
};
let toEmoji = t => {
  let multiple = (emoji, count) =>
    Printf.sprintf({js|%s×%d|js}, emoji, count);

  switch (t) {
  | {lettuce: false, onions: 0, cheese: 0} => {js|🍔|js}
  | {lettuce, onions, cheese} =>
    Printf.sprintf(
      {js|🍔{%s}|js},
      [|
        lettuce ? {js|🥬|js} : "",
        multiple({js|🧅|js}, onions),
        multiple({js|🧀|js}, cheese),
      |]
      |> Js.Array.filter(~f=str => str != "")
      |> Js.Array.join(~sep=","),
    )
  };
};

Note that destructuring of the record has been moved from the argument list to the branches of the switch expression. Now Item.Burger.toEmoji gets the name t for its single argument.


Magnifique! The order confirmation widget now supports burgers with different toppings. In the next chapter, we’ll start writing tests for our code.

Overview

  • Record types are like Js.t objects but their fields must be explicitly defined

    • Records and Js.t objects are both JavaScript objects during runtime
  • You can get the fields out of a record using destructuring and pattern matching:

    • let destructuring:
      reason
      let {a, b, c} = record;
      let {a, b, c} = record;
    • Switch expression branch pattern matching:
      reason
      switch (record) {
      | {a: "foo", b: "bar", c: 42} => "Magnifique!"
      | {a, b, c} => a ++ b ++ string_of_int(c)
      };
      switch (record) {
      | {a: "foo", b: "bar", c: 42} => "Magnifique!"
      | {a, b, c} => a ++ b ++ string_of_int(c)
      };
  • It’s common practice to group related types and functions into a submodule

  • Try not to ignore record fields when pattern matching on records. Instead of

    reason
    | {a, b, _} =>
    | {a, b, _} =>

    Prefer

    reason
    | {a, b, c: _, d: _} =>
    | {a, b, c: _, d: _} =>

    Assuming c and d aren’t used inside the branch.

  • Try not to pattern match on tuples of more than 2 elements because it tends to be hard to read

Exercises

1. Inside Item.re, create another submodule for sandwich-related types and functions.

Solution

The Item.Sandwich submodule should look something like this:

re
module Sandwich = {
  type t =
    | Portabello
    | Ham
    | Unicorn
    | Turducken;

  let toPrice = t => {
    let day = Js.Date.make() |> Js.Date.getDay |> int_of_float;

    switch (t) {
    | Portabello
    | Ham => 10.
    | Unicorn => 80.
    | Turducken when day == 2 => 10.
    | Turducken => 20.
    };
  };

  let toEmoji = t =>
    Printf.sprintf(
      {js|🥪(%s)|js},
      switch (t) {
      | Portabello => {js|🍄|js}
      | Ham => {js|🐷|js}
      | Unicorn => {js|🦄|js}
      | Turducken => {js|🦃🦆🐓|js}
      },
    );
};
module Sandwich = {
  type t =
    | Portabello
    | Ham
    | Unicorn
    | Turducken;

  let toPrice = t => {
    let day = Js.Date.make() |> Js.Date.getDay |> int_of_float;

    switch (t) {
    | Portabello
    | Ham => 10.
    | Unicorn => 80.
    | Turducken when day == 2 => 10.
    | Turducken => 20.
    };
  };

  let toEmoji = t =>
    Printf.sprintf(
      {js|🥪(%s)|js},
      switch (t) {
      | Portabello => {js|🍄|js}
      | Ham => {js|🐷|js}
      | Unicorn => {js|🦄|js}
      | Turducken => {js|🦃🦆🐓|js}
      },
    );
};

You also need to refactor Item.toPrice and Item.toEmoji functions to use the new functions in Item.Sandwich.

2. Add tomatoes: bool and bacon: int fields to Item.Burger.t. Let’s say that adding tomatoes costs $0.05 and each piece of bacon[1] costs $0.5.

Solution

After adding tomatoes and bacon fields to Item.Burger.t, the changes to Item.Burger.toEmoji are fairly mechanical, so let’s focus on Item.Burger.toPrice:

re
type t = {
  lettuce: bool,
  onions: int,
  cheese: int,
  tomatoes: bool,
  bacon: int,
};

let toPrice = ({onions, cheese, tomatoes, bacon, lettuce: _}) =>
  15.  // base cost
  +. float_of_int(onions)
  *. 0.2
  +. float_of_int(cheese)
  *. 0.1
  +. (tomatoes ? 0.05 : 0.0)
  +. float_of_int(bacon)
  *. 0.5;
type t = {
  lettuce: bool,
  onions: int,
  cheese: int,
  tomatoes: bool,
  bacon: int,
};

let toPrice = ({onions, cheese, tomatoes, bacon, lettuce: _}) =>
  15.  // base cost
  +. float_of_int(onions)
  *. 0.2
  +. float_of_int(cheese)
  *. 0.1
  +. (tomatoes ? 0.05 : 0.0)
  +. float_of_int(bacon)
  *. 0.5;

Note that we keep lettuce: _ at the end of the pattern match since the value of lettuce isn’t ever used.

3. Make Item.Burger.toPrice function more readable by writing a helper function that calculates the cost of a topping by multiplying its price with its quantity.

reason
let toPrice = ({onions, cheese, tomato, bacon, lettuce: _}) => {
  let toppingCost = /* write me */;

  15.  // base cost
  +. toppingCost(onions, 0.2)
  +. toppingCost(cheese, 0.1)
  /* some stuff involving tomato and bacon */;
};
let toPrice = ({onions, cheese, tomato, bacon, lettuce: _}) => {
  let toppingCost = /* write me */;

  15.  // base cost
  +. toppingCost(onions, 0.2)
  +. toppingCost(cheese, 0.1)
  /* some stuff involving tomato and bacon */;
};
Solution

After adding the toppingCost helper function, Burger.toPrice should look something like this:

re
let toPrice = ({onions, cheese, tomatoes, bacon, lettuce: _}) => {
  let toppingCost = (quantity, cost) => float_of_int(quantity) *. cost;

  15.  // base cost
  +. toppingCost(onions, 0.2)
  +. toppingCost(cheese, 0.1)
  +. (tomatoes ? 0.05 : 0.0)
  +. toppingCost(bacon, 0.5);
};
let toPrice = ({onions, cheese, tomatoes, bacon, lettuce: _}) => {
  let toppingCost = (quantity, cost) => float_of_int(quantity) *. cost;

  15.  // base cost
  +. toppingCost(onions, 0.2)
  +. toppingCost(cheese, 0.1)
  +. (tomatoes ? 0.05 : 0.0)
  +. toppingCost(bacon, 0.5);
};

4. Right now, the Item.Burger.toEmoji function shows more emojis than absolutely necessary. Refactor the multiple inner function in Burger.toEmoji so that it exhibits the following behavior:

itemItem.Burger.toEmoji(item)
Burger({lettuce: true, onions: 1, cheese: 1})🍔{🥬,🧅,🧀}
Burger({lettuce: true, onions: 0, cheese: 0})🍔{🥬}
Hint

Use a switch expression.

Solution

After refactoring the multiple helper function inside Item.Burger.toEmoji to avoid showing unnecessary emojis, it should look something like this:

re
let multiple = (emoji, count) =>
  switch (count) {
  | 0 => ""
  | 1 => emoji
  | count => Printf.sprintf({js|%s×%d|js}, emoji, count)
  };
let multiple = (emoji, count) =>
  switch (count) {
  | 0 => ""
  | 1 => emoji
  | count => Printf.sprintf({js|%s×%d|js}, emoji, count)
  };

Since the body of this function only consists of a switch expression, you can actually refactor the multiple function to use the fun syntax:

re
let multiple = emoji =>
  fun
  | 0 => ""
  | 1 => emoji
  | count => Printf.sprintf({js|%s×%d|js}, emoji, count);
let multiple = emoji =>
  fun
  | 0 => ""
  | 1 => emoji
  | count => Printf.sprintf({js|%s×%d|js}, emoji, count);

This might look a little strange, but in OCaml, all functions take exactly one argument. What looks like a two-argument function is actually a one-argument function that returns another one-argument function[2]. Manually adding a type annotation to the function makes it more clear that it’s entirely equivalent to the version that uses a switch expression:

re
let multiple: (string, int) => string =
  emoji =>
    fun
    | 0 => ""
    | 1 => emoji
    | count => Printf.sprintf({js|%s×%d|js}, emoji, count);
let multiple: (string, int) => string =
  emoji =>
    fun
    | 0 => ""
    | 1 => emoji
    | count => Printf.sprintf({js|%s×%d|js}, emoji, count);

View source code and demo for this chapter.



  1. Of course, it’s not just any kind of bacon. It’s unicorn bacon. By the way, the tomatoes are also magically delicious, because they’re grown in a special field fertilized by unicorn manure. ↩︎

  2. A simpler example that illustrates how function arguments work in OCaml would be:

    reason
    let add = (x, y, z) => x + y + z;
    // The above is an easier-to-read version of this:
    let explicitAdd = x => y => z => x + y + z;
    let add = (x, y, z) => x + y + z;
    // The above is an easier-to-read version of this:
    let explicitAdd = x => y => z => x + y + z;

    See this playground snippet for an extended example and the official OCaml docs for more details. ↩︎