Grim's web corner

Notes, essays and ramblings

Lists that keep track of their reversal

# open Article_lib ;;

Recently, we (Cargocut, a collective enthusiastic and amused by the use of OCaml) released the nel package, a tiny library (with a very modest API) for describing non-empty lists (containing at least one element, for which the pair of functions hd/tl is total). The purpose of this data structure is to serve as an error buffer for our Pidgin library, in the context of applicative validation, ensuring that in the event of an error, we have at least one error (thanks to the semigroup nature of a non-empty list). Even though the implementation is very straightforward and naive:

type 'a t = 
  | ( :: ) of 'a * 'a list

However, even though it is naively trivial, this data structure shows that sometimes we want to use something that looks like a list, but maintains more invariants (in this case, the presence of at least one element). This is probably why we were fortunate enough to see a discussion started as an issue by Antonin Décimo (one of the maintainers of OCaml, notably known for his extensive contributions to the OCaml runtime).

Since the goal of nel is to remain a tiny single-purpose library, this discussion probably did not belong in the most appropriate place (which is why the issue was closed and converted into a Discuss thread). Nevertheless, it illustrates that some developers would like to have more invariants for such common constructs as lists, for example. In the rest of this article, I will describe Antonin's proposal in my own words, as simply as possible, and then present several implementations.

To rev or not to rev, that's the question

Lists are very convenient to use in OCaml, as they work well with recursion (thanks to their recursive definition) and pattern matching ([] and :: are standard OCaml constructors that can be used to define a slightly different zoology of lists, brilliantly non-prefixable through disambiguation).

As Antonin points out, OCaml programmers make extensive use of folding, mapping, and list concatenation, while keeping tail-recursion in mind whenever possible (even though Tail Recursion Modulo Constructor makes the traditional approaches trivial). Indeed, for reasons of complexity and nesting, it is preferable to build a list by prepending elements, then reverse it at the end of the traversal, rather than append elements to its tail. For example, here is a naive implementation of map:

let map f list = 
  let rec aux acc = function 
    | [] -> List.rev acc 
    | x :: xs -> aux (f x :: acc) xs
  in aux [] list

Usually, the implicit invariant that the list is being built in reverse order is local and fairly easy to reason about. However, as Antonin explains, one is quickly tempted to add a collection of Hungarian-prefix-style functions (using rev_*, in his own words), as demonstrated by the existence of functions such as rev_append, rev_map, rev_iter, etc. According to him, this is why we need to track the construction order in the type of a potential list.

Amusingly, when I first read the issue, my initial intuition was that this was probably a lot of work to capture local invariants. After thinking about it, I realised that this was essentially a lack of motivation that could be applied to static typing in general: "why bother with types when we can be careful and write tests". However, when working with type systems of varying levels of expressiveness, we generally try to strike a trade-off between static guarantees and usability. Typing things too precisely, even when a language allows it, can unfortunately sometimes make code more complicated to use.

After briefly discussing it with Xavier Van de Woestyne, we quickly realised that the invariant we had considered local, building a list in reverse order, wasn't quite so local after all. Indeed, the existence of rev_append (and, by extension, rev_map) points quite clearly to the fact that, for performance reasons, we prefer to leave the responsibility for reversing a list to the caller (for example, when appending it to the end of another list built in a different way). We were even fortunate enough to find a very concrete example of this constraint being relaxed in one of Xavier's earliest contributions to Merlin: "destruct: Removal of residual patterns". Tracking whether or not a list needs to be reversed seems useful for certain classes of problems.

Although this was outside the scope of the nel library, the exercise was entertaining enough to be worth trying (and could potentially lead to a useful library). While Antonin calls for type wizards, which I am most definitely not, I'll present a few ideas I came up with in the next section.

A first by-construction approach

As is often the case in OCaml, when we want to enforce properties by construction, we turn to GADTs, which allow us to encode constraints on type parameters through constructors (using local type equalities). First, I define some tags that will allow me to index reversed and non-reversed lists:

type rev = private R
type ord = private O

I give them constructors so that, outside the module, the compiler considers rev and ord to be distinct (if they are abstract), as discussed in "GADT pattern exhaustiveness checking and abstract types". The private marker is, here, purely cosmetic, as I don't want them to be used for anything other than tagging. But it is probably unnecessary.

The second step is to describe a list type that maintains this tag. Since, at the constructor level, the only way to construct a list is to use :: and [], we can assert that every list construction is reversed when we only use the constructors:

