Burger Discounts
International Burger Day falls on Tuesday of next week, so Madame Jellobutter decides to celebrate it by running the classic “buy 2 burgers get 1 free” promotion. She clearly lays out the rules of this promotion:
Given an order comprising multiple items, find the price of the second most expensive burger. Subtract this number from the pre-tax total of the order.
Add a new file src/order-confirmation/Discount.re:
// Buy 2 burgers, get 1 free
let getFreeBurger = (items: array(Item.t)) => {
let burgers =
items
|> Js.Array.sortInPlaceWith(~f=(item1, item2) =>
- compare(Item.toPrice(item1), Item.toPrice(item2))
)
|> Js.Array.filter(~f=item =>
switch (item) {
| Burger(_) => true
| Sandwich(_)
| Hotdog => false
}
);
switch (burgers) {
| [|Burger(_), Burger(cheaperBurger)|] =>
Some(Item.Burger.toPrice(cheaperBurger))
| _ => None
};
};// Buy 2 burgers, get 1 free
let getFreeBurger = (items: array(Item.t)) => {
let burgers =
items
|> Js.Array.sortInPlaceWith(~f=(item1, item2) =>
- compare(Item.toPrice(item1), Item.toPrice(item2))
)
|> Js.Array.filter(~f=item =>
switch (item) {
| Burger(_) => true
| Sandwich(_)
| Hotdog => false
}
);
switch (burgers) {
| [|Burger(_), Burger(cheaperBurger)|] =>
Some(Item.Burger.toPrice(cheaperBurger))
| _ => None
};
};Discount.getFreeBurger function
The new function Discount.getFreeBurger takes an array of items, finds the second-most-expensive burger, and returns its price encased in Some. If there is no second burger, it returns None.
The functions used in Discount.getFreeBurger are:
- Js.Array.sortInPlaceWith takes a callback function
~fwith type signature('a, 'a) => int(accept two arguments of the same type and returnint). It’s used to sort the items by price (highest to lowest). - Stdlib.compare has type signature
('a, 'a) => int. It’s a polymorphic function capable of comparing many types, includingbool,int,string, etc. Note that you can always just writecompareinstead ofStdlib.compare, because theStdlibmodule is always opened by default. - Js.Array.filter takes a callback function
~fwith type signature'a => bool. It’s used to make sure all items in theburgersarray are all burgers.
At the moment, this code doesn’t compile, and that’s not the only thing wrong with it, but we’ll address each issue in due course.
Limitation of type inference
You should see this compilation error:
File "src/order-confirmation/Discount.re", line 9, characters 11-17:
9 | | Burger(_) => true
^^^^^^
Error: Unbound constructor BurgerFile "src/order-confirmation/Discount.re", line 9, characters 11-17:
9 | | Burger(_) => true
^^^^^^
Error: Unbound constructor BurgerOCaml’s type inference isn’t able to figure out that the callback function passed to Js.Array.filter receives an argument of Item.t, so it doesn’t know where the Burger constructor is coming from. But why does type inference work in the callback to Js.Array.sortInPlaceWith?
|> Js.Array.sortInPlaceWith(~f=(item1, item2) =>
- compare(Item.toPrice(item1), Item.toPrice(item2))
)|> Js.Array.sortInPlaceWith(~f=(item1, item2) =>
- compare(Item.toPrice(item1), Item.toPrice(item2))
)The reason is that Item.toPrice is invoked inside this callback, and its type signature is already known to be Item.t => float. So type inference can figure out that item1 and item2 must both be of type Item.t, because Item.toPrice can only accept an argument of type Item.t.
Type annotate callback argument
There aren’t any function invocations inside the callback to Js.Array.filter, so we can help the compiler out by type annotating the item argument:
|> Js.Array.filter(~f=(item: Item.t) =>
switch (item) {
| Burger(_) => true
| Sandwich(_)
| Hotdog => false
}
);|> Js.Array.filter(~f=(item: Item.t) =>
switch (item) {
| Burger(_) => true
| Sandwich(_)
| Hotdog => false
}
);Use full name
Explicit type annotation always works, but sometimes it’s enough to just give the compiler a hint. For example, we can use full names[1] for the constructors inside the switch expression:
|> Js.Array.filter(~f=item =>
switch (item) {
| Item.Burger(_) => true
| Item.Sandwich(_)
| Item.Hotdog => false
}
);|> Js.Array.filter(~f=item =>
switch (item) {
| Item.Burger(_) => true
| Item.Sandwich(_)
| Item.Hotdog => false
}
);Because Item.Burger is a constructor of the Item.t variant type, item must have type Item.t. For the sake of convenience, you don’t need to use full names in the second branch of the switch expression—OCaml is smart enough to infer which module the Sandwich and Hotdog constructors belong to.
|> Js.Array.filter(~f=item =>
switch (item) {
| Item.Burger(_) => true
| Sandwich(_)
| Hotdog => false
}
);|> Js.Array.filter(~f=item =>
switch (item) {
| Item.Burger(_) => true
| Sandwich(_)
| Hotdog => false
}
);By using the full name of the Burger constructor, we can now easily refactor the callback function to use the fun syntax:
|> Js.Array.filter(
~f=
fun
| Item.Burger(_) => true
| Sandwich(_)
| Hotdog => false,
);|> Js.Array.filter(
~f=
fun
| Item.Burger(_) => true
| Sandwich(_)
| Hotdog => false,
);Add new tests
Add some tests in new file src/order-confirmation/DiscountTests.re:
open Fest;
test("0 burgers, no discount", () =>
expect
|> equal(
Discount.getFreeBurger([|
Hotdog,
Sandwich(Ham),
Sandwich(Turducken),
|]),
None,
)
);
test("1 burger, no discount", () =>
expect
|> equal(
Discount.getFreeBurger([|
Hotdog,
Sandwich(Ham),
Burger({
lettuce: false,
onions: 0,
cheese: 0,
tomatoes: false,
bacon: 0,
}),
|]),
None,
)
);
test("2 burgers of same price, discount", () =>
expect
|> equal(
Discount.getFreeBurger([|
Hotdog,
Burger({
lettuce: false,
onions: 0,
cheese: 0,
tomatoes: false,
bacon: 0,
}),
Sandwich(Ham),
Burger({
lettuce: false,
onions: 0,
cheese: 0,
tomatoes: false,
bacon: 0,
}),
|]),
Some(15.),
)
);open Fest;
test("0 burgers, no discount", () =>
expect
|> equal(
Discount.getFreeBurger([|
Hotdog,
Sandwich(Ham),
Sandwich(Turducken),
|]),
None,
)
);
test("1 burger, no discount", () =>
expect
|> equal(
Discount.getFreeBurger([|
Hotdog,
Sandwich(Ham),
Burger({
lettuce: false,
onions: 0,
cheese: 0,
tomatoes: false,
bacon: 0,
}),
|]),
None,
)
);
test("2 burgers of same price, discount", () =>
expect
|> equal(
Discount.getFreeBurger([|
Hotdog,
Burger({
lettuce: false,
onions: 0,
cheese: 0,
tomatoes: false,
bacon: 0,
}),
Sandwich(Ham),
Burger({
lettuce: false,
onions: 0,
cheese: 0,
tomatoes: false,
bacon: 0,
}),
|]),
Some(15.),
)
);To run these tests, add a new cram test to src/order-confirmation/tests.t:
Discount tests
$ node ./output/src/order-confirmation/DiscountTests.mjs | sed '/duration_ms/d'Discount tests
$ node ./output/src/order-confirmation/DiscountTests.mjs | sed '/duration_ms/d'Run npm run test:watch to see that the unit tests pass, then run npm promote to make the cram test pass.
Records are immutable
It’s unnecessary to fully write out every burger record in the tests. Define a burger record at the very top of DiscountTests:
let burger: Item.Burger.t = {
lettuce: false,
onions: 0,
cheese: 0,
tomatoes: false,
bacon: 0,
};let burger: Item.Burger.t = {
lettuce: false,
onions: 0,
cheese: 0,
tomatoes: false,
bacon: 0,
};Then refactor the second and third tests to use this record:
test("1 burger, no discount", () =>
expect
|> equal(
Discount.getFreeBurger([|Hotdog, Sandwich(Ham), Burger(burger)|]),
None,
)
);
test("2 burgers of same price, discount", () =>
expect
|> equal(
Discount.getFreeBurger([|
Hotdog,
Burger(burger),
Sandwich(Ham),
Burger(burger),
|]),
Some(15.),
)
);test("1 burger, no discount", () =>
expect
|> equal(
Discount.getFreeBurger([|Hotdog, Sandwich(Ham), Burger(burger)|]),
None,
)
);
test("2 burgers of same price, discount", () =>
expect
|> equal(
Discount.getFreeBurger([|
Hotdog,
Burger(burger),
Sandwich(Ham),
Burger(burger),
|]),
Some(15.),
)
);It’s safe to reuse a single record this way because records are immutable. You can pass a record to any function and never worry that its fields might be changed by that function.
Record spread syntax
Add a new test to DiscountTests:
test("2 burgers of different price, discount of cheaper one", () =>
expect
|> equal(
Discount.getFreeBurger([|
Hotdog,
Burger({
...burger,
tomatoes: true,
}), // 15.05
Sandwich(Ham),
Burger({
...burger,
bacon: 2,
}) // 16.00
|]),
Some(15.05),
)
);test("2 burgers of different price, discount of cheaper one", () =>
expect
|> equal(
Discount.getFreeBurger([|
Hotdog,
Burger({
...burger,
tomatoes: true,
}), // 15.05
Sandwich(Ham),
Burger({
...burger,
bacon: 2,
}) // 16.00
|]),
Some(15.05),
)
);Again, we’re reusing the burger record, but this time, we use record spread syntax to make copies of the burger record that have slightly different field values. For example,
{...burger, tomatoes: true}{...burger, tomatoes: true}means to make a copy of burger but with tomatoes set to true. It’s just a shorter and more convenient way to write this:
{
lettuce: burger.lettuce,
onions: burger.onions,
cheese: burger.cheese,
bacon: burger.bacon,
tomatoes: true,
}{
lettuce: burger.lettuce,
onions: burger.onions,
cheese: burger.cheese,
bacon: burger.bacon,
tomatoes: true,
}Ignoring function return values
Add another test to DiscountTests that checks whether Discount.getFreeBurger modifies the array passed to it:
test("Input array isn't changed", () => {
let items = [|
Item.Hotdog,
Burger({...burger, tomatoes: true}),
Sandwich(Ham),
Burger({...burger, bacon: 2}),
|];
Discount.getFreeBurger(items);
expect
|> deepEqual(
items,
[|
Item.Hotdog,
Burger({...burger, tomatoes: true}),
Sandwich(Ham),
Burger({...burger, bacon: 2}),
|],
);
});test("Input array isn't changed", () => {
let items = [|
Item.Hotdog,
Burger({...burger, tomatoes: true}),
Sandwich(Ham),
Burger({...burger, bacon: 2}),
|];
Discount.getFreeBurger(items);
expect
|> deepEqual(
items,
[|
Item.Hotdog,
Burger({...burger, tomatoes: true}),
Sandwich(Ham),
Burger({...burger, bacon: 2}),
|],
);
});You’ll get this compilation error:
File "src/order-confirmation/DiscountTests.re", line 65, characters 2-31:
65 | Discount.getFreeBurger(items);
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Error: This expression has type float option
but an expression was expected of type unit
because it is in the left-hand side of a sequenceFile "src/order-confirmation/DiscountTests.re", line 65, characters 2-31:
65 | Discount.getFreeBurger(items);
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Error: This expression has type float option
but an expression was expected of type unit
because it is in the left-hand side of a sequenceWhen you call a function in OCaml, you must use its return value, unless the return value is () (the unit value). However, inside this test, we are calling Discount.getFreeBurger to test its side effects, so the return value isn’t needed; as such, we can explicitly discard it by using Stdlib.ignore[2]:
Discount.getFreeBurger(items) |> ignore;Discount.getFreeBurger(items) |> ignore;Runtime representation of variant
After compilation succeeds, we find that the “Input array isn’t changed” unit test fails. Part of the output (cleaned up for readability) looks like this:
[
0,
{
TAG: 1,
_0: {
bacon: 0,
cheese: 0,
lettuce: false,
onions: 0,
tomatoes: true
}
},
{
TAG: 0,
_0: 1
},
{
TAG: 1,
_0: {
bacon: 2,
cheese: 0,
lettuce: false,
onions: 0,
tomatoes: false
}
}
][
0,
{
TAG: 1,
_0: {
bacon: 0,
cheese: 0,
lettuce: false,
onions: 0,
tomatoes: true
}
},
{
TAG: 0,
_0: 1
},
{
TAG: 1,
_0: {
bacon: 2,
cheese: 0,
lettuce: false,
onions: 0,
tomatoes: false
}
}
]This is how Melange maps the original OCaml values to their JavaScript runtime values:
| OCaml source | JavaScript runtime |
|---|---|
Item.Hotdog | 0 |
Sandwich(Ham) | {TAG: 0, _0: 1} |
Burger({...burger, tomatoes: true}) | {TAG: 1, _0: {bacon: 0, cheese: 0, lettuce: false, onions: 0, tomatoes: true}} |
A variant constructor without arguments, like Hotdog, gets turned into an integer. If the constructor has an argument, like Sandwich(Ham), then it’s turned into a record where the TAG field is an integer and the _0 field contains the argument. Records, like the one encased in the Burger constructor, are turned into JS objects. OCaml arrays, like the one that contains all the items, are turned into JS arrays.
WARNING
Variant constructors in the runtime don’t always have the TAG field. That field only appears when there’s more than one variant constructor with an argument. See Data types and runtime representation for more details.
Arrays are mutable
The “Input array isn’t changed” unit test fails because arrays in OCaml are mutable (just as in JavaScript) and the Discount.getFreeBurger function mutates its array argument. The easiest way to fix this is to swap the order of Js.Array.sortInPlaceWith and Js.Array.filter invocations:
items
|> Js.Array.filter(~f=item =>
switch (item) {
| Item.Burger(_) => true
| Sandwich(_)
| Hotdog => false
}
)
|> Js.Array.sortInPlaceWith(~f=(item1, item2) =>
- compare(Item.toPrice(item1), Item.toPrice(item2))
)items
|> Js.Array.filter(~f=item =>
switch (item) {
| Item.Burger(_) => true
| Sandwich(_)
| Hotdog => false
}
)
|> Js.Array.sortInPlaceWith(~f=(item1, item2) =>
- compare(Item.toPrice(item1), Item.toPrice(item2))
)Although sorting still happens in-place, the array being sorted is a new one created by Js.Array.filter (the array containing only burgers), not the original input array.
Runtime representation of option
We need to add one more test to check that Discount.getFreeBurger works when there are more than two burgers:
test("3 burgers of different price, return Some(15.15)", () =>
expect
|> equal(
Discount.getFreeBurger([|
Burger(burger), // 15
Hotdog,
Burger({
...burger,
tomatoes: true,
cheese: 1,
}), // 15.15
Sandwich(Ham),
Burger({
...burger,
bacon: 2,
}) // 16.00
|]),
Some(15.15),
)
);test("3 burgers of different price, return Some(15.15)", () =>
expect
|> equal(
Discount.getFreeBurger([|
Burger(burger), // 15
Hotdog,
Burger({
...burger,
tomatoes: true,
cheese: 1,
}), // 15.15
Sandwich(Ham),
Burger({
...burger,
bacon: 2,
}) // 16.00
|]),
Some(15.15),
)
);This test fails, with the key part of the output being:
+ Expected values to be strictly equal:
+ + actual - expected
+
+ + undefined
+ - 15.15+ Expected values to be strictly equal:
+ + actual - expected
+
+ + undefined
+ - 15.15Recall that Discount.getFreeBurger has the return type option(float). This is how Melange maps option(float) values to the JavaScript runtime[3]:
| OCaml source | JavaScript runtime |
|---|---|
None | undefined |
Some(15.15) | 15.15 |
So Node test runner is basically telling you that None was returned, but Some(15.15) was expected.
Pattern matching on arrays
The test is failing because the current “success” pattern match only accounts for a two-element array:
switch (burgers) {
| [|Burger(_), Burger(cheaperBurger)|] =>
Some(Item.Burger.toPrice(cheaperBurger))
| _ => Noneswitch (burgers) {
| [|Burger(_), Burger(cheaperBurger)|] =>
Some(Item.Burger.toPrice(cheaperBurger))
| _ => NoneOCaml only allows you to pattern match on arrays of fixed length, so to fix this, we must instead match on a tuple of the first and second elements of the array:
switch (burgers[0], burgers[1]) {
| (Burger(_), Burger(cheaperBurger)) =>
Some(Item.Burger.toPrice(cheaperBurger))
| _ => None
};switch (burgers[0], burgers[1]) {
| (Burger(_), Burger(cheaperBurger)) =>
Some(Item.Burger.toPrice(cheaperBurger))
| _ => None
};Array access is unsafe
The first and second tests now fail due to index out of bounds errors (since they work on arrays of length 0 and 1, respectively). Array access in OCaml is unsafe by default, so the simplest fix is to check the length of the array before using the switch expression:
Js.Array.length(burgers) < 2
? None
: (
switch (burgers[0], burgers[1]) {
| (Burger(_), Burger(cheaperBurger)) =>
Some(Item.Burger.toPrice(cheaperBurger))
| _ => None
}
);Js.Array.length(burgers) < 2
? None
: (
switch (burgers[0], burgers[1]) {
| (Burger(_), Burger(cheaperBurger)) =>
Some(Item.Burger.toPrice(cheaperBurger))
| _ => None
}
);An alternative approach is to catch the exception that gets raised using an exception branch inside the switch expression:
switch (burgers[0], burgers[1]) {
| exception _ => None
| (Burger(_), Burger(cheaperBurger)) =>
Some(Item.Burger.toPrice(cheaperBurger))
| _ => None
};switch (burgers[0], burgers[1]) {
| exception _ => None
| (Burger(_), Burger(cheaperBurger)) =>
Some(Item.Burger.toPrice(cheaperBurger))
| _ => None
};Array.get array access function
In OCaml, the array access operator [] is just a function call. That is, burger[0] is completely equivalent to Array.get(burger, 0).
Since the Stdlib module is opened by default, the Stdlib.Array.get function is used for Array.get, but it’s possible to override this by defining our own Array module. Add a new file src/order-confirmation/Array.re:
// Safe array access function
let get: (array('a), int) => option('a) =
(array, index) =>
switch (index) {
| index when index < 0 || index >= Js.Array.length(array) => None
| index => Some(Stdlib.Array.get(array, index))
};// Safe array access function
let get: (array('a), int) => option('a) =
(array, index) =>
switch (index) {
| index when index < 0 || index >= Js.Array.length(array) => None
| index => Some(Stdlib.Array.get(array, index))
};This function returns None if the index is out of bounds; otherwise it returns Some(Stdlib.Array.get(array, index)), i.e. the element at index encased by Some.
Introducing our own Array module triggers a new compilation error:
File "src/order-confirmation/Discount.re", line 18, characters 5-11:
18 | | (Burger(_), Burger(cheaperBurger)) =>
^^^^^^
Error: This variant pattern is expected to have type Item.t option
There is no constructor Burger within type optionFile "src/order-confirmation/Discount.re", line 18, characters 5-11:
18 | | (Burger(_), Burger(cheaperBurger)) =>
^^^^^^
Error: This variant pattern is expected to have type Item.t option
There is no constructor Burger within type optionThe “success” branch must now include Some in the pattern match:
switch (burgers[0], burgers[1]) {
| (Some(Burger(_)), Some(Burger(cheaperBurger))) =>
Some(Item.Burger.toPrice(cheaperBurger))
| _ => None
};switch (burgers[0], burgers[1]) {
| (Some(Burger(_)), Some(Burger(cheaperBurger))) =>
Some(Item.Burger.toPrice(cheaperBurger))
| _ => None
};Note that we no longer need to check the array length or catch an exception. Our new Array.get function is a safe function that returns None instead of raising an exception.
Your code should now compile and all unit tests should pass. If you haven’t done so already, run npm run promote to promote the latest test output to become the expected test output inside tests.t.
Variant constructors are not functions
What happens if you to try to rewrite Some(String.length("foobar")) to "foobar" |> String.length |> Some?
You’ll get a compilation error:
Error This expression should not be a constructor, the expected type is int -> 'aError This expression should not be a constructor, the expected type is int -> 'aVariant constructors like Some are not functions, so they can’t be used with the pipe last (|>) operator. If you have a long chain of function invocations but you need to return a variant at the end, consider using an extra variable, e.g.
let result =
name
|> String.split_on_char(' ')
|> List.map(String.map(c => c |> Char.code |> (+)(1) |> Char.chr))
|> String.concat(" ")
|> String.cat("Hello, ");
Some(result);let result =
name
|> String.split_on_char(' ')
|> List.map(String.map(c => c |> Char.code |> (+)(1) |> Char.chr))
|> String.concat(" ")
|> String.cat("Hello, ");
Some(result);See full example on Melange Playground.
When the variant type you’re using is option, though, you can still chain functions by using the Option.some helper function:
name
|> String.split_on_char(' ')
|> List.map(String.map(c => c |> Char.code |> (+)(1) |> Char.chr))
|> String.concat(" ")
|> String.cat("Hello, ")
|> Option.some;name
|> String.split_on_char(' ')
|> List.map(String.map(c => c |> Char.code |> (+)(1) |> Char.chr))
|> String.concat(" ")
|> String.cat("Hello, ")
|> Option.some;Nice, you’ve implemented the burger discount, and you also understand more about arrays in OCaml. In the next chapter, you’ll implement the same discount logic using lists, which are a better fit for this problem.
Overview
- Type inference is less effective inside functions that don’t call other functions. In those cases, you can give the compiler more information:
- Type annotate the function arguments
- Use the full name for a value used inside the function
- The
Stdlibmodule is opened by default - Records are immutable
- Use record spread syntax to copy a record while changing some fields on the copied record, e.g.
{...burger, lettuce: true, onions: 3} - OCaml doesn’t allow you to ignore the return value of functions (unless the value is
()), so you can useStdlib.ignoreto explicitly discard return values - Runtime representations of common data types:
- Variant constructor without argument → integer
- Variant constructor with argument → JavaScript object
- Record → JavaScript object
- Array → JavaScript array
None→undefinedSome(value)→value
- Variant constructors are not functions
- Array facts:
- Arrays are mutable, just like in JavaScript
- You can pattern match on arrays of fixed length
- Array access is unsafe by default
- What looks like operator usage in
array[index]is actually just a call toArray.get(array, index) - You can create your own
Arraymodule to override the behavior ofArray.get
Exercises
1. Discount.getFreeBurger can be improved a bit. In particular, there’s no need to match on the Burger constructor when non-burger items have already been filtered out. Refactor the function so that the “success” pattern match looks like this:
| Some(cheaperPrice) => Some(cheaperPrice)| Some(cheaperPrice) => Some(cheaperPrice)Also refactor the “failure” pattern match so there’s no wildcard.
Hint
Use Js.Array.map
Solution
// Buy 2 burgers, get 1 free
let getFreeBurger = (items: array(Item.t)) => {
let prices =
items
|> Js.Array.filter(~f=item =>
switch (item) {
| Item.Burger(_) => true
| Sandwich(_)
| Hotdog => false
}
)
|> Js.Array.map(~f=Item.toPrice)
|> Js.Array.sortInPlaceWith(~f=(x, y) => - compare(x, y));
switch (prices[1]) {
| None => None
| Some(cheaperPrice) => Some(cheaperPrice)
};
};// Buy 2 burgers, get 1 free
let getFreeBurger = (items: array(Item.t)) => {
let prices =
items
|> Js.Array.filter(~f=item =>
switch (item) {
| Item.Burger(_) => true
| Sandwich(_)
| Hotdog => false
}
)
|> Js.Array.map(~f=Item.toPrice)
|> Js.Array.sortInPlaceWith(~f=(x, y) => - compare(x, y));
switch (prices[1]) {
| None => None
| Some(cheaperPrice) => Some(cheaperPrice)
};
};Since the chain of function invocations now results in an array of floats, we rename the variable from burgers to prices. We only need to match on the second element of prices because if it exists, then a first element must also exist (but we don’t need to know its value).
2. Discount.getFreeBurger can still be improved. Refactor it to remove the switch expression entirely.
Solution
// Buy 2 burgers, get 1 free
let getFreeBurger = (items: array(Item.t)) => {
let prices =
items
|> Js.Array.filter(~f=item =>
switch (item) {
| Item.Burger(_) => true
| Sandwich(_)
| Hotdog => false
}
)
|> Js.Array.map(~f=Item.toPrice)
|> Js.Array.sortInPlaceWith(~f=(x, y) => - compare(x, y));
prices[1];
};// Buy 2 burgers, get 1 free
let getFreeBurger = (items: array(Item.t)) => {
let prices =
items
|> Js.Array.filter(~f=item =>
switch (item) {
| Item.Burger(_) => true
| Sandwich(_)
| Hotdog => false
}
)
|> Js.Array.map(~f=Item.toPrice)
|> Js.Array.sortInPlaceWith(~f=(x, y) => - compare(x, y));
prices[1];
};3. Add a new function Discount.getHalfOff that gives you a discount of half off the entire meal if there’s at least one burger that has one of every topping. Add these new tests to DiscountTests and make sure they pass:
test("No burger has 1 of every topping, return None", () =>
expect
|> equal(
Discount.getHalfOff([|
Hotdog,
Sandwich(Portabello),
Burger({
lettuce: true,
tomatoes: true,
cheese: 1,
onions: 1,
bacon: 0,
}),
|]),
None,
)
);
test("One burger has 1 of every topping, return Some", () => {
let items = [|
Item.Hotdog,
Sandwich(Portabello),
Burger({
lettuce: true,
tomatoes: true,
cheese: 1,
onions: 1,
bacon: 1,
}),
|];
expect
|> equal(
Discount.getHalfOff(items),
{
// Don't use hardcoded value since Item.toPrice is non-deterministic
let sum =
items
|> Js.Array.map(~f=Item.toPrice)
|> Js.Array.reduce(~init=0.0, ~f=(+.));
Some(sum /. 2.0);
},
);
});test("No burger has 1 of every topping, return None", () =>
expect
|> equal(
Discount.getHalfOff([|
Hotdog,
Sandwich(Portabello),
Burger({
lettuce: true,
tomatoes: true,
cheese: 1,
onions: 1,
bacon: 0,
}),
|]),
None,
)
);
test("One burger has 1 of every topping, return Some", () => {
let items = [|
Item.Hotdog,
Sandwich(Portabello),
Burger({
lettuce: true,
tomatoes: true,
cheese: 1,
onions: 1,
bacon: 1,
}),
|];
expect
|> equal(
Discount.getHalfOff(items),
{
// Don't use hardcoded value since Item.toPrice is non-deterministic
let sum =
items
|> Js.Array.map(~f=Item.toPrice)
|> Js.Array.reduce(~init=0.0, ~f=(+.));
Some(sum /. 2.0);
},
);
});Hint
Use Js.Array.some
Solution
// Buy 1+ burger with 1 of every topping, get half off
let getHalfOff = (items: array(Item.t)) => {
let meetsCondition =
items
|> Js.Array.some(
~f=
fun
| Item.Burger({
lettuce: true,
tomatoes: true,
onions: 1,
cheese: 1,
bacon: 1,
}) =>
true
| Burger(_)
| Sandwich(_)
| Hotdog => false,
);
switch (meetsCondition) {
| false => None
| true =>
let total =
items
|> Js.Array.reduce(~init=0.0, ~f=(total, item) =>
total +. Item.toPrice(item)
);
Some(total /. 2.0);
};
};// Buy 1+ burger with 1 of every topping, get half off
let getHalfOff = (items: array(Item.t)) => {
let meetsCondition =
items
|> Js.Array.some(
~f=
fun
| Item.Burger({
lettuce: true,
tomatoes: true,
onions: 1,
cheese: 1,
bacon: 1,
}) =>
true
| Burger(_)
| Sandwich(_)
| Hotdog => false,
);
switch (meetsCondition) {
| false => None
| true =>
let total =
items
|> Js.Array.reduce(~init=0.0, ~f=(total, item) =>
total +. Item.toPrice(item)
);
Some(total /. 2.0);
};
};4. Update Discount.getHalfOff so that it returns a discount of one half off the entire meal if there’s at least one burger that has at least one of every topping. While you’re at it, update the relevant tests in DiscountTests:
module HalfOff = {
test("No burger has 1+ of every topping, return None", () =>
expect
|> equal(
Discount.getHalfOff([|
Hotdog,
Sandwich(Portabello),
Burger({
lettuce: true,
tomatoes: true,
cheese: 1,
onions: 1,
bacon: 0,
}),
|]),
None,
)
);
test("One burger has 1+ of every topping, return Some", () => {
let items = [|
Item.Hotdog,
Sandwich(Portabello),
Burger({
lettuce: true,
tomatoes: true,
cheese: 1,
onions: 1,
bacon: 2,
}),
|];
expect
|> equal(
Discount.getHalfOff(items),
{
// Don't use hardcoded value since Item.toPrice is non-deterministic
let sum =
items
|> Js.Array.map(~f=Item.toPrice)
|> Js.Array.reduce(~init=0.0, ~f=(+.));
Some(sum /. 2.0);
},
);
});
};module HalfOff = {
test("No burger has 1+ of every topping, return None", () =>
expect
|> equal(
Discount.getHalfOff([|
Hotdog,
Sandwich(Portabello),
Burger({
lettuce: true,
tomatoes: true,
cheese: 1,
onions: 1,
bacon: 0,
}),
|]),
None,
)
);
test("One burger has 1+ of every topping, return Some", () => {
let items = [|
Item.Hotdog,
Sandwich(Portabello),
Burger({
lettuce: true,
tomatoes: true,
cheese: 1,
onions: 1,
bacon: 2,
}),
|];
expect
|> equal(
Discount.getHalfOff(items),
{
// Don't use hardcoded value since Item.toPrice is non-deterministic
let sum =
items
|> Js.Array.map(~f=Item.toPrice)
|> Js.Array.reduce(~init=0.0, ~f=(+.));
Some(sum /. 2.0);
},
);
});
};Note the use of submodule HalfOff to group these two tests together.
Hint
Use when guard
Solution
// Buy 1+ burger with 1+ of every topping, get half off
let getHalfOff = (items: array(Item.t)) => {
let meetsCondition =
items
|> Js.Array.some(
~f=
fun
| Item.Burger({
lettuce: true,
tomatoes: true,
onions,
cheese,
bacon,
})
when onions > 0 && cheese > 0 && bacon > 0 =>
true
| Burger(_)
| Sandwich(_)
| Hotdog => false,
);
switch (meetsCondition) {
| false => None
| true =>
let total =
items
|> Js.Array.reduce(~init=0.0, ~f=(total, item) =>
total +. Item.toPrice(item)
);
Some(total /. 2.0);
};
};// Buy 1+ burger with 1+ of every topping, get half off
let getHalfOff = (items: array(Item.t)) => {
let meetsCondition =
items
|> Js.Array.some(
~f=
fun
| Item.Burger({
lettuce: true,
tomatoes: true,
onions,
cheese,
bacon,
})
when onions > 0 && cheese > 0 && bacon > 0 =>
true
| Burger(_)
| Sandwich(_)
| Hotdog => false,
);
switch (meetsCondition) {
| false => None
| true =>
let total =
items
|> Js.Array.reduce(~init=0.0, ~f=(total, item) =>
total +. Item.toPrice(item)
);
Some(total /. 2.0);
};
};View source code and demo for this chapter.
The official term for something like
Item.Burger(module name followed by value name) is access path, but this term isn’t widely used. ↩︎Another valid way to discard the return value of a function is:
reasonlet _: option(float) = Discount.getFreeBurger(items);let _: option(float) = Discount.getFreeBurger(items);This works, but
ignoreis more explicit and therefore the recommended approach. ↩︎Technically
optionis a variant, but Melange treats them as a special case—optionvalues are never represented as JS objects in the runtime. ↩︎