What is appropriate use of "include" module?

About include for modules the documentation says…

Note: this is equivalent to a compiler-level copy paste. We heavily discourage include. Use it as last resort!

My app has a lot of unique ID data fields and I want each to have a unique abstract type, like…

type personId = Person(string)
type companyId = Company(string)

I need a common functions for these ID fields, like make, deserialize, serialize, cmp, etc. So I created a module called Uuid with a type definition of this…

type t
let cmp: (t, t) => int
let make: unit => t
let fromString: string => option<t>
let fromStringExn: string => t

To create a specific ID field, I create a file called PersonId.res and the contents are a single line include Uuid. I do this for each unique ID kind I need. Almost no typing required. I incorporate these types into other types. For example, in a Person.res file, I might do this…

type t = { 
  id : PersonId.t
  firstName: string
  lastName: string 

Is this an appropriate use for include? If not, what is a better way to handle it?

The better way to do this is with modules and interfaces. Put your implementation in a file Uuid.res:

module type Intf = {
  type t
  let make: unit => t

module Impl = {
  type t = string
  let make = () => ...

Then instantiate unique ID types anywhere with one line:

module PersonId: Uuid.Intf = Uuid.Impl

Then use PersonId.t as needed.

By the way, with abstract types the implementation can be just type t = string, a variant wrapper is not needed.


Another option is using “phantom types” to make Uuid polymorphic while also enabling modules like Person and Company to each have a unique type of ID.

module Uuid: {
  type t<'a>
  let fromString: ('a, string) => option<t<'a>>
  let toString: t<'a> => string
} = {
  type t<'a> = string
  let fromString = (_, s) =>
    if s == "bad" {
    } else {
  let toString = x => x

Here we have type t<'a> = string, in which the 'a is a “phantom” type. It doesn’t correspond to anything in the actual runtime code. You can have a value of type t<int> and a value of type t<whatever> and they’ll both be strings, but they won’t be compatible with each other. Using this, we can use the Uuid module polymorphically:

module Person = {
  type id = Id
  type t = {id: Uuid.t<id>, name: string}

module Dog = {
  type id = Id
  type t = {id: Uuid.t<id>, name: string}

You would use them like this:

let dog_id = Uuid.fromString(Dog.Id, "abc")
let person_id = Uuid.fromString(Person.Id, "xyz")

The Person.Id / Dog.Id constructors are kind of dummy values, since they don’t do anything besides tell the compiler which type you’re using. But now we get the type mismatches that we want:

let bob: Person.t = {id: Belt.Option.getExn(dog_id), name: "Bob"}
// ERROR: This has type: Uuid.t<Dog.id>
//  Somewhere wanted: Uuid.t<Person.id>
//  The incompatible parts:
//    Dog.id vs Person.id

But all of the Uuid functions, i.e. Uuid.toString, will still work on either one, since they’re fully polymorphic.


These are some great solutions! Very difficult to choose between them. What would you do? The polymorphic one generates less javascript but seems a little more tricky to use. The other one follows the pattern that each type has its own module, and there is a bit more javascript. Here’s what they look like…

module Person = {
  type id = Id
  type t = {
    first: string,
    last: string,
    id: UniqueId.t<id>,

let someId = UniqueId.make(Person.Id)


module Person = {
  module Id: UniqueIdType = UniqueId
  type t = {first: string, last: string, id: Id.t}

let someId = Person.Id.make()

It depends. :wink: @yawaramin’s technique is simpler (in a good way) and there’s nothing wrong with it. The phantom type technique’s advantage is that the Uuid functions are all polymorphic. But that polymorphism may not be needed in your project, in which case there’s no real advantage to it. If you aren’t sure what you need, then the simpler one is probably better.

1 Like