# 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

revandordto 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:

-

Constructing from a regular list requires traversing the entire list (which may be negligible because we assume that, in general, we will start from an empty list and progressively accumulate elements, as is often done in recursive algorithms that work with lists).

-

A more annoying problem: we have to traverse the entire list regardless in order to produce a regular list. This means that to convert an ord-tagged list into a regular list, we traverse the list once, while converting arev-tagged list into a regular list requires traversing it once, then reversing it, resulting in another traversal.

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:

- We can track the reversal of a list at the type level.

- We can handle revuniformly by using type-level switches.

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?

- Do you think this kind of invariant is important enough to track in the type system?

- Do you know of other encodings for this kind of indexing?

- Has this topic already been explored in the literature?

- Would we want to expose a library to address this problem?

- And for those coming from other languages such as Haskell, Scala, F#, and other languages with richer type systems such as Rocq, Lean, Idris, and all the others, how do they deal with this kind of problem?

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.