Skip to content

Celsius Converter Using Option

After all the changes we made in the last chapter, your CelsiusConverter.re might look something like this:

re
let getValueFromEvent = (evt): string => React.Event.Form.target(evt)##value;

let convert = celsius => 9.0 /. 5.0 *. celsius +. 32.0;

[@react.component]
let make = () => {
  let (celsius, setCelsius) = React.useState(() => "");

  <div>
    <input
      value=celsius
      onChange={evt => {
        let newCelsius = getValueFromEvent(evt);
        setCelsius(_ => newCelsius);
      }}
    />
    {React.string({js|°C = |js})}
    {(
       celsius == ""
         ? {js|?°F|js}
         : (
           switch (
             celsius
             |> float_of_string
             |> convert
             |> Js.Float.toFixed(~digits=2)
           ) {
           | exception _ => "error"
           | fahrenheit => fahrenheit ++ {js|°F|js}
           }
         )
     )
     |> React.string}
  </div>;
};
let getValueFromEvent = (evt): string => React.Event.Form.target(evt)##value;

let convert = celsius => 9.0 /. 5.0 *. celsius +. 32.0;

[@react.component]
let make = () => {
  let (celsius, setCelsius) = React.useState(() => "");

  <div>
    <input
      value=celsius
      onChange={evt => {
        let newCelsius = getValueFromEvent(evt);
        setCelsius(_ => newCelsius);
      }}
    />
    {React.string({js|°C = |js})}
    {(
       celsius == ""
         ? {js|?°F|js}
         : (
           switch (
             celsius
             |> float_of_string
             |> convert
             |> Js.Float.toFixed(~digits=2)
           ) {
           | exception _ => "error"
           | fahrenheit => fahrenheit ++ {js|°F|js}
           }
         )
     )
     |> React.string}
  </div>;
};

What happens if you forget the | exception _ branch of your switch expression? Your program will crash when invalid input is entered. The compiler won’t warn you to add an exception branch because it doesn’t keep track of which functions throw exceptions. Next, we’ll show you a better way which completely avoids functions that can fail in unexpected ways.

float_of_string_opt

Refactor the switch expression to use float_of_string_opt instead. This function has the type signature string => option(float). It takes a string argument and returns Some(number) if it succeeds and None if it fails—meaning that even if this function fails, no exception is raised.

re
(
  switch (celsius |> float_of_string_opt) {
  | None => "error"
  | Some(fahrenheit) =>
    (fahrenheit |> convert |> Js.Float.toFixed(~digits=2)) ++ {js|°F|js}
  }
)
(
  switch (celsius |> float_of_string_opt) {
  | None => "error"
  | Some(fahrenheit) =>
    (fahrenheit |> convert |> Js.Float.toFixed(~digits=2)) ++ {js|°F|js}
  }
)

In terms of functionality, this does exactly what the previous version did. But a critical difference is that if you comment out the | None branch, the compiler will refuse to accept it:

File "CelsiusConverter.re", lines 21-32, characters 11-10:
21 | ...........(
22 |            switch (celsius |> float_of_string_opt) {
23 |            //  | None => "error"
24 |            | Some(fahrenheit) =>
25 |              (
...
29 |              )
30 |              ++ {js| °F|js}
31 |            }
32 |          )
Error (warning 8 [partial-match]): this pattern-matching is not exhaustive.
Here is an example of a case that is not matched:
None
File "CelsiusConverter.re", lines 21-32, characters 11-10:
21 | ...........(
22 |            switch (celsius |> float_of_string_opt) {
23 |            //  | None => "error"
24 |            | Some(fahrenheit) =>
25 |              (
...
29 |              )
30 |              ++ {js| °F|js}
31 |            }
32 |          )
Error (warning 8 [partial-match]): this pattern-matching is not exhaustive.
Here is an example of a case that is not matched:
None

You would get a similar error if you left off the | Some(_) branch. Having an option value be the input for a switch expression means that you can’t forget to handle the failure case, much less the success case. There’s another advantage: The | Some(fahrenheit) branch gives you access to the float that was successfully converted from the string, and only this branch has access to that value. So you can be reasonably sure that the success case is handled here and not somewhere else. You are starting to experience the power of pattern matching in OCaml.

