Signal boosting deriving ppx_fields_conv and ppx_compare for bug-resistant record types

Although this is mentioned in Real World OCaml, I’ve talked to a couple of different OCaml based shops now and every one of them has people who do a :exploding_head: when I point out they can replace laborious, error-prone code with ppx_compare and ppx_fields_conv.

So, I thought I’d try to signal boost with this post in the hope that it’s more broadly useful.

(Note: the Time.t usage below is Core specific)

Getters and setters

Suppose you have this record

module Foo = struct
  type t = {
    id : int;
    name: string
    mutable last_seen : Time.t;
  }
end

You don’t feel so much pressure to write field getters in OCaml due to immutable by default.

Saying x.Foo.id is not much different from Foo.id x, though the second form has some convenience; that is, you can write

List.map lst ~f:Foo.id

instead of

List.map lst ~f:(fun x -> x.Foo.id)

For the last_seen field though you may want to provide a setter, just so that you can do some additional side-effecty things perhaps.

let set_last_seen x time =
  printf !"x.last_seen set to %{Time}\n" time;
  x.last_seen <- time

Seems awfully robotic though. Can’t these be generated for you?

Yup. You can simply say

module Foo = struct
  type t = {
    id : int;
    name : string;
    mutable last_seen : Time.t
  } [@@deriving fields]
end

and you will get free Foo.id, Foo.name, and Foo.last_seen get functions and all of the mutable fields, in this case last_seen will get a free Foo.set_last_seen function. You can decide whether or not to expose these getters/setters by also saying [@@deriving fields] in the .mli file.

You can even “override” the setters:

type t = { ... } [@@deriving fields]

let set_last_seen x time =
  printf !"x.last_seen set to %{Time}\n" time;
  set_last_seen_time x time

There’s also the auto-generated set_all_mutable_fields function that has as arguments (with the proper types!) all names of mutable fields, for ensuring you don’t forget about stuff.

In addition to providing getters and setters, fields gives you functions for iterating/folding over all fields in a strong, type-safe way! Probably best to illustrate with an example.

Bug-resistant record diff printing

type t = {
  id : int;
  name : string;
  mutable last_seen : Time.t
 } [@@deriving fields]

let print_diff oldt newt =
  let pr = fun equal to_s f ->
    let oldval = Field.get field oldt in
    let newval = Field.get field newt in
    if not (equal oldval newval) then
      printf "diff: %s %s -> %s\n" (Field.name field) (to_s oldval) (to_s newval)
  in
  Fields.iter
    ~id:(pr Int.equal Int.to_string)
    ~name:(pr String.equal Fn.id)
    ~last_seen:(pr Time.equal Time.to_string)

If you add a new field to t, the type of Fields.iter will change and you’ll get an error telling you to handle the new field.

Free equality and comparison testing

Suppose you have a record with many fields in it. You want to write an equality function because you don’t trust the semantics of polymorphic compare: currently, kind of a backdoor through the type system that does a byte-by-byte compare of records; very controversial. Would be nice to be more predictable, at least.

type t = {
  id : int;
  name: string
}

Trivial enough to write by hand

let equal a b =
  Int.equal a.id b.id && String.equal a.name b.name

But the problem there is if you forget to update your equal function after you add a field to t.

You can use fancy arg destructuring to cause a compile error to protect you from forgetting about new fields:

let equal { id=a_id; name=a_name } { id=b_id; name=b_name } =
  Int.equal a_id b_id && String.equal a_name b_name

In the above, adding more fields will force a compilation error.

You can also use the Fields folding mentioned in the previous section. All of this seems pretty robotic though if you know you’re just to test everything for equality. Can you auto-generate stuff like this?

You can! Declare your type like this:

type t =
  id : int;
  name : string
} [@@deriving equal]

and the equal function gets generated for you. It’s basically identical to the de-structured one above. Same goes for compare if you say [@@deriving compare]. compare is like equal but it evaluates to -1, 0, or 1 based on less-than, equal or greater-than.

Deriving equal even works for nested records, so long as they’re all tagged [@@deriving equal]. And, when needed, supposing you have something more complicated, you can slide in your own equality functions, at any level of nesting.

Closing remarks

These simple, cute little ppx rewriters both save developer time and delete future suffering from your (or your organization’s) timeline.

They let you reduce future technical debt without having to make big payments up front, a rare win-win in programming.

14 Likes

I never understood that part of Real World OCaml’s section on first class record fields. What is x in fun x -> x.Foo.id? Is this some kind of local module open syntax like fun x -> Foo.(x.id)?

This isn’t about [@@deriving fields] at all. The case here is where you have a record type Foo.t defined in module Foo with field id. i.e.:

module Foo = struct
   type t = { id : string }
end

If you want to reference that field, you can’t write this:

let get_id x = x.id

because the field id isn’t in the local scope. You can fix this by qualifying the record field by the module it is contained in:

let get_id x = x.Foo.id

which is the syntax you were confused about. Given that OCaml can also figure out which constructor is intended with enough type information, you can instead write:

let get_id (x:Foo.t) = x.id

which I think is less confusing. Indeed, the x.Foo.id syntax has always been kinda weird, since it looks like Foo is a member of x in some way, but actually, it’s Foo.id which is a member of x.

2 Likes