type (_, _) glist =
  | [] : (rev, 'a) glist
  | ( :: ) : 'a * (rev, 'a) glist -> (rev, 'a) glist

This way, we can only construct reversed lists, for example (I haven't installed any pretty-printers for my type, so reading it is a little cumbersome):

# [1] ;;
- : (rev, int) glist = (::) (1, [])

We can see that [1] (which is actually 1 :: []) correctly returns a list whose tag is rev. Now, we would also like to be able to describe non-reversed lists (otherwise the module would not be particularly useful). My idea is simply to add a constructor whose purpose is to reverse a reversed list:

type (_, _) glist =
  | [] : (rev, 'a) glist
  | ( :: ) : 'a * (rev, 'a) glist -> (rev, 'a) glist
  | Ord : (rev, 'a) glist -> (ord, 'a) glist
 type (_, _) glist =
   | [] : (rev, 'a) glist
   | ( :: ) : 'a * (rev, 'a) t -> (rev, 'a) glist
+  | Ord : (rev, 'a) t -> (ord, 'a) glist

We can now build some useful combinators and let type inference guide us to ensure that the tags are assigned correctly:

# let rev_empty = [] ;;
val rev_empty : (rev, 'a) glist = []
# let empty = Ord [] ;;
val empty : (ord, 'a) glist = Ord []
# let ord l = Ord l ;;
val ord : (rev, 'a) glist -> (ord, 'a) glist = <fun>

The counter-intuitive part of this definition is that we never actually reverse (reorder) the list. To do this, we will start by creating two functions:

val of_rev_list : 'a list -> (rev, 'a) glist
val of_list : 'a list -> (ord, 'a) glist

The intuition behind the types of these two functions should be enough: the first simply builds a reversed list from an already reversed list, while the second builds a list from a non-reversed list. Let's start by implementing of_rev_list:

let of_rev_list list =
  let rec aux : (rev, 'a) glist -> 'a list -> (rev, 'a) glist =
   fun acc -> function
     | [] -> acc
     | x :: xs -> aux (x :: acc) xs
  in
  aux [] list

We will traverse all the elements of our list and progressively reconstruct a glist. What may seem strange is that the list is being built in reverse:

# of_rev_list [1; 2; 3] ;;
- : (rev, int) glist = (::) (3, (::) (2, (::) (1, [])))

However, the actual reversal (the projection to regular lists) will take place later. To transform a non-reversed list, we already have ord, so the function is trivial to implement:

let of_list list = 
  list 
  |> of_rev_list 
  |> ord

The fun part (from my perspective) of this encoding is that a non-reversed list has exactly the same structure as a reversed list (which reinforces its somewhat strange nature). Indeed, the only difference is that a non-reversed list is wrapped in the Ord constructor, which maintains the ord tag:

# of_list [1; 2; 3] ;;
- : (ord, int) glist = Ord ((::) (3, (::) (2, (::) (1, []))))

Now that we can construct lists from scratch and from existing regular lists, we can actually perform the reversal by providing the to_list combinator:

(* We want to be able to process lists of two types 
 ([rev] list and [ord] list) *)
let to_list : type a. (a, 'b) glist -> 'b list = fun glist -> 
  (* First, we only deal with rev list *)
  let rec aux : 'b list -> (rev, 'b) glist -> 'b list = 
    fun acc -> function 
    | [] -> acc 
    | x :: xs -> aux (x :: acc) xs
  in match glist with 
  | Ord xs -> 
     (* The list was already reversed by [aux] *)
     aux [] xs
  | ([] | _ :: _) as xs ->
     (* We get a reversed list, so let's reverse it *)
     List.rev (aux [] xs)

We now have enough tools to track reversal in the type. Let's imagine, for example, that we implement a map function on regular lists that does not reverse its final result:

# let my_map f list =
    let rec aux acc = function 
      | List.[] -> acc 
      | List.(x :: xs) -> aux (f x :: acc) xs
    in aux [] list ;;
val my_map : ('a -> 'b) -> 'a list -> (rev, 'b) glist = <fun>

We can quickly test this. As expected, our result should be reversed:

# [1; 2; 3; 4; 5] |> my_map (fun x -> x + 42) |> to_list ;;
- : int list = [47; 46; 45; 44; 43]

We can also make sure that un-reversing works, using the ord function:

# [1; 2; 3; 4; 5] |> my_map (fun x -> x + 42) |> ord |> to_list ;;
- : int list = [43; 44; 45; 46; 47]

And even though the purpose of this type is probably not to build indexed lists only to convert them back into regular lists, we can still build common functions, such as mapping over our glists, which, of course, preserve their tags (mapping over a list does not change its reversal):

let map : type a. ('b -> 'c) -> (a, 'b) glist -> (a, 'c) glist =
 fun f xs ->
  let rec aux : (rev, 'c) glist -> (rev, 'b) glist -> (rev, 'c) glist =
   fun acc -> function
     | [] -> acc
     | x :: xs -> aux (f x :: acc) xs
  in
  match xs with
  | Ord xs -> Ord (aux [] xs)
  | [] -> []
  | _ :: _ as xs -> aux [] xs

The problem seems solved, however, attentive readers will have noticed several major weaknesses in this proposal (which is why I didn't share it in the original discussion). Indeed, this solution is rather costly:

I would add another point of friction: this solution requires rewriting the list API, and even though it seems to (awkwardly) fulfil its promises in terms of type-level tracking, it doesn't seem to be a viable solution for a project of reasonable scope. Still, it was a fun approach (constraining things through the constructors of a data structure) that, in less performance-critical cases, could be interesting and useful.

Let's look at the proposal I actually gave: an approach with less machinery, and probably less exciting, but which, from my perspective, holds more promise.

A second constraint-based approach

This detour into defining a new type using GADTs has nevertheless given us some intuition that the constructors of a list, and its API, can enforce constraints to maintain reversal. Inspired by this first approach, we can easily use a constraint-based approach to maintain a similar set of guarantees without having to rewrite the type of our list, using a phantom witness.

This time, since we will impose fewer constraints at the constructor level (made possible by the use of GADTs), we will start by describing our interface. As before, we begin by describing our tags:

type rev = [ `Rev ]
type ord = [ `Ord ]

This time, we use polymorphic variants for a reason that we will see shortly afterwards. We can now describe our list type that maintains its reversal:

type ('ord, 'a) olist =
    private 'a list
    constraint 'ord = [< `Rev | `Ord ]

We describe a private alias for a list, ensuring that we cannot construct an olist manually. We then add a constraint on the 'ord type parameter to ensure that it must be an instance of [< Rev | Ord ], which will serve as our tag. This is why our tags are described using polymorphic variants: it becomes possible to describe a type that is the union (a closed one, in this case) of rev and ord.

include Olist

Now we can describe the set of operations that we would like to have. As before, we want empty and rev_empty:

val empty : (ord, 'a) olist
val rev_empty : (rev, 'a) olist

Next, we can define the cons function, which only operates on reversed lists (just as in our GADT example), and we can easily imagine an append function:

val cons : 'a -> (rev, 'a) olist -> (rev, 'a) olist
val append : (ord, 'a) olist -> (ord, 'a) olist -> (ord, 'a) olist

As in our previous example, when we enforce the fact that a list built using cons is a reversed list, cons takes a value and a reversed list, and produces a reversed list. append enforces the opposite: we combine two ord lists.

We could also trivially imagine rev_append, with more meaningful information in its type (which, from my point of view, is much easier to read because we don't have to rely on the documentation to know which list will be reversed: it is the first one):

val rev_append : (rev, 'a) olist -> (ord, 'a) olist -> (ord, 'a) olist

We can then easily imagine the constraints on of_list and of_rev_list, the latter simply tagging lists:

val of_list : 'a list -> (ord, 'a) olist
val of_rev_list : 'a list -> (rev, 'a) olist

And the functions for reversing the nature of the list: rev and ord:

val ord : (rev, 'a) olist -> (ord, 'a) olist
val rev : 'a list -> (rev, 'a) olist

As before, we also have functions that preserve the reversal of their input, such as map, whose type is fairly straightforward to write:

val map : ('a -> 'b) -> ('k, 'a) olist -> ('k, 'b) olist

Now that we have an API (as complete as the previous one), we can move on to the implementation, which is much simpler than the previous one. First, we start by describing our type (removing the private marker because we want to be able to construct olist within our module):

type rev = [ `Rev ]
type ord = [ `Ord ]
type ('ord, 'a) olist = 
   'a list 
   constraint 'ord = [< rev | ord ]

We can now trivially implement our functions, and since the tag is just a phantom witness, we simply call existing functions:

let empty = []
let rev_empty = []

let cons x xs = x :: xs 
let append xs ys = xs @ ys
let rev_append a b = List.rev_append a b

let of_list x = x 
let of_rev_list x = x

let ord x = List.rev x 
let rev x = List.rev x

let map f x = List.map f x

And exactly as before, we can implement our my_map function fairly easily. It does not perform the final rev, and this is reflected in its type:

# let my_map f list =
    let rec aux acc = function 
      | List.[] -> acc 
      | List.(x :: xs) -> aux (cons (f x) acc) xs
    in aux rev_empty list ;;
val my_map : ('a -> 'b) -> 'a list -> (rev, 'b) olist = <fun>

We retain the same guarantees as before. The main difference is that we use a less direct style: we go through rev_empty rather than [], and through cons rather than ::. However, unlike the GADT solution, we do not have to reconstruct the list back and forth, and we are overall fully compatible with the existing List API.

At this point, I feel (and Antonin shares this view) that we have sketched out a flexible and functional solution. However, there is one very slight annoyance: we have separate rev and ord functions, even though their implementations are identical. For the sake of elegance, we might imagine a solution that allows us not to split the rev operation into two different functions (even though this is a fairly small price to pay).

A third approach using type-level switches

The last solution I am going to present is entirely based on the previous one, with a slight change to the types, allowing us to unify the rev function which, when given a list tagged rev, returns a list tagged ord, and vice versa.

- type rev = [ `Rev ]
- type ord = [ `Ord ]
+ type (_, _) dir
+ type ord = ([ `Ord ], [ `Rev ]) dir
+ type rev = ([ `Rev ], [ `Ord ]) dir

 type ('ord, 'a) olist = 
    private 'a list 
-   constraint 'ord = [< rev | ord ]
+   constraint 'ord = (_, _) dir

(* ... *)

- val rev : (ord, 'a) olist -> (rev, 'a) olist
- val ord : (rev, 'a) olist -> (ord, 'a) olist
+ val rev : (('o, 'r) dir, 'a) olist -> (('r, 'o) dir, 'a) olist

As we can see, we expose two type parameters in dir, and encode the fact that the reversal of ('a, 'b) dir is ('b, 'a) dir, allowing us to capture the relationship between ord and rev at the type level.

And in our implementation (the ml file), we can simply describe our dir type this way, since it will never be inhabited:

type (_, _) dir = |
include Tlist

And as with our previous examples, here is our my_map function, which does not perform the final reversal:

# let my_map f list =
    let rec aux acc = function 
      | List.[] -> acc 
      | List.(x :: xs) -> aux (cons (f x) acc) xs
    in aux rev_empty list ;;
val my_map : ('a -> 'b) -> 'a list -> (rev, 'b) olist = <fun>

At this point, I think that, provided we consider this invariant important enough to track, we have achieved our goals, namely:

In the original discussion, Florian Angeletti (and yes, we had invoked some Type Wizards) proposed an encoding of type-level switches that takes advantage of objects:

module type S = sig
  type yes = Yes 
  type no = No

  type ord = <neg: rev; ord:yes >
  and rev = <neg:ord; ord:no >

  type ('order,'a) t

  val rev: (<neg:'n; ..>, 'a) t -> ('n,'a) t
end

Which pointed out that the direction (dir) should not be an additional parameter to the rev function. I think his proposal (which made my previous implementation possible in the first place) is roughly equivalent, while, from my point of view, requiring a little more intellectual gymnastics. It also points out that an object type is a type-level record, and here, unifying against < neg : 'n ; .. > projects a field out of it. That's a type-level function, computed by the type checker.

To conclude

This was a very enjoyable little journey. I would really like to thank Antonin for starting this conversation (even though the repository may not have been the most appropriate place for it, I'm glad he took the initiative) and Florian, who, once again, is incredibly impressive in his knowledge of encodings in OCaml's type system!

I'll finish by throwing a few questions out there! What about you?

Feel free to reach out to me at grm@functional.cafe. I hope you found this short, somewhat naive article interesting, and, hopefully, see you in less than a year for another one.

Bibliography

  1. cargocut/nel2026
    • Mickael Spawn
    • Cargocut
  2. A type for ordered lists?2026
    • Antonin Décimo
  3. OCaml.org: A type for ordered lists?2023
    • Antonin Décimo
  4. Destruct: Removal of Residual patterns fix2024
    • Xavier Van de Woestyne
  5. The “Tail Modulo Constructor” program transformation2023
    • Xavier Leroy
    • Damien Doligez
    • Alain Frisch
    • Jacques Garrigue
    • Didier Rémy
    • KC Sivaramakrishnna
    • Jérôme Vouillon
  6. GADT pattern exhaustiveness checking and abstract types
    • Alain Frisch
    • Gabriel Scherer
  7. What about phantom types2026
    • Raphaël Proust