Weeknotes: Flexibility vs type-clutter with EIO (draft)

Jul 1, 2026

My colleague and contributor to the EIO project, Patrick Ferris, asked me to expand on the comments I made about how EIO caused code clutter in my recent weeknote. The main cause, which I relatedly realised was a self-inflicted mistake, was due to type pollution, and I cut that from the original post after I realised it wasn't really the fault of EIO, more me not realising the implications of a particular decision until too late. But given Patrick asked, here's what I did wrong :)

In EIO paths are represented by the type:

Eio.Path.t

This type encapsulates both the name of the path (e.g., /home/mwd/somefile.txt) and the capabilities of access to the file system from EIO (basically is access allowed to the entire file system or is it relative to some sandbox). However, because files in UNIXy based systems are used to represent many things (files on disks, sockets, pipes, etc.), the path type in EIO is polymorphic, which is to say it can represent the different styles of file by taking in different types of path. You can't just have an Eio.Path.t in practice, you have to specialise it. So for a file system path I actually want:

Eio.Fs.dir_ty Eio.Path.t

The dir is for directory and the ty for type, but my brain annoying always does read it as "dirty", which isn't the kind of attribute you want on files generally. But in general when working with files on disk this is the concrete EIO type to use.

So, in Webbplats I have a bunch of record types for representing a website: you have a Site.t that contains a set of Section.t instances, each of which contains a set of Page.t instances. There's also types for Image.t and Config.t that also touch files. For now let's start with a simplified Page.t, that used to be:

type t = {
    title: string;
    filename: Fpath.t;
    titleimage: Fpath.t option;
}

I'm using (or was using) Fpath to store paths, which is like Python's pathlib.Path but for OCaml, but otherwise that would just be string if you wanted to keep things simple.

Under EIO you might hope to change that to:

type t = {
    title: string;
    filename: Eio.Fs.dir_ty Eio.Path.t;
    titleimage: Eio.Fs.dir_ty Eio.Path.t option;
}

Had I stopped here, things would have been fine, I wasn't happy with this for two reasons:

  • It's really quite verbose, and I personally find it harder to read that before.
  • I like my code to generally be flexible where it can be, so why am I pinning down the path more than I might need?

Patrick pointed out that the later is encapsulated in the robustness principle, which states:

"be conservative in what you do, be liberal in what you accept from others"

Which I think sums up my general approach to coding nicely. And so instead of pinning down the path type, I just left it generic:

type 'a t = {
    title: string;
    filename: 'a Eio.Path.t;
    titleimage: 'a Eio.Path.t option;
}

This made it easier to read, and was less committing to the type of file I was using, but it comes at the expence that now my Page.t is polymorphic too, as it inherits the lack of commitment made by the types of files I'm using. I do realise now I come to write these notes I've made a mistake here in that I have committed the type of file path used for the filename and for the titleimage to be the same type! But it did work, and I'm not going to get distracted by that now :)

The problem here is that the fact that Page.t is polymorphic means all the methods around Page.t had to become polymorthic too. So we go from:

val of_file: `Eio.Path.t -> t
val get_title: t -> string
...

to:

val of_file: `a Eio.Path.t -> `a t
val get_title: `a t -> string
...

There's a couple of dozen methods on Page.t, and all had to be updated. And then there's Section.t, which is just a collection of Page.t with some additional metadata, but it now also has to be polymorphic too:

type 'a t = {
    name: string;
    pages: `a Page.t list;
}

And because that is polymorphic all its methods need to have their signatures updated, and then Site.t also needed to be updated, etc. Ultimately my attempt to remain flexible has just rippled through the code base so everything is now generalised in a way it wasn't before. And I even got it wrong, as I was still saying that all files must be of the same type, to live the dream I wanted, I should have had Page.t as:

type ('a, 'b) t = {
    title: string;
    filename: 'a Eio.Path.t;
    titleimage: 'b Eio.Path.t option;
}

And then I'd have been accruing most the alphabet in my type definitions given how many files a website is made from. Doh.

There was one other gotchya too, which became apparent after I had completed my typing. With the code I actually do use the specific file system functions to open and read a file, at which point those EIO functions know they don't accept polymorphic versions, and so I ended up having to constrain my types also:

type `a t = {
    title: string;
    filename: `a Eio.Path.t;
} constraint `a [> Eio.Fs.dir_ty ]

This made a little sense when I had mistakenly constrained all files to the same type, but is clearly just unwieldily in the correct general case.

The tl;dr here is that the robustness principle was the wrong strategy here, and I should have just pinned my types to Eio.Fs.dir_ty. In the end I did just that:

[mwd-eio-move 90c60cb] Remove polymorphic types
 14 files changed, 139 insertions(+), 142 deletions(-)

I am still left with these verbose type names, but I can just create a local type for that:

type path = Eio.Fs.dir_ty Eio.Path.t

type t = {
    title: string;
    filename: path;
    titleimage: path option;
}

and then my code is readable.

Anyway, none of this is rocket science, and just an example of my lack of experience with OCaml's type system that meant my instincts were totally wrong. But I offer this up incase it'll save someone else doing the same mistake in future.

Tags: weeknotes, eio, ocaml