Safe Maths

This example shows how to perform calculations while avoiding overflows. For example; + actually uses Num.add, which can crash if the bytes of the result can not fit in the provided type:

» Num.max_u64 + Num.max_u64
This Roc code crashed with: "Integer addition overflowed!"

* : U64

If you want to avoid a program-ending crash, you can instead use:

» Num.add_checked(Num.max_u64, Num.max_u64)

Err Overflow : Result U64 [Overflow]

That would allow you to display a clean error to the user or handle the failure in an intelligent way, so that your user does not lose all their progress! Use checked math functions if reliability is important for your application.

For a realistic demonstration, we will use checked math functions to calculate the variance of a population.

The variance formula is: σ² = ∑(X - µ)² / N where:

Code

app [main!] { cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br" }

import cli.Stdout
import cli.Arg exposing [Arg]

## Safely calculates the variance of a population.
##
## variance formula: σ² = ∑(X - µ)² / N
##
## σ² = variance
## X = each element
## µ = mean of elements
## N = length of list
##
## Performance note: safe or checked math prevents crashes but also runs slower.
##
safe_variance : List (Frac a) -> Result (Frac a) [EmptyInputList, Overflow]
safe_variance = |maybe_empty_list|

    # Check length to prevent division by zero
    when List.len(maybe_empty_list) is
        0 -> Err(EmptyInputList)
        _ ->
            non_empty_list = maybe_empty_list

            # Length of list as a fraction (for compatibility in division)
            n = non_empty_list |> List.len |> Num.to_frac

            mean =
                non_empty_list # sum of all elements:
                |> List.walk_try(0.0, |state, elem| Num.add_checked(state, elem))
                |> Result.map_ok(|x| x / n)?

            non_empty_list
            |> List.walk_try(
                0.0,
                |state, elem|
                    diff = Num.sub_checked(elem, mean)? # (X - µ)
                    squared = Num.mul_checked(diff, diff)? # (X - µ)²
                    Num.add_checked(squared, state), # ∑
            )
            |> Result.map_ok(|x| x / n)

main! : List Arg => Result {} _
main! = |_args|

    variance_result =
        [46, 69, 32, 60, 52, 41]
        |> safe_variance
        |> Result.map_ok(Num.to_str)
        |> Result.map_ok(|v| "σ² = ${v}")

    output_str =
        when variance_result is
            Ok(str) -> str
            Err(EmptyInputList) -> "Error: EmptyInputList: I can't calculate the variance over an empty list."
            Err(Overflow) -> "Error: Overflow: When calculating the variance, a number got too large to store in the available memory for the type."

    Stdout.line!(output_str)

expect safe_variance([]) == Err(EmptyInputList)
expect safe_variance([0]) == Ok(0)
expect safe_variance([100]) == Ok(0)
expect safe_variance([4, 22, 99, 204, 18, 20]) == Ok(5032.138888888888888888)
expect safe_variance([46, 69, 32, 60, 52, 41]) == Ok(147.666666666666666666)

Output

Run this from the directory that has main.roc in it:

$ roc main.roc
σ² = 147.666666666666666666

Run unit tests with roc test main.roc