OCaml Tips: Implementing a range Function
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
andstep
) - 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!