3 minute read

Lots of programming languages have some built-in range functionality, that’s typically used to generate a list/array of integer numbers. Here are a couple of examples from Ruby and Clojure:

# This is Ruby

(1..10).to_a
# => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

(1..10).filter(&:even?)
# => [2, 4, 6, 8, 10]

# And some related functionality, that doesn't use literal ranges
5.downto(1).to_a # => [5, 4, 3, 2, 1]

10.step(1, -2).to_a # => [10, 8, 6, 4, 2]
;; This is Clojure

(range 1 10)
;; => (1 2 3 4 5 6 7 8 9)

(range 1 10 2)
;; => (1 3 5 7 9)

(range 100 0 -10)
;; (100 90 80 70 60 50 40 30 20 10)

Ruby has a special syntax for ranges (.. and ...) and Clojure provides a range function to generate ranges (basically a sequences of integer numbers). I’m pretty sure you’ve seen something like this. Not the most useful function in the world for sure, but it’s handy from time to time.

There are many ways we can implement something similar in OCaml, with the simplest one probably being to use List.init internally:

(* simplest/shortest possible version *)
let range i = List.init i succ

range 10
- : int list = [1; 2; 3; 4; 5; 6; 7; 8; 9; 10]

(* a version with some boundaries *)
let range from until =
  List.init (until - from) (fun i -> i + from)

range 1 10
- : int list = [1; 2; 3; 4; 5; 6; 7; 8; 9]

range 5 15
- : int list = [5; 6; 7; 8; 9; 10; 11; 12; 13; 14]

(* let's name this -- for good measure *)
let (--) = range

1 -- 10
- : int list = [1; 2; 3; 4; 5; 6; 7; 8; 9]

(* you can also consider using the names --> and --< for inclusive and exclusive ranges *)

The above implementations are super basic and have a few quirks (e.g. the second implementation doesn’t allow setting the step and it can’t handle descending ranges), but they mostly get the job done. A more full-featured implementation would look something like this:

let range ?(from=1) ?(step=1) until =
  let cmp = match step with
    | i when i < 0 -> (>)
    | i when i > 0 -> (<)
    | _ -> raise (Invalid_argument "step cannot be 0")
  in
  Seq.unfold (function
        | i when cmp i until -> Some (i, i + step)
        | _ -> None
    ) from

This function has a few advantages over the implementations so far:

  • It uses optional labeled arguments (from and step)
  • It allows us to set the step explicitly
  • It handles descending ranges
  • It’s implemented in terms of Seq, meaning it’s lazy

Note: The order of the arguments in the definition matters, as optional arguments are only filled in once a positional argument after them has been applied. If ?step is the last argument that can never happen.

And here’s how using it in practice looks:

range 10 |> List.of_seq;;
- : int list = [1; 2; 3; 4; 5; 6; 7; 8; 9]

range ~from:10 20 |> List.of_seq;;
- : int list = [10; 11; 12; 13; 14; 15; 16; 17; 18; 19]

range 100 ~step:10 |> List.of_seq;;
- : int list = [1; 11; 21; 31; 41; 51; 61; 71; 81; 91]

range ~from:5 20 ~step:5 |> List.of_seq;;
- : int list = [5; 10; 15]

range ~from:20 5 ~step:(-5) |> List.of_seq;;
- : int list = [20; 15; 10]

Now we’re talking!

One funny thing to note is that originally I wanted to use from and to as the parameter names, but I couldn’t use to as it’s a keyword is OCaml:

for variable = start_value to end_value do
  expression
done

I keep forgetting about this, as I never use those for loops and Clojure has spoiled me with its extremely small set of keywords.

Another funny bit worth sharing, as that one of the test cases for Seq.unfold is exactly a trivial implementation of range:

(* unfold *)
let () =
  let range first last =
    let step i = if i > last then None
                 else Some (i, succ i) in
    Seq.unfold step first
  in
  begin
    assert ([1;2;3] = !!(range 1 3));
    assert ([] = !!(range 1 0));
  end
;;

A quick search on Sherlodoc reveals a ton of similar functions in many OCaml libraries, so clearly there’s some use for a range function in one form or another.

That’s all I have for you today. Keep hacking!

Tags: ,

Updated: