numbat 1.23.0

A statically typed programming language for scientific computations with first class support for physical dimensions and units.
Documentation
use core::scalar
use core::error
use core::strings

@description("Get the length of a list")
@example("len([3, 2, 1])")
fn len<A>(xs: List<A>) -> Scalar

@description("Get the first element of a list. Yields a runtime error if the list is empty.")
@example("head([3, 2, 1])")
fn head<A>(xs: List<A>) -> A

@description("Get everything but the first element of a list. Yields a runtime error if the list is empty.")
@example("tail([3, 2, 1])")
fn tail<A>(xs: List<A>) -> List<A>

@description("Prepend an element to a list")
@example("cons(77, [3, 2, 1])")
fn cons<A>(x: A, xs: List<A>) -> List<A>

@description("Append an element to the end of a list")
@example("cons_end(77, [3, 2, 1])")
fn cons_end<A>(x: A, xs: List<A>) -> List<A>

@description("Check if a list is empty")
@example("is_empty([3, 2, 1])")
@example("is_empty([])")
fn is_empty<A>(xs: List<A>) -> Bool = xs == []

@description("Concatenate two lists")
@example("concat([3, 2, 1], [10, 11])")
fn concat<A>(xs1: List<A>, xs2: List<A>) -> List<A> =
  if is_empty(xs1)
    then xs2
    else cons(head(xs1), concat(tail(xs1), xs2))

@description("Get the first `n` elements of a list")
@example("take(2, [3, 2, 1, 0])")
fn take<A>(n: Scalar, xs: List<A>) -> List<A> =
  if n == 0 || is_empty(xs)
    then []
    else cons(head(xs), take(n - 1, tail(xs)))

@description("Get everything but the first `n` elements of a list")
@example("drop(2, [3, 2, 1, 0])")
fn drop<A>(n: Scalar, xs: List<A>) -> List<A> =
  if n == 0 || is_empty(xs)
    then xs
    else drop(n - 1, tail(xs))

@description("Get the element at index `i` in a list")
@example("element_at(2, [3, 2, 1, 0])")
fn element_at<A>(i: Scalar, xs: List<A>) -> A =
  if i == 0
    then head(xs)
    else element_at(i - 1, tail(xs))

@description("Generate a range of integer numbers from `start` to `end` (inclusive)")
@example("range(2, 12)")
fn range(start: Scalar, end: Scalar) -> List<Scalar> =
  if start > end
    then []
    else cons(start, range(start + 1, end))


@description("Reverse the order of a list")
@example("reverse([3, 2, 1])")
fn reverse<A>(xs: List<A>) -> List<A> =
  if is_empty(xs)
    then []
    else cons_end(head(xs), reverse(tail(xs)))

@description("Generate a new list by applying a function to each element of the input list")
@example("map(sqr, [3, 2, 1])", "Square all elements of a list.")
fn map<A, B>(f: Fn[(A) -> B], xs: List<A>) -> List<B> =
  if is_empty(xs)
    then []
    else cons(f(head(xs)), map(f, tail(xs)))

@description("Generate a new list by applying a function to each element of the input list. This function takes two inputs: a variable, and the element of the list.")
@example("map2(contains, 2, [[0], [2], [1, 2], [0, 2, 3], []])", "Returns a list of bools corresponding to whether the sublist contains a 2 or not.")
fn map2<A, B, C>(f: Fn[(A, B) -> C], other: A, xs: List<B>) -> List<C> =
  if is_empty(xs) 
    then []
    else cons(f(other, head(xs)), map2(f, other, tail(xs)))


@description("Filter a list by a predicate")
@example("filter(is_finite, [0, 1e10, NaN, -inf])")
fn filter<A>(p: Fn[(A) -> Bool], xs: List<A>) -> List<A> =
  if is_empty(xs)
    then []
    else if p(head(xs))
      then cons(head(xs), filter(p, tail(xs)))
      else filter(p, tail(xs))

@description("Fold a function over a list")
@example("foldl(str_append, \"\", [\"Num\", \"bat\", \"!\"])", "Join a list of strings by folding.")
fn foldl<A, B>(f: Fn[(A, B) -> A], acc: A, xs: List<B>) -> A =
  if is_empty(xs)
    then acc
    else foldl(f, f(acc, head(xs)), tail(xs))