Option.map

It’s a shame we had to give up the long chain of function calls from when we were still using float_of_string:

reason
celsius
|> float_of_string
|> convert
|> Js.Float.toFixed(~digits=2)
celsius
|> float_of_string
|> convert
|> Js.Float.toFixed(~digits=2)

Actually, we can still use a very similar chain of functions with float_of_string_opt if we make a couple of small additions:

reason
celsius
|> float_of_string_opt
|> Option.map(convert)
|> Option.map(Js.Float.toFixed(~digits=2))
celsius
|> float_of_string_opt
|> Option.map(convert)
|> Option.map(Js.Float.toFixed(~digits=2))

Option.map takes a function and an option value, and only invokes the function if the option was Some(_). Hovering over it, you can see that its type signature is:

('a => 'b, option('a)) => option('b)
('a => 'b, option('a)) => option('b)

Breaking the type signature down:

  • The first argument, 'a => 'b, is function which accepts a value of type 'a (placeholder for any type) and returns a value of type 'b (also a placeholder for any type, though it may be a different type than 'a).
  • The second argument, option('a), is an option that wraps around a value of type 'a.
  • The return type of Option.map is option('b), which is an option that wraps around a value of type 'b.

The implementation of Option.map is fairly straightforward, consisting of a single switch expression:

reason
let map = (func, option) =>
  switch (option) {
  | None => None
  | Some(v) => Some(func(v))
  };
let map = (func, option) =>
  switch (option) {
  | None => None
  | Some(v) => Some(func(v))
  };

You may be interested in browsing the many other helper functions related to option in the standard library’s Option module.

At this point, your switch expression might look like this:

re
(
  switch (
    celsius
    |> float_of_string_opt
    |> Option.map(convert)
    |> Option.map(Js.Float.toFixed(~digits=2))
  ) {
  | None => "error"
  | Some(fahrenheit) => fahrenheit ++ {js|°F|js}
  }
)
(
  switch (
    celsius
    |> float_of_string_opt
    |> Option.map(convert)
    |> Option.map(Js.Float.toFixed(~digits=2))
  ) {
  | None => "error"
  | Some(fahrenheit) => fahrenheit ++ {js|°F|js}
  }
)

when guard

What if we wanted to render a message of complaint when the temperature goes above 212° F (the boiling point of water) and not even bother to render the converted number? It could look like this:

re
(
  switch (celsius |> float_of_string_opt |> Option.map(convert)) {
  | None => "error"
  | Some(fahrenheit) =>
    fahrenheit > 212.0
      ? {js|Unreasonably hot🥵|js}
      : Js.Float.toFixed(fahrenheit, ~digits=2) ++ {js| °F|js}
  }
)
(
  switch (celsius |> float_of_string_opt |> Option.map(convert)) {
  | None => "error"
  | Some(fahrenheit) =>
    fahrenheit > 212.0
      ? {js|Unreasonably hot🥵|js}
      : Js.Float.toFixed(fahrenheit, ~digits=2) ++ {js| °F|js}
  }
)

This works, but OCaml gives you a construct that allows you to do the float comparison without using a nested ternary expression:

re
(
  switch (celsius |> float_of_string_opt |> Option.map(convert)) {
  | None => "error"
  | Some(fahrenheit) when fahrenheit > 212.0 => {js|Unreasonably hot🥵|js}
  | Some(fahrenheit) =>
    Js.Float.toFixed(fahrenheit, ~digits=2) ++ {js| °F|js}
  }
)
(
  switch (celsius |> float_of_string_opt |> Option.map(convert)) {
  | None => "error"
  | Some(fahrenheit) when fahrenheit > 212.0 => {js|Unreasonably hot🥵|js}
  | Some(fahrenheit) =>
    Js.Float.toFixed(fahrenheit, ~digits=2) ++ {js| °F|js}
  }
)

The when guard allows you to add extra conditions to a switch expression branch, keeping nesting of conditionals to a minimum and making your code more readable.


Hooray! Our Celsius converter is finally complete. Later, we’ll see how to create a component that can convert back and forth between Celsius and Fahrenheit. But first, we’ll explore Dune, the build system used by Melange.

Overview

  • Prefer functions that return option over those that throw exceptions.
    • When the input of a switch expression is option, the compiler can helpfully remind you to handle the error case.
  • Option.map is very useful when chaining functions that return option.
  • You can use a when guard to make your switch expression more expressive without nesting conditionals.

Exercises

1. If you enter a space in the input, it’ll unintuitively produce a Fahrenheit value of 32.00 degrees (because float_of_string_opt(" ") == Some(0.)). Handle this case correctly by showing “? °F” instead.

Hint

Use the String.trim function.

Solution

Add a call to String.trim in your render logic:

re
(
  String.trim(celsius) == ""
    ? {js|?°F|js}
    : (
      switch (celsius |> float_of_string_opt |> Option.map(convert)) {
      | None => "error"
      | Some(fahrenheit) when fahrenheit > 212.0 => {js|Unreasonably hot🥵|js}
      | Some(fahrenheit) =>
        Js.Float.toFixed(fahrenheit, ~digits=2) ++ {js| °F|js}
      }
    )
)
(
  String.trim(celsius) == ""
    ? {js|?°F|js}
    : (
      switch (celsius |> float_of_string_opt |> Option.map(convert)) {
      | None => "error"
      | Some(fahrenheit) when fahrenheit > 212.0 => {js|Unreasonably hot🥵|js}
      | Some(fahrenheit) =>
        Js.Float.toFixed(fahrenheit, ~digits=2) ++ {js| °F|js}
      }
    )
)

2. Add another branch with a when guard that renders “Unreasonably cold🥶” if the temperature is less than -128.6°F (the lowest temperature ever recorded on Earth).

Solution

Add another Some(fahrenheit) branch with a when guard:

re
(
  switch (celsius |> float_of_string_opt |> Option.map(convert)) {
  | None => "error"
  | Some(fahrenheit) when fahrenheit < (-128.6) => {js|Unreasonably cold🥶|js}
  | Some(fahrenheit) when fahrenheit > 212.0 => {js|Unreasonably hot🥵|js}
  | Some(fahrenheit) =>
    Js.Float.toFixed(fahrenheit, ~digits=2) ++ {js|°F|js}
  }
)
(
  switch (celsius |> float_of_string_opt |> Option.map(convert)) {
  | None => "error"
  | Some(fahrenheit) when fahrenheit < (-128.6) => {js|Unreasonably cold🥶|js}
  | Some(fahrenheit) when fahrenheit > 212.0 => {js|Unreasonably hot🥵|js}
  | Some(fahrenheit) =>
    Js.Float.toFixed(fahrenheit, ~digits=2) ++ {js|°F|js}
  }
)

3. Use Js.Float.fromString instead of float_of_string_opt to parse a string to float. Note that Js.Float.fromString does not return None if it fails to parse a string to a valid float.

Hint

Use Js.Float.isNaN.

Solution

Define a new helper function that takes a string and returns option:

re
let floatFromString = text => {
  let value = Js.Float.fromString(text);
  Js.Float.isNaN(value) ? None : Some(value);
};
let floatFromString = text => {
  let value = Js.Float.fromString(text);
  Js.Float.isNaN(value) ? None : Some(value);
};

Then substitute float_of_string_opt with floatFromString in your switch expression:

re
(
  switch (celsius |> floatFromString |> Option.map(convert)) {
  | None => "error"
  | Some(fahrenheit) when fahrenheit < (-128.6) => {js|Unreasonably cold🥶|js}
  | Some(fahrenheit) when fahrenheit > 212.0 => {js|Unreasonably hot🥵|js}
  | Some(fahrenheit) =>
    Js.Float.toFixed(fahrenheit, ~digits=2) ++ {js|°F|js}
  }
)
(
  switch (celsius |> floatFromString |> Option.map(convert)) {
  | None => "error"
  | Some(fahrenheit) when fahrenheit < (-128.6) => {js|Unreasonably cold🥶|js}
  | Some(fahrenheit) when fahrenheit > 212.0 => {js|Unreasonably hot🥵|js}
  | Some(fahrenheit) =>
    Js.Float.toFixed(fahrenheit, ~digits=2) ++ {js|°F|js}
  }
)

View source code and demo for this chapter.