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 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 fold
ing 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.