When reading the book “More OCaml” a few months ago, I had a moment when I got exposed to a type representing an infinite list. So simple! Beautiful!!
Fast forward a few months later, and I’m re-exploring the subject but this time learning a bit about the standard lib’s Seq
module.
(I’ve made the following code a bit more verbose on purpose, for clarity)
(*
dune exec --no-print-directory ./primes.exe
*)
module Via_seq = struct
let rec make_primes (seq : int Seq.t) : int Seq.t =
match seq () with
| Seq.Nil -> fun () -> Seq.Nil
| Seq.Cons (h, seq) ->
let next_primes = Seq.filter (fun n -> n mod h <> 0) seq in
fun () -> Cons (h, make_primes next_primes)
;;
let primes : int Seq.t = make_primes @@ Seq.ints 2
let main () =
print_string
@@ String.concat " "
@@ List.map string_of_int
@@ List.of_seq
@@ Seq.take 20 primes
;;
end
module Via_own = struct
(** A lazy list represents an infinite list.
It has no tail, but a "tail function".
It has no `Nil` constructor because the list has no end. *)
type 'a lazylist = Cons of 'a * (unit -> 'a lazylist)
(** Builds a lazylist from [n], always increasing by 1 *)
let rec lseq n = Cons (n, fun () -> lseq (n + 1))
let rec ltake n (Cons (h, tf)) =
if n > 0 then
h :: ltake (n - 1) (tf ())
else
[]
;;
let rec lfilter test (Cons (h, tf)) : 'a lazylist =
if test h then
Cons (h, fun () -> lfilter test (tf ()))
else
lfilter test (tf ())
;;
let rec make_primes (Cons (h, tf)) =
let next_primes = lfilter (fun n -> n mod h <> 0) (tf ()) in
Cons (h, fun () -> make_primes next_primes)
;;
let primes : int lazylist = make_primes (lseq 2)
let main () =
print_string @@ String.concat " " @@ List.map string_of_int @@ ltake 20 primes
;;
end
let () =
()
; print_endline "Primes (via Seq module)"
; Via_seq.main ()
; print_newline ()
; print_newline ()
; print_endline "Primes (via own lazylist type)"
; Via_own.main ()
; print_newline ()
;;
Even after playing with it for a while, the Seq
module doesn’t feel as nice in comparaison, more complicated.
One thing that bugs me is introducing Nil
which I feel kinda breaks the concept a bit (the list may or may not be infinite). As an example, if I want to represent a sequence of prime numbers, then introducing Nil
conceptually doesn’t make much sens to me.
I looked around at other implementations and introducing Nil
seems to be not so uncommon. However, I fail to understand what it buys me. Taking my generating primes example, if I ever wanted to stop producing primes, I could easily produce Some numbers up to a certain point (None
otherwise) to signal to a consumer that it can stop consuming this “infinite” list (there won’t be other values of interest from this point on).
I found a reference to this simpler way of thinking about sequences here: 8.4. Sequences — OCaml Programming: Correct + Efficient + Beautiful
The “More OCaml” book is listed as a reference in the summary section.
So my current understanding is that there seems to be two school of thoughts:
- represent infinite sequences
- representing sequences that may or may not be infinite
In both cases, the value of the next item is produced as-needed, by evaluating a thunk.
I’d be interested to learn more. When and why one would want to use one representation vs the other?
And if there are other known/useful libraries on the matter.