Better Sandwiches
Cafe Emoji is a stunning success, despite the fact that all sandwiches have the same filling of Mystery Meat™️. In order to offer her diners more variety, Madame Jellobutter decides to add multiple types of sandwich to her menu.
Currently, your src/order-confirmation/Item.re
looks something like this:
type t =
| Sandwich
| Burger
| Hotdog;
let toPrice =
fun
| Sandwich => 10.
| Burger => 15.
| Hotdog => 5.;
let toEmoji =
fun
| Sandwich => {js|🥪|js}
| Burger => {js|🍔|js}
| Hotdog => {js|🌭|js};
type t =
| Sandwich
| Burger
| Hotdog;
let toPrice =
fun
| Sandwich => 10.
| Burger => 15.
| Hotdog => 5.;
let toEmoji =
fun
| Sandwich => {js|🥪|js}
| Burger => {js|🍔|js}
| Hotdog => {js|🌭|js};
sandwich
variant type
To start, add a new variant type sandwich
and then add that type as the argument for the Sandwich
constructor of Item.t
:
type sandwich =
| Portabello
| Ham
| Unicorn;
type t =
| Sandwich(sandwich)
| Burger
| Hotdog;
type sandwich =
| Portabello
| Ham
| Unicorn;
type t =
| Sandwich(sandwich)
| Burger
| Hotdog;
You’ll immediately get a compilation error inside the Item.toPrice
function:
File "src/order-confirmation/Item.re", line 13, characters 2-12:
13 | | Sandwich => 10.
^^^^^^^^
Error: The constructor Sandwich expects 1 argument(s),
but is applied here to 0 argument(s)
File "src/order-confirmation/Item.re", line 13, characters 2-12:
13 | | Sandwich => 10.
^^^^^^^^
Error: The constructor Sandwich expects 1 argument(s),
but is applied here to 0 argument(s)
This is easy to fix by adding a sandwich
argument in the Sandwich
branches for both Item.toPrice
and Item.toEmoji
:
let toPrice =
fun
| Sandwich => 10.
| Sandwich(sandwich) => 10.
| Burger => 15.
| Hotdog => 5.;
let toEmoji =
fun
| Sandwich => {js|🥪|js}
| Sandwich(sandwich) => {js|🥪|js}
| Burger => {js|🍔|js}
| Hotdog => {js|🌭|js};
let toPrice =
fun
| Sandwich => 10.
| Sandwich(sandwich) => 10.
| Burger => 15.
| Hotdog => 5.;
let toEmoji =
fun
| Sandwich => {js|🥪|js}
| Sandwich(sandwich) => {js|🥪|js}
| Burger => {js|🍔|js}
| Hotdog => {js|🌭|js};
Now there’s a different error:
File "src/order-confirmation/Item.re", line 13, characters 13-20:
13 | | Sandwich(sandwich) => 10.
^^^^^^^^
Error (warning 27 [unused-var-strict]): unused variable sandwich.
File "src/order-confirmation/Item.re", line 13, characters 13-20:
13 | | Sandwich(sandwich) => 10.
^^^^^^^^
Error (warning 27 [unused-var-strict]): unused variable sandwich.
A valid use of wildcard
OCaml demands that we use variables we declare[1], unless they begin with underscore. So we can silence the error by renaming sandwich
to _sandwich
:
| Sandwich(_sandwich) => 10.
| Sandwich(_sandwich) => 10.
However, this essentially turns _sandwich
into a wildcard, which is generally a bad idea. The reality, though, is that the warning against using wildcards is more a guideline than a hard and fast rule. In this case, it’s useful to temporarily use wildcards to silence the compiler errors while we fill in the missing logic for Item.toPrice
and Item.toEmoji
. Still, when the wildcard is meant to be temporary, we can give it a more attention-grabbing name so it’s less likely to become permanent:
| Sandwich(_todo_please_use_this_sandwich) => 10.
| Sandwich(_todo_please_use_this_sandwich) => 10.
Update Item.toPrice
function
The price of a sandwich should vary depending on the type of sandwich received by Item.toPrice
[2]:
let toPrice =
fun
| Sandwich(sandwich) =>
switch (sandwich) {
| Portabello => 7.
| Ham => 10.
| Unicorn => 80.
}
| Burger => 15.
| Hotdog => 5.;
let toPrice =
fun
| Sandwich(sandwich) =>
switch (sandwich) {
| Portabello => 7.
| Ham => 10.
| Unicorn => 80.
}
| Burger => 15.
| Hotdog => 5.;
Actually, there’s no need for the nested switch, as OCaml allows you to pattern match into nested structures:
let toPrice =
fun
| Sandwich(Portabello) => 7.
| Sandwich(Ham) => 10.
| Sandwich(Unicorn) => 80.
| Burger => 15.
| Hotdog => 5.;
let toPrice =
fun
| Sandwich(Portabello) => 7.
| Sandwich(Ham) => 10.
| Sandwich(Unicorn) => 80.
| Burger => 15.
| Hotdog => 5.;
Combining branches in switch
Madame Jellobutter decides to upgrade her portabellos from canned mushrooms to artisanal mushrooms lovingly grown by mustachioed hipsters on a local farm, which increases the price of portabello sandwiches to $10. Now portabello and ham sandwiches are the same price, so you update the Item.toPrice
function accordingly:
let toPrice =
fun
| Sandwich(Portabello) => 10.
| Sandwich(Ham) => 10.
| Sandwich(Unicorn) => 80.
| Burger => 15.
| Hotdog => 5.;
let toPrice =
fun
| Sandwich(Portabello) => 10.
| Sandwich(Ham) => 10.
| Sandwich(Unicorn) => 80.
| Burger => 15.
| Hotdog => 5.;
We can combine the portabello and ham branches because they return the same value:
let toPrice =
fun
| Sandwich(Portabello)
| Sandwich(Ham) => 10.
| Sandwich(Unicorn) => 80.
| Burger => 15.
| Hotdog => 5.;
let toPrice =
fun
| Sandwich(Portabello)
| Sandwich(Ham) => 10.
| Sandwich(Unicorn) => 80.
| Burger => 15.
| Hotdog => 5.;
Since the outer constructor Sandwich
is the same in both branches, we can combine them into a single branch:
let toPrice =
fun
| Sandwich(Portabello | Ham) => 10.
| Sandwich(Unicorn) => 80.
| Burger => 15.
| Hotdog => 5.;
let toPrice =
fun
| Sandwich(Portabello | Ham) => 10.
| Sandwich(Unicorn) => 80.
| Burger => 15.
| Hotdog => 5.;
The power of pattern matching gives us both clarity and brevity. This is the OCaml Way!
Update Item.toEmoji
function
Item.toEmoji
should now return a combination of two emojis when it gets a Sandwich(_)
argument:
Sandwich | Emoji |
---|---|
Portabello | 🥪(🍄) |
Ham | 🥪(🐷) |
Unicorn | 🥪(🦄) |
We can implement this using Js.Array.join
:
let toEmoji =
fun
| Sandwich(sandwich) =>
[|
{js|🥪(|js},
switch (sandwich) {
| Portabello => {js|🍄|js}
| Ham => {js|🐷|js}
| Unicorn => {js|🦄|js}
},
")",
|]
|> Js.Array.join(~sep="")
| Burger => {js|🍔|js}
| Hotdog => {js|🌭|js};
let toEmoji =
fun
| Sandwich(sandwich) =>
[|
{js|🥪(|js},
switch (sandwich) {
| Portabello => {js|🍄|js}
| Ham => {js|🐷|js}
| Unicorn => {js|🦄|js}
},
")",
|]
|> Js.Array.join(~sep="")
| Burger => {js|🍔|js}
| Hotdog => {js|🌭|js};
This touches on the issue of coding style for switch expressions. When possible, put shorter branches before longer branches:
let toEmoji =
fun
| Burger => {js|🍔|js}
| Hotdog => {js|🌭|js}
| Sandwich(sandwich) =>
[|
{js|🥪(|js},
switch (sandwich) {
| Portabello => {js|🍄|js}
| Ham => {js|🐷|js}
| Unicorn => {js|🦄|js}
},
")",
|]
|> Js.Array.join(~sep="");
let toEmoji =
fun
| Burger => {js|🍔|js}
| Hotdog => {js|🌭|js}
| Sandwich(sandwich) =>
[|
{js|🥪(|js},
switch (sandwich) {
| Portabello => {js|🍄|js}
| Ham => {js|🐷|js}
| Unicorn => {js|🦄|js}
},
")",
|]
|> Js.Array.join(~sep="");
{j||j}
quoted string literal
The approach using Js.Array.join
works, but readability could be improved by using a quoted string literal with the quoted string identifier j
:
let toEmoji =
fun
| Burger => {js|🍔|js}
| Hotdog => {js|🌭|js}
| Sandwich(sandwich) => {
let emoji =
switch (sandwich) {
| Portabello => {js|🍄|js}
| Ham => {js|🐷|js}
| Unicorn => {js|🦄|js}
};
{j|🥪($emoji)|j};
};
let toEmoji =
fun
| Burger => {js|🍔|js}
| Hotdog => {js|🌭|js}
| Sandwich(sandwich) => {
let emoji =
switch (sandwich) {
| Portabello => {js|🍄|js}
| Ham => {js|🐷|js}
| Unicorn => {js|🦄|js}
};
{j|🥪($emoji)|j};
};
{j||j}
quoted string literals are similar to template literals in JavaScript except that they can only accept variables, not arbitrary expressions.
Also note that unlike switch expressions, the fun
syntax does not accept multi-line expressions in branches unless you add {}
around them.
Printf.sprintf
The OCaml standard library also provides a type-safe way to do string interpolation in the form of the Printf.sprintf function:
let toEmoji =
fun
| Burger => {js|🍔|js}
| Hotdog => {js|🌭|js}
| Sandwich(sandwich) =>
Printf.sprintf(
{|🥪(%s)|},
switch (sandwich) {
| Portabello => {js|🍄|js}
| Ham => {js|🐷|js}
| Unicorn => {js|🦄|js}
},
);
let toEmoji =
fun
| Burger => {js|🍔|js}
| Hotdog => {js|🌭|js}
| Sandwich(sandwich) =>
Printf.sprintf(
{|🥪(%s)|},
switch (sandwich) {
| Portabello => {js|🍄|js}
| Ham => {js|🐷|js}
| Unicorn => {js|🦄|js}
},
);
Printf.sprintf
has a couple of advantages over {j||j}
quoted string literals:
- Since it’s just a function, you can pass expressions into it
- It supports conversion specifications like
%s
,%i
,%d
, etc which concisely handle basic string conversion logic for all primitive data types. This can often make your code shorter and easier to understand.
Bundling
An unfortunate downside of the Printf
module is that using it can add a surprising amount to your app’s production bundle size. Let’s generate a bundle to see just how much of a difference it makes. However, since this is a multi-page app, Vite won’t automatically build bundles for index.html
files that aren’t in the project’s root directory. You must add a little extra configuration to vite.config.js
:
import { resolve } from 'path'
import { defineConfig } from 'vite'
import { nodeResolve } from '@rollup/plugin-node-resolve'
export default defineConfig({
plugins: [nodeResolve()],
server: {
watch: {
ignored: ['**/_opam']
}
},
build: {
rollupOptions: {
input: {
order_confirmation: resolve(__dirname, 'src/order-confirmation/index.html'),
},
},
},
});
import { resolve } from 'path'
import { defineConfig } from 'vite'
import { nodeResolve } from '@rollup/plugin-node-resolve'
export default defineConfig({
plugins: [nodeResolve()],
server: {
watch: {
ignored: ['**/_opam']
}
},
build: {
rollupOptions: {
input: {
order_confirmation: resolve(__dirname, 'src/order-confirmation/index.html'),
},
},
},
});
Now run npm run bundle
again and you should see some output like this:
vite v5.0.11 building for production...
✓ 61 modules transformed.
dist/src/order_confirmation/index.html 0.35 kB │ gzip: 0.24 kB
dist/assets/order_confirmation-14ani3dg.css 0.27 kB │ gzip: 0.18 kB
dist/assets/order_confirmation-2_8a7RSk.js 190.13 kB │ gzip: 57.85 kB
✓ built in 1.20s
vite v5.0.11 building for production...
✓ 61 modules transformed.
dist/src/order_confirmation/index.html 0.35 kB │ gzip: 0.24 kB
dist/assets/order_confirmation-14ani3dg.css 0.27 kB │ gzip: 0.18 kB
dist/assets/order_confirmation-2_8a7RSk.js 190.13 kB │ gzip: 57.85 kB
✓ built in 1.20s
You can see that the JS bundle for the Order Confirmation app is 190 kB. If you change Item.toEmoji
back to use a {j||j}
quoted string literal, the bundle size will be 144 kB, about 46 kB smaller.
Frabjous! The order confirmation widget now supports more types of sandwiches. In the next chapter, we’ll see how to expand the options for burgers.
Overview
- Variant constructors can contain arguments
- OCaml’s pattern matching syntax makes your code more readable and concise
- You can match to any level of nesting:
| A(B(C)) => 42
- There are many options for combining branches in switch expressions, e.g.
| A => 42 | B => 42
becomes| A | B => 42
| A(B) => 42 | A(C) => 42
becomes| A(B | C) => 42
- You can match to any level of nesting:
- A valid use of wildcards in switch expressions is when you want to silence compiler errors while you fill in the logic for new branches in switch expressions
{j||j}
quoted string literals are similar to JavaScript’s template literals, but they are not type safe and should be avoided in production codePrint.sprintf
is a type-safe function for string interpolation- For Vite multi-page apps, you must add a little extra configuration to build bundles for
index.html
files not in the project’s root directory
Exercises
1. Madame Jellobutter wants to add another type of sandwich and she’s letting you decide what it is. Give it an appropriate price and find the most suitable emoji(s) for it.
Solution
Add a new Turducken
constructor to variant type sandwich
and update the Item.toPrice
and Item.toEmoji
functions accordingly:
type sandwich =
| Portabello
| Ham
| Unicorn
| Turducken;
type t =
| Sandwich(sandwich)
| Burger
| Hotdog;
let toPrice =
fun
| Sandwich(Portabello | Ham) => 10.
| Sandwich(Unicorn) => 80.
| Sandwich(Turducken) => 20.
| Burger => 15.
| Hotdog => 5.;
let toEmoji =
fun
| Burger => {js|🍔|js}
| Hotdog => {js|🌭|js}
| Sandwich(sandwich) =>
Printf.sprintf(
{js|🥪(%s)|js},
switch (sandwich) {
| Portabello => {js|🍄|js}
| Ham => {js|🐷|js}
| Unicorn => {js|🦄|js}
| Turducken => {js|🦃🦆🐓|js}
},
);
type sandwich =
| Portabello
| Ham
| Unicorn
| Turducken;
type t =
| Sandwich(sandwich)
| Burger
| Hotdog;
let toPrice =
fun
| Sandwich(Portabello | Ham) => 10.
| Sandwich(Unicorn) => 80.
| Sandwich(Turducken) => 20.
| Burger => 15.
| Hotdog => 5.;
let toEmoji =
fun
| Burger => {js|🍔|js}
| Hotdog => {js|🌭|js}
| Sandwich(sandwich) =>
Printf.sprintf(
{js|🥪(%s)|js},
switch (sandwich) {
| Portabello => {js|🍄|js}
| Ham => {js|🐷|js}
| Unicorn => {js|🦄|js}
| Turducken => {js|🦃🦆🐓|js}
},
);
Of course, you could’ve chosen a completely different sandwich, and that would be just fine.
2. Change the logic of Item.toPrice
so that the new sandwich you just added is cheaper or more expensive depending on the day of the week.
Hint 1
Look at the functions in the Js.Date module.
Hint 2
Use a when
guard in the switch expression.
Solution
In the alternate dimension where the turducken sandwich was added, Madame Jellobutter wants to do a promotion called “Turducken Tuesdays” where turducken sandwiches become half-price on Tuesdays. To support this generous discount, Item.toPrice
needs to be updated like so:
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 => 15.
| Hotdog => 5.
};
};
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 => 15.
| Hotdog => 5.
};
};
Note that the fun
syntax had to be abandoned, because the body of Item.toPrice
now consists of more than just a switch expression.
3. The following program uses {j||j}
quoted string literals for string interpolation:
let compute = (a, b) => (a +. 10.) /. b;
// Rewrite using Printf.sprintf, limit result to 3 decimal places
let result = compute(40., 47.) |> string_of_float;
Js.log({j|result to 3 decimal places = $(result)|j});
// Rewrite using Printf.sprintf
let player: Js.t({..}) = {
"name": "Wilbur",
"level": 9001234,
"immortal": false,
};
{
let name = player##name;
// bonus: use the flag that makes long numbers more readable
let level = player##level |> string_of_int;
let immortal = player##immortal |> string_of_bool;
Js.log({j|Player: name=$(name), level=$(level), immortal=$(immortal)|j});
};
let compute = (a, b) => (a +. 10.) /. b;
// Rewrite using Printf.sprintf, limit result to 3 decimal places
let result = compute(40., 47.) |> string_of_float;
Js.log({j|result to 3 decimal places = $(result)|j});
// Rewrite using Printf.sprintf
let player: Js.t({..}) = {
"name": "Wilbur",
"level": 9001234,
"immortal": false,
};
{
let name = player##name;
// bonus: use the flag that makes long numbers more readable
let level = player##level |> string_of_int;
let immortal = player##immortal |> string_of_bool;
Js.log({j|Player: name=$(name), level=$(level), immortal=$(immortal)|j});
};
Use Printf.sprintf
instead and improve the program as suggested by the comments. You can edit the program interactively in Melange Playground.
Hint
Consult the conversion specification documentation.
Solution
After replacing the {j||j}
quoted string literals with Printf.sprintf
and improving it a bit, you might end up with something like this.
View source code and demo for this chapter.
Technically, undeclared variables produce a warning and it’s possible to tell OCaml to not treat them as compilation errors. A common way to do this is via the built-in
warning
attribute. ↩︎You may be wondering why a unicorn sandwich costs $80. Well, that is just the price of ethically-sourced unicorn meat. All the “guests” at the Sanctuarium Unicornis are pampered daily with massages, horn polishings, and heirloom carrots. Once a week, a 7 kg chunk of fatty meat is extracted (essentially liposuction for unicorns), and the wound is healed almost instantly by the farm’s resident white mage. ↩︎