fn _merge(xs, ys, cmp) =
  if is_empty(xs)
    then ys
    else if is_empty(ys)
      then xs
      else if cmp(head(xs)) < cmp(head(ys))
        then cons(head(xs), _merge(tail(xs), ys, cmp))
        else cons(head(ys), _merge(xs, tail(ys), cmp))


@description("Sort a list of elements, using the given key function that maps the element to a quantity")
@example("fn last_digit(x) = mod(x, 10)\nsort_by_key(last_digit, [701, 313, 9999, 4])","Sort by last digit.")
fn sort_by_key<A, D: Dim>(key: Fn[(A) -> D], xs: List<A>) -> List<A> =
  if is_empty(xs)
    then []
    else if len(xs) == 1
      then xs
      else _merge(sort_by_key(key, take(floor(len(xs) / 2), xs)),
                  sort_by_key(key, drop(floor(len(xs) / 2), xs)),
                  key)

@description("Sort a list of quantities in ascending order")
@example("sort([3, 2, 7, 8, -4, 0, -5])")
fn sort<D: Dim>(xs: List<D>) -> List<D> = sort_by_key(id, xs)

@description("Returns true if the element `x` is in the list `xs`.")
@example("[3, 2, 7, 8, -4, 0, -5] |> contains(0)")
@example("[3, 2, 7, 8, -4, 0, -5] |> contains(1)")
fn contains<A>(x: A, xs: List<A>) -> Bool = 
  if is_empty(xs)
    then false
    else if x == head(xs)
      then true
      else contains(x, tail(xs))

fn _unique<A>(acc: List<A>, xs: List<A>) -> List<A> = 
  if is_empty(xs)
    then acc
    else if is_empty(acc)
      then _unique([head(xs)], tail(xs))
      else if (acc |> contains(head(xs)))
        then _unique(acc, tail(xs))
        else _unique((cons_end(head(xs), acc)), tail(xs))

@description("Remove duplicates from a given list.")
@example("unique([1, 2, 2, 3, 3, 3])")
fn unique<A>(xs: List<A>) -> List<A> = xs |> _unique([])

@description("Add an element between each pair of elements in a list")
@example("intersperse(0, [1, 1, 1, 1])")
fn intersperse<A>(sep: A, xs: List<A>) -> List<A> =
  if is_empty(xs)
    then []
    else if is_empty(tail(xs))
      then xs
      else cons(head(xs), cons(sep, intersperse(sep, tail(xs))))

fn _add(x, y) = x + y # TODO: replace this with a local function once we support them
@description("Sum all elements of a list")
@example("sum([3 m, 200 cm, 1000 mm])")
fn sum<D: Dim>(xs: List<D>) -> D = foldl(_add, 0, xs)

# TODO: implement linspace using `map` or similar once we have closures. This is ugly.
fn _linspace_helper(start, end, n_steps, i) =
  if i == n_steps
    then []
    else cons(start + (end - start) * i / (n_steps - 1), _linspace_helper(start, end, n_steps, i + 1))

@description("Generate a list of `n_steps` evenly spaced numbers from `start` to `end` (inclusive)")
@example("linspace(-5 m, 5 m, 11)")
fn linspace<D: Dim>(start: D, end: D, n_steps: Scalar) -> List<D> =
  if n_steps <= 1
    then error("Number of steps must be larger than 1")
    else _linspace_helper(start, end, n_steps, 0)

@description("Convert a list of strings into a single string by concatenating them with a separator")
@example("join([\"snake\", \"case\"], \"_\")")
fn join(xs: List<String>, sep: String) =
  if is_empty(xs)
    then ""
    else if len(xs) == 1
      then head(xs)
      else "{head(xs)}{sep}{join(tail(xs), sep)}"

@description("Split a string into a list of strings using a separator")
@example("split(\"Numbat is a statically typed programming language.\", \" \")")
fn split(input: String, separator: String) -> List<String> =
  if input == ""
    then []
    else if !str_contains(separator, input)
      then [input]
      else cons(str_slice(0, idx_separator, input),
                split(str_slice(idx_separator + str_length(separator), str_length(input), input), separator))
  where
    idx_separator = str_find(separator, input)