janestreet/ppx_fields_conv

Generation of accessor and iteration functions for ocaml records

63

stars

110

commits

OCaml

primary language

Jul 10, 2026

updated

Browse cluster: OCaml metaprogramming and PPX

README

ppx_fields_conv
===============

<!-- set up
```ocaml
open! Base
open! Fieldslib
```

```ocaml env=extended
open! Base
open! Fieldslib
```

```ocaml env=alloc
open! Base
open! Fieldslib
```
-->

Generation of accessor and iteration functions for ocaml records.

`ppx_fields_conv` is a ppx rewriter that can be used to define:

* first-class values representing record fields
* additional functions to:
  - get and set record fields
  - iterate and fold over all fields of a record
  - create new record values

# Basic Usage

One common use of `ppx_fields_conv` is to derive accessor functions on record types.
For example, one can derive `getters` on the following simple record type:

```ocaml
module M = struct
  type t =
    { dir : [ `Buy | `Sell ]
    ; quantity : int
    ; price : float
    ; mutable cancelled : bool
    }
  [@@deriving fields ~getters]
end
```

Which produces functions with the following signatures:

```ocaml
module _ : sig
  type t

  val cancelled : t -> bool @@ portable
  val price : t -> float @@ portable
  val quantity : t -> int @@ portable
  val dir : t -> [ `Buy | `Sell ] @@ portable
end =
  M
```

## Implicit unboxed records

To also derive `fields` on the implicit unboxed version of a record, add the
`~unboxed` flag. Getters for an implicit unboxed version are mangled with a "_u"
suffix to distinguish them from the boxed version (there are no setters because
all unboxed record fields are immutable). All other definitions are put in a
module named `Fields_of_<typename>_u` (or `Fields_u` if `<typename>` is `t`).

See [Full list of selectors](#full-list-of-selectors) below for an example.

# Selecting definitions

The `[@@deriving fields]` clause accepts _selectors_ specifying which definitions it
should provide. Use `~getters` and `~setters` to explicitly select toplevel accessors,
`~fields` to select `Field.t` values, and `~names` to select `Fields.names`. Use
`~iterators` with a tuple containing names chosen from `create`, `make_creator`, and so
on, to select elements of `Fields`. Use `~direct_iterators` with a tuple of names to
select elements of `Fields.Direct`. For example:

```ocaml
type t = { x : int; y : int }
[@@deriving fields ~getters ~fields ~iterators:(fold, iter)]
```

The above defines the accessors `x` and `y`, the field values
`Fields.x` and `Fields.y`, `Fields.fold`, and `Fields.iter`.

By default, `[@@deriving fields]` with no selectors derives all of the functions below
except `Fields.fold_right` and `Direct_iterator.fold_right`. This behavior can be changed
with the `-deriving-fields-require-selectors` command-line argument, which currently
defaults to `false`. Passing `true` instead causes `[@@derving fields]` with no selectors
to produce an error during preprocessing. Note that the default behavior derives
`map_poly`, so it doesn't work with records with unboxed fields.

## Implicitly-selected definitions

The definitions of several of these functions depend on the `getters`, `setters`, and
`fields` definitions. As a result, some of their dependencies might be derived in `*.ml`
files, even if they were not explicitly indicated in the selector list. Specifically, in
structures (and in the toplevel of `*.ml` files):

- `[@@deriving fields ~fields]` also derives the functions under `~getters` and `~setters`
- `[@@deriving fields ~iterators:_]` and `[@@deriving fields ~direct_iterators:_]` (for
  any legal arguments to `iterators` and `direct_iterators`; see below) also derives
  `~fields` (which transitively derives `~getters` and `~setters`)

It's fine to use these "free" derived functions in the `*.ml`. However, to expose them in
`*.mli`s, one must explicitly include them in the selector list, since
`[@@deriving fields]`, when used in signatures, only derives declarations for exactly the
requested functions (and not their dependencies).

## Full list of selectors

The full list of permitted selectors and the signatures of the corresponding functions
follows:

<!--BEGIN type_decl-->
```ocaml
type t =
  { dir : [ `Buy | `Sell ]
  ; quantity : int
  ; price : float
  ; mutable cancelled : bool
  }
[@@deriving
  fields
    ~unboxed
    ~getters
    ~local_getters
    ~setters
    ~names
    ~fields
    ~iterators:
      ( create
      , make_creator
      , exists
      , fold
      , fold_right
      , for_all
      , iter
      , map
      , to_list
      , map_poly )
    ~direct_iterators:
      (exists, fold, fold_right, for_all, iter, map, to_list, set_all_mutable_fields)]
```
<!--END-->

then code will be generated for functions of the following type (note that the `@@
portable` annotations are for OxCaml only, and are converted to ignored attributes in
regular OCaml):

<!--BEGIN generated_sig-->
```ocaml skip
(* getters *)
val cancelled : t -> bool @@ portable [@@zero_alloc]
val price     : t -> float @@ portable [@@zero_alloc]
val quantity  : t -> int @@ portable [@@zero_alloc]
val dir       : t -> [ `Buy | `Sell ] @@ portable [@@zero_alloc]

(* local getters *)
val cancelled__local : local_ t -> bool @@ portable [@@zero_alloc]
val price__local     : local_ t -> local_ float @@ portable [@@zero_alloc]
val quantity__local  : local_ t -> local_ int @@ portable [@@zero_alloc]
val dir__local       : local_ t -> local_ [ `Buy | `Sell ] @@ portable [@@zero_alloc]

(* setters *)
val set_cancelled : t -> bool -> unit @@ portable [@@zero_alloc]

(* getters for t# *)
val cancelled_u : t# -> bool @@ portable [@@zero_alloc]
val price_u     : t# -> float @@ portable [@@zero_alloc]
val quantity_u  : t# -> int @@ portable [@@zero_alloc]
val dir_u       : t# -> [ `Buy | `Sell ] @@ portable [@@zero_alloc]

(* local getters for t# *)
val cancelled_u__local : local_ t# -> local_ bool @@ portable [@@zero_alloc]
val price_u__local     : local_ t# -> local_ float @@ portable [@@zero_alloc]
val quantity_u__local  : local_ t# -> local_ int @@ portable [@@zero_alloc]
val dir_u__local       : local_ t# -> local_ [ `Buy | `Sell ] @@ portable [@@zero_alloc]

(* higher order fields and functions over all fields *)
module Fields : sig

  val names : string list @@ portable

  val cancelled : (t, bool            ) Field.t @@ portable
  val price     : (t, float           ) Field.t @@ portable
  val quantity  : (t, int             ) Field.t @@ portable
  val dir       : (t, [ `Buy | `Sell ]) Field.t @@ portable

  val create
    :  dir       : [ `Buy | `Sell ]
    -> quantity  : int
    -> price     : float
    -> cancelled : bool
    -> t
    @@ portable

  val make_creator
    :  dir       : ((t, [ `Buy | `Sell ]) Field.t -> 'a -> ('arg -> [ `Buy | `Sell ]) * 'b)
    -> quantity  : ((t, int             ) Field.t -> 'b -> ('arg -> int             ) * 'c)
    -> price     : ((t, float           ) Field.t -> 'c -> ('arg -> float           ) * 'd)
    -> cancelled : ((t, bool            ) Field.t -> 'd -> ('arg -> bool            ) * 'e)
    -> 'a -> ('arg -> t) * 'e
    @@ portable

  val fold
    :  init      : 'a
    -> dir       : local_ ('a -> (t, [ `Buy | `Sell ]) Field.t -> 'b)
    -> quantity  : local_ ('b -> (t, int             ) Field.t -> 'c)
    -> price     : local_ ('c -> (t, float           ) Field.t -> 'd)
    -> cancelled : local_ ('d -> (t, bool            ) Field.t -> 'e)
    -> 'e
    @@ portable

  val fold_right
    :  dir       : local_ ((t, [ `Buy | `Sell ]) Field.t -> 'd -> 'e)
    -> quantity  : local_ ((t, int             ) Field.t -> 'c -> 'd)
    -> price     : local_ ((t, float           ) Field.t -> 'b -> 'c)
    -> cancelled : local_ ((t, bool            ) Field.t -> 'a -> 'b)
    -> init      : 'a
    -> 'e
    @@ portable

  val map
    :  dir       : local_ ((t, [ `Buy | `Sell ]) Field.t -> [ `Buy | `Sell ])
    -> quantity  : local_ ((t, int             ) Field.t -> int)
    -> price     : local_ ((t, float           ) Field.t -> float)
    -> cancelled : local_ ((t, bool            ) Field.t -> bool)
    -> t
    @@ portable

  val iter
    :  dir       : local_ ((t, [ `Buy | `Sell ]) Field.t -> unit)
    -> quantity  : local_ ((t, int             ) Field.t -> unit)
    -> price     : local_ ((t, float           ) Field.t -> unit)
    -> cancelled : local_ ((t, bool            ) Field.t -> unit)
    -> unit
    @@ portable

  val for_all
    :  dir       : local_ ((t, [ `Buy | `Sell ]) Field.t -> bool)
    -> quantity  : local_ ((t, int             ) Field.t -> bool)
    -> price     : local_ ((t, float           ) Field.t -> bool)
    -> cancelled : local_ ((t, bool            ) Field.t -> bool)
    -> bool
    @@ portable

  val exists
    :  dir       : local_ ((t, [ `Buy | `Sell ]) Field.t -> bool)
    -> quantity  : local_ ((t, int             ) Field.t -> bool)
    -> price     : local_ ((t, float           ) Field.t -> bool)
    -> cancelled : local_ ((t, bool            ) Field.t -> bool)
    -> bool
    @@ portable

  val to_list
    :  dir       : local_ ((t, [ `Buy | `Sell ]) Field.t -> 'a)
    -> quantity  : local_ ((t, int             ) Field.t -> 'a)
    -> price     : local_ ((t, float           ) Field.t -> 'a)
    -> cancelled : local_ ((t, bool            ) Field.t -> 'a)
    -> 'a list
  @@ portable

  val map_poly : local_ ([< `Read | `Set_and_create ], t, 'a) Field.user -> 'a list @@ portable

  (** Functions that take a record directly *)
  module Direct : sig

      val fold
        :  t
        -> init      : 'a
        -> dir       : local_ ('a -> (t, [ `Buy | `Sell ]) Field.t -> t -> [ `Buy | `Sell ] -> 'b)
        -> quantity  : local_ ('b -> (t, int             ) Field.t -> t -> int              -> 'c)
        -> price     : local_ ('c -> (t, float           ) Field.t -> t -> float            -> 'd)
        -> cancelled : local_ ('d -> (t, bool            ) Field.t -> t -> bool             -> 'e)
        -> 'e
        @@ portable

      val fold_right
        :  t
        -> dir       : local_ ((t, [ `Buy | `Sell ]) Field.t -> t -> [ `Buy | `Sell ] -> 'd -> 'e)
        -> quantity  : local_ ((t, int             ) Field.t -> t -> int              -> 'c -> 'd)
        -> price     : local_ ((t, float           ) Field.t -> t -> float            -> 'b -> 'c)
        -> cancelled : local_ ((t, bool            ) Field.t -> t -> bool             -> 'a -> 'b)
        -> init:'a
        -> 'e
        @@ portable

      val map
        :  t
        -> dir       : local_ ((t, [ `Buy | `Sell ]) Field.t -> t -> [ `Buy | `Sell ] -> [ `Buy | `Sell ])
        -> quantity  : local_ ((t, int             ) Field.t -> t -> int              -> int)
        -> price     : local_ ((t, float           ) Field.t -> t -> float            -> float)
        -> cancelled : local_ ((t, bool            ) Field.t -> t -> bool             -> bool)
        -> t
        @@ portable

      val iter
        :  t
        -> dir       : local_ ((t, [ `Buy | `Sell ]) Field.t -> t -> [ `Buy | `Sell ] -> unit)
        -> quantity  : local_ ((t, int             ) Field.t -> t -> int              -> unit)
        -> price     : local_ ((t, float           ) Field.t -> t -> float            -> unit)
        -> cancelled : local_ ((t, bool            ) Field.t -> t -> bool             -> unit)
        -> unit
        @@ portable

      val for_all
        :  t
        -> dir       : local_ ((t, [ `Buy | `Sell ]) Field.t -> t -> [ `Buy | `Sell ] -> bool)
        -> quantity  : local_ ((t, int             ) Field.t -> t -> int              -> bool)
        -> price     : local_ ((t, float           ) Field.t -> t -> float            -> bool)
        -> cancelled : local_ ((t, bool            ) Field.t -> t -> bool             -> bool)
        -> bool
        @@ portable

      val exists
        :  t
        -> dir       : local_ ((t, [ `Buy | `Sell ]) Field.t -> t -> [ `Buy | `Sell ] -> bool)
        -> quantity  : local_ ((t, int             ) Field.t -> t -> int              -> bool)
        -> price     : local_ ((t, float           ) Field.t -> t -> float            -> bool)
        -> cancelled : local_ ((t, bool            ) Field.t -> t -> bool             -> bool)
        -> bool
        @@ portable

      val to_list
        :  t
        -> dir       : local_ ((t, [ `Buy | `Sell ]) Field.t -> t -> [ `Buy | `Sell ] -> 'a)
        -> quantity  : local_ ((t, int             ) Field.t -> t -> int              -> 'a)
        -> price     : local_ ((t, float           ) Field.t -> t -> float            -> 'a)
        -> cancelled : local_ ((t, bool            ) Field.t -> t -> bool             -> 'a)
        -> 'a list
        @@ portable

      val set_all_mutable_fields : local_ t -> cancelled:bool -> unit @@ portable [@@zero_alloc]
    end
end

module Fields_u : sig
  (* similar to the signature of [Fields], but every [t] has replaced with a
     [t#] and [set_all_mutable_fields] doesn't mutate any fields *)

  val names : string list @@ portable

  val cancelled : (t#, bool            ) Field.t @@ portable
  val price     : (t#, float           ) Field.t @@ portable
  val quantity  : (t#, int             ) Field.t @@ portable
  val dir       : (t#, [ `Buy | `Sell ]) Field.t @@ portable

  val create
    :  dir       : [ `Buy | `Sell ]
    -> quantity  : int
    -> price     : float
    -> cancelled : bool
    -> t#
    @@ portable

  val make_creator
    :  dir       : ((t#, [ `Buy | `Sell ]) Field.t -> 'a -> ('arg -> [ `Buy | `Sell ]) * 'b)
    -> quantity  : ((t#, int             ) Field.t -> 'b -> ('arg -> int             ) * 'c)
    -> price     : ((t#, float           ) Field.t -> 'c -> ('arg -> float           ) * 'd)
    -> cancelled : ((t#, bool            ) Field.t -> 'd -> ('arg -> bool            ) * 'e)
    -> 'a -> ('arg -> t#) * 'e
    @@ portable

  val fold
    :  init      : 'a
    -> dir       : local_ ('a -> (t#, [ `Buy | `Sell ]) Field.t -> 'b)
    -> quantity  : local_ ('b -> (t#, int             ) Field.t -> 'c)
    -> price     : local_ ('c -> (t#, float           ) Field.t -> 'd)
    -> cancelled : local_ ('d -> (t#, bool            ) Field.t -> 'e)
    -> 'e
    @@ portable

  val fold_right
    :  dir       : local_ ((t#, [ `Buy | `Sell ]) Field.t -> 'd -> 'e)
    -> quantity  : local_ ((t#, int             ) Field.t -> 'c -> 'd)
    -> price     : local_ ((t#, float           ) Field.t -> 'b -> 'c)
    -> cancelled : local_ ((t#, bool            ) Field.t -> 'a -> 'b)
    -> init      : 'a
    -> 'e
    @@ portable

  val map
    :  dir       : local_ ((t#, [ `Buy | `Sell ]) Field.t -> [ `Buy | `Sell ])
    -> quantity  : local_ ((t#, int             ) Field.t -> int)
    -> price     : local_ ((t#, float           ) Field.t -> float)
    -> cancelled : local_ ((t#, bool            ) Field.t -> bool)
    -> t#
    @@ portable

  val iter
    :  dir       : local_ ((t#, [ `Buy | `Sell ]) Field.t -> unit)
    -> quantity  : local_ ((t#, int             ) Field.t -> unit)
    -> price     : local_ ((t#, float           ) Field.t -> unit)
    -> cancelled : local_ ((t#, bool            ) Field.t -> unit)
    -> unit
    @@ portable

  val for_all
    :  dir       : local_ ((t#, [ `Buy | `Sell ]) Field.t -> bool)
    -> quantity  : local_ ((t#, int             ) Field.t -> bool)
    -> price     : local_ ((t#, float           ) Field.t -> bool)
    -> cancelled : local_ ((t#, bool            ) Field.t -> bool)
    -> bool
    @@ portable

  val exists
    :  dir       : local_ ((t#, [ `Buy | `Sell ]) Field.t -> bool)
    -> quantity  : local_ ((t#, int             ) Field.t -> bool)
    -> price     : local_ ((t#, float           ) Field.t -> bool)
    -> cancelled : local_ ((t#, bool            ) Field.t -> bool)
    -> bool
    @@ portable

  val to_list
    :  dir       : local_ ((t#, [ `Buy | `Sell ]) Field.t -> 'a)
    -> quantity  : local_ ((t#, int             ) Field.t -> 'a)
    -> price     : local_ ((t#, float           ) Field.t -> 'a)
    -> cancelled : local_ ((t#, bool            ) Field.t -> 'a)
    -> 'a list
  @@ portable

  val map_poly : local_ ([< `Read | `Set_and_create ], t#, 'a) Field.user -> 'a list @@ portable

  (** Functions that take a record directly *)
  module Direct : sig

      val fold
        :  t#
        -> init      : 'a
        -> dir       : local_ ('a -> (t#, [ `Buy | `Sell ]) Field.t -> t# -> [ `Buy | `Sell ] -> 'b)
        -> quantity  : local_ ('b -> (t#, int             ) Field.t -> t# -> int              -> 'c)
        -> price     : local_ ('c -> (t#, float           ) Field.t -> t# -> float            -> 'd)
        -> cancelled : local_ ('d -> (t#, bool            ) Field.t -> t# -> bool             -> 'e)
        -> 'e
        @@ portable

      val fold_right
        :  t#
        -> dir       : local_ ((t#, [ `Buy | `Sell ]) Field.t -> t# -> [ `Buy | `Sell ] -> 'd -> 'e)
        -> quantity  : local_ ((t#, int             ) Field.t -> t# -> int              -> 'c -> 'd)
        -> price     : local_ ((t#, float           ) Field.t -> t# -> float            -> 'b -> 'c)
        -> cancelled : local_ ((t#, bool            ) Field.t -> t# -> bool             -> 'a -> 'b)
        -> init:'a
        -> 'e
        @@ portable

      val map
        :  t#
        -> dir       : local_ ((t#, [ `Buy | `Sell ]) Field.t -> t# -> [ `Buy | `Sell ] -> [ `Buy | `Sell ])
        -> quantity  : local_ ((t#, int             ) Field.t -> t# -> int              -> int)
        -> price     : local_ ((t#, float           ) Field.t -> t# -> float            -> float)
        -> cancelled : local_ ((t#, bool            ) Field.t -> t# -> bool             -> bool)
        -> t#
        @@ portable

      val iter
        :  t#
        -> dir       : local_ ((t#, [ `Buy | `Sell ]) Field.t -> t# -> [ `Buy | `Sell ] -> unit)
        -> quantity  : local_ ((t#, int             ) Field.t -> t# -> int              -> unit)
        -> price     : local_ ((t#, float           ) Field.t -> t# -> float            -> unit)
        -> cancelled : local_ ((t#, bool            ) Field.t -> t# -> bool             -> unit)
        -> unit
        @@ portable

      val for_all
        :  t#
        -> dir       : local_ ((t#, [ `Buy | `Sell ]) Field.t -> t# -> [ `Buy | `Sell ] -> bool)
        -> quantity  : local_ ((t#, int             ) Field.t -> t# -> int              -> bool)
        -> price     : local_ ((t#, float           ) Field.t -> t# -> float            -> bool)
        -> cancelled : local_ ((t#, bool            ) Field.t -> t# -> bool             -> bool)
        -> bool
        @@ portable

      val exists
        :  t#
        -> dir       : local_ ((t#, [ `Buy | `Sell ]) Field.t -> t# -> [ `Buy | `Sell ] -> bool)
        -> quantity  : local_ ((t#, int             ) Field.t -> t# -> int              -> bool)
        -> price     : local_ ((t#, float           ) Field.t -> t# -> float            -> bool)
        -> cancelled : local_ ((t#, bool            ) Field.t -> t# -> bool             -> bool)
        -> bool
        @@ portable

      val to_list
        :  t#
        -> dir       : local_ ((t#, [ `Buy | `Sell ]) Field.t -> t# -> [ `Buy | `Sell ] -> 'a)
        -> quantity  : local_ ((t#, int             ) Field.t -> t# -> int              -> 'a)
        -> price     : local_ ((t#, float           ) Field.t -> t# -> float            -> 'a)
        -> cancelled : local_ ((t#, bool            ) Field.t -> t# -> bool             -> 'a)
        -> 'a list
        @@ portable

      val set_all_mutable_fields : local_ t# -> unit @@ portable [@@zero_alloc]
    end
end
```
<!--END-->

Use of `[@@deriving fields]` in an `*.mli` will extend the signature for functions with
the above types; In an `*.ml`, definitions will be generated.

`Field.t` is defined in `Fieldslib`, including:

```ocaml skip
type ('perm, 'record, 'field) t_with_perm

type ('record, 'field) t = ([ `Read | `Set_and_create], 'record, 'field) t_with_perm

val name :  (_, _, _) t_with_perm -> string
val get  : (_, 'r, 'a) t_with_perm -> 'r -> 'a
```

## `zero_alloc` attribute

By default, `ppx_fields_conv` will generate `[@@zero_alloc]` attributes for all getters,
setters, and `set_all_mutable_fields`. It is possible, however, that the generated
implementations for a given record may actually allocate, causing the compiler to complain
(this can happen e.g. if your record contains all float fields, or you mask the generated
code with custom definitions).

```ocaml env=alloc
module Some_float_type = struct
  type t = float
end
```

```ocaml env=alloc
type t =
  { x : Some_float_type.t
  ; y : Some_float_type.t
  }
[@@deriving fields ~getters]
```
```mdx-error
Line 2, characters 7-8:
Error: Annotation check for zero_alloc failed on function TOP9.x (camlTOP9__x_172_177_code).
Hint: add [@@fields.no_zero_alloc] to disable the zero-alloc guarantees that [@@deriving fields] tries to make by default.

Line 3, characters 7-8:
Error: Annotation check for zero_alloc failed on function TOP9.y (camlTOP9__y_171_176_code).
Hint: add [@@fields.no_zero_alloc] to disable the zero-alloc guarantees that [@@deriving fields] tries to make by default.
```

In these instance you can tag your record with an
additional attribute `[@@fields.no_zero_alloc]` to disable adding the `[@@zero_alloc]`
attribute:

```ocaml env=alloc
type t =
  { x : Some_float_type.t
  ; y : Some_float_type.t
  }
[@@fields.no_zero_alloc]
[@@deriving fields ~getters]
```

# Functions over all fields

Use of the generated functions together with `Fieldslib` allow us to
define functions over t which check exhaustiveness w.r.t record
fields, avoiding common semantic errors which can occur when a record
is extended with new fields but we forget to update functions.

For example if you are writing a custom equality operator to ignore
small price differences:

```ocaml
let ( = ) a b : bool =
  let use op field = op (Field.get field a) (Field.get field b) in
  let price_equal p1 p2 = Float.(abs (p1 - p2) < 0.001) in
  Fields.for_all
    ~dir:(use Poly.equal)
    ~quantity:(use ( = ))
    ~price:(use price_equal)
    ~cancelled:(use Bool.equal)
;;
```

A type error would occur if you were to add a new field and not change
the definition of `( = )`:

```ocaml env=extended
type t = {
  dir : [ `Buy | `Sell ];
  quantity : int;
  price : float;
  mutable cancelled : bool;
  symbol : string;
} [@@deriving fields ~iterators:(for_all, fold)]
```

```ocaml env=extended
let ( = ) a b : bool =
  let use op field = op (Field.get field a) (Field.get field b) in
  let price_equal p1 p2 = Float.(abs (p1 - p2) < 0.001) in
  Fields.for_all
    ~dir:(use Poly.equal)
    ~quantity:(use ( = ))
    ~price:(use price_equal)
    ~cancelled:(use Bool.equal)
;;
```
```mdx-error
Lines 4-8, characters 5-34:
Error: This expression has type
         symbol:(([< `Read | `Set_and_create ], t, string) Field.t_with_perm ->
                 bool) @ local ->
         bool
       but an expression was expected of type bool
  Hint: This function application is partial,
  maybe some arguments are missing.
```

Or similarly you could use `fold` to create `to_string` function:

```ocaml
let to_string t =
  let conv to_s acc f =
    Printf.sprintf "%s: %s" (Field.name f) (to_s (Field.get f t)) :: acc
  in
  let fs =
    Fields.fold
      ~init:[]
      ~dir:
        (conv (function
          | `Buy -> "Buy"
          | `Sell -> "Sell"))
      ~quantity:(conv Int.to_string)
      ~price:(conv Float.to_string)
      ~cancelled:(conv Bool.to_string)
  in
  String.concat fs ~sep:", "
;;
```

Addition of a new field would cause a type error reminding you to
update the definition of `to_string`.

# Interaction with `ppx_template`

<!--
```ocaml
open! Base
```
-->

Sometimes a record type is [templated](%{root}/ppx/ppx_template/doc/README.mdx) over
the kind of its parameter. For example:

```ocaml
type%template ('a : k) nonemptylist_with_length =
  { hd : 'a
  ; len : int
  ; rest : ('a nonemptylist_with_length [@kind k]) option
  }
[@@deriving fields ~getters ~local_getters ~iterators:create ~direct_iterators:to_list]
[@@kind k = (value, bits64, float64)]
```

Getters and other functions that are derived at the top-level are generated with each name
templated the same way as the corresponding type. For example, the `hd` getter for
`list_with_length [@kind bits64]` is `hd [@kind bits64]`. Here is an example client of
these getters:

```ocaml
let%template[@kind k = (value, bits64, float64)] [@mode m = (global, local)] validate_lengths
  (l @ m)
  =
  let rec inner_loop l =
    let len_expected =
      match (rest [@kind k] [@mode m]) l with
      | Some rest -> inner_loop rest
      | None -> 1
    in
    assert ((len [@kind k] [@mode m]) l = len_expected);
    len_expected + 1
  in
  ignore (inner_loop l : int)
;;
```

Functions derived in `Fields` are not themselves templated: rather,
the whole module `Fields` is templated:

```ocaml
let%template[@kind k = (value, bits64, float64)] __ =
  let module Fields = Fields_of_nonemptylist_with_length [@kind k] in
  let _ = Fields.create in
  let _ = Fields.Direct.to_list in
  ()
;;
```

Note that `map_poly` cannot currently be used with non-value record fields. Reach out to
the maintainers if you have an example of a case where this would be useful.

```ocaml
type t = { x : int64# } [@@deriving fields ~iterators:map_poly]
```
```mdx-error
Line 1, characters 6-7:
Error: This expression has type
         ([< `Read | `Set_and_create ], t, int64#) Field.t_with_perm
       but an expression was expected of type
         ([< `Read | `Set_and_create ], t, 'a) Field.t_with_perm
       Hint: did you derive [fields ~iterators:map_poly] on a record with non-value fields?
       The layout of int64# is bits64
         because it is the unboxed version of the primitive type int64.
       But the layout of int64# must be a value layout
         because of the annotation on the wildcard _ at line 1, characters 6-7.
```

Not written in Markdown, so it's shown here as plain text — view it formatted on GitHub.

Contributors

jdimino-js

29 commits

public-release

27 commits

xclerc

27 commits

trefis

16 commits

janestreet/ppx_fields_conv

Generation of accessor and iteration functions for ocaml records

63

stars

110

commits

OCaml

primary language

Jul 10, 2026

updated

Browse cluster: OCaml metaprogramming and PPX

README

ppx_fields_conv
===============

<!-- set up
```ocaml
open! Base
open! Fieldslib
```

```ocaml env=extended
open! Base
open! Fieldslib
```

```ocaml env=alloc
open! Base
open! Fieldslib
```
-->

Generation of accessor and iteration functions for ocaml records.

`ppx_fields_conv` is a ppx rewriter that can be used to define:

* first-class values representing record fields
* additional functions to:
  - get and set record fields
  - iterate and fold over all fields of a record
  - create new record values

# Basic Usage

One common use of `ppx_fields_conv` is to derive accessor functions on record types.
For example, one can derive `getters` on the following simple record type:

```ocaml
module M = struct
  type t =
    { dir : [ `Buy | `Sell ]
    ; quantity : int
    ; price : float
    ; mutable cancelled : bool
    }
  [@@deriving fields ~getters]
end
```

Which produces functions with the following signatures:

```ocaml
module _ : sig
  type t

  val cancelled : t -> bool @@ portable
  val price : t -> float @@ portable
  val quantity : t -> int @@ portable
  val dir : t -> [ `Buy | `Sell ] @@ portable
end =
  M
```

## Implicit unboxed records

To also derive `fields` on the implicit unboxed version of a record, add the
`~unboxed` flag. Getters for an implicit unboxed version are mangled with a "_u"
suffix to distinguish them from the boxed version (there are no setters because
all unboxed record fields are immutable). All other definitions are put in a
module named `Fields_of_<typename>_u` (or `Fields_u` if `<typename>` is `t`).

See [Full list of selectors](#full-list-of-selectors) below for an example.

# Selecting definitions

The `[@@deriving fields]` clause accepts _selectors_ specifying which definitions it
should provide. Use `~getters` and `~setters` to explicitly select toplevel accessors,
`~fields` to select `Field.t` values, and `~names` to select `Fields.names`. Use
`~iterators` with a tuple containing names chosen from `create`, `make_creator`, and so
on, to select elements of `Fields`. Use `~direct_iterators` with a tuple of names to
select elements of `Fields.Direct`. For example:

```ocaml
type t = { x : int; y : int }
[@@deriving fields ~getters ~fields ~iterators:(fold, iter)]
```

The above defines the accessors `x` and `y`, the field values
`Fields.x` and `Fields.y`, `Fields.fold`, and `Fields.iter`.

By default, `[@@deriving fields]` with no selectors derives all of the functions below
except `Fields.fold_right` and `Direct_iterator.fold_right`. This behavior can be changed
with the `-deriving-fields-require-selectors` command-line argument, which currently
defaults to `false`. Passing `true` instead causes `[@@derving fields]` with no selectors
to produce an error during preprocessing. Note that the default behavior derives
`map_poly`, so it doesn't work with records with unboxed fields.

## Implicitly-selected definitions

The definitions of several of these functions depend on the `getters`, `setters`, and
`fields` definitions. As a result, some of their dependencies might be derived in `*.ml`
files, even if they were not explicitly indicated in the selector list. Specifically, in
structures (and in the toplevel of `*.ml` files):

- `[@@deriving fields ~fields]` also derives the functions under `~getters` and `~setters`
- `[@@deriving fields ~iterators:_]` and `[@@deriving fields ~direct_iterators:_]` (for
  any legal arguments to `iterators` and `direct_iterators`; see below) also derives
  `~fields` (which transitively derives `~getters` and `~setters`)

It's fine to use these "free" derived functions in the `*.ml`. However, to expose them in
`*.mli`s, one must explicitly include them in the selector list, since
`[@@deriving fields]`, when used in signatures, only derives declarations for exactly the
requested functions (and not their dependencies).

## Full list of selectors

The full list of permitted selectors and the signatures of the corresponding functions
follows:

<!--BEGIN type_decl-->
```ocaml
type t =
  { dir : [ `Buy | `Sell ]
  ; quantity : int
  ; price : float
  ; mutable cancelled : bool
  }
[@@deriving
  fields
    ~unboxed
    ~getters
    ~local_getters
    ~setters
    ~names
    ~fields
    ~iterators:
      ( create
      , make_creator
      , exists
      , fold
      , fold_right
      , for_all
      , iter
      , map
      , to_list
      , map_poly )
    ~direct_iterators:
      (exists, fold, fold_right, for_all, iter, map, to_list, set_all_mutable_fields)]
```
<!--END-->

then code will be generated for functions of the following type (note that the `@@
portable` annotations are for OxCaml only, and are converted to ignored attributes in
regular OCaml):

<!--BEGIN generated_sig-->
```ocaml skip
(* getters *)
val cancelled : t -> bool @@ portable [@@zero_alloc]
val price     : t -> float @@ portable [@@zero_alloc]
val quantity  : t -> int @@ portable [@@zero_alloc]
val dir       : t -> [ `Buy | `Sell ] @@ portable [@@zero_alloc]

(* local getters *)
val cancelled__local : local_ t -> bool @@ portable [@@zero_alloc]
val price__local     : local_ t -> local_ float @@ portable [@@zero_alloc]
val quantity__local  : local_ t -> local_ int @@ portable [@@zero_alloc]
val dir__local       : local_ t -> local_ [ `Buy | `Sell ] @@ portable [@@zero_alloc]

(* setters *)
val set_cancelled : t -> bool -> unit @@ portable [@@zero_alloc]

(* getters for t# *)
val cancelled_u : t# -> bool @@ portable [@@zero_alloc]
val price_u     : t# -> float @@ portable [@@zero_alloc]
val quantity_u  : t# -> int @@ portable [@@zero_alloc]
val dir_u       : t# -> [ `Buy | `Sell ] @@ portable [@@zero_alloc]

(* local getters for t# *)
val cancelled_u__local : local_ t# -> local_ bool @@ portable [@@zero_alloc]
val price_u__local     : local_ t# -> local_ float @@ portable [@@zero_alloc]
val quantity_u__local  : local_ t# -> local_ int @@ portable [@@zero_alloc]
val dir_u__local       : local_ t# -> local_ [ `Buy | `Sell ] @@ portable [@@zero_alloc]

(* higher order fields and functions over all fields *)
module Fields : sig

  val names : string list @@ portable

  val cancelled : (t, bool            ) Field.t @@ portable
  val price     : (t, float           ) Field.t @@ portable
  val quantity  : (t, int             ) Field.t @@ portable
  val dir       : (t, [ `Buy | `Sell ]) Field.t @@ portable

  val create
    :  dir       : [ `Buy | `Sell ]
    -> quantity  : int
    -> price     : float
    -> cancelled : bool
    -> t
    @@ portable

  val make_creator
    :  dir       : ((t, [ `Buy | `Sell ]) Field.t -> 'a -> ('arg -> [ `Buy | `Sell ]) * 'b)
    -> quantity  : ((t, int             ) Field.t -> 'b -> ('arg -> int             ) * 'c)
    -> price     : ((t, float           ) Field.t -> 'c -> ('arg -> float           ) * 'd)
    -> cancelled : ((t, bool            ) Field.t -> 'd -> ('arg -> bool            ) * 'e)
    -> 'a -> ('arg -> t) * 'e
    @@ portable

  val fold
    :  init      : 'a
    -> dir       : local_ ('a -> (t, [ `Buy | `Sell ]) Field.t -> 'b)
    -> quantity  : local_ ('b -> (t, int             ) Field.t -> 'c)
    -> price     : local_ ('c -> (t, float           ) Field.t -> 'd)
    -> cancelled : local_ ('d -> (t, bool            ) Field.t -> 'e)
    -> 'e
    @@ portable

  val fold_right
    :  dir       : local_ ((t, [ `Buy | `Sell ]) Field.t -> 'd -> 'e)
    -> quantity  : local_ ((t, int             ) Field.t -> 'c -> 'd)
    -> price     : local_ ((t, float           ) Field.t -> 'b -> 'c)
    -> cancelled : local_ ((t, bool            ) Field.t -> 'a -> 'b)
    -> init      : 'a
    -> 'e
    @@ portable

  val map
    :  dir       : local_ ((t, [ `Buy | `Sell ]) Field.t -> [ `Buy | `Sell ])
    -> quantity  : local_ ((t, int             ) Field.t -> int)
    -> price     : local_ ((t, float           ) Field.t -> float)
    -> cancelled : local_ ((t, bool            ) Field.t -> bool)
    -> t
    @@ portable

  val iter
    :  dir       : local_ ((t, [ `Buy | `Sell ]) Field.t -> unit)
    -> quantity  : local_ ((t, int             ) Field.t -> unit)
    -> price     : local_ ((t, float           ) Field.t -> unit)
    -> cancelled : local_ ((t, bool            ) Field.t -> unit)
    -> unit
    @@ portable

  val for_all
    :  dir       : local_ ((t, [ `Buy | `Sell ]) Field.t -> bool)
    -> quantity  : local_ ((t, int             ) Field.t -> bool)
    -> price     : local_ ((t, float           ) Field.t -> bool)
    -> cancelled : local_ ((t, bool            ) Field.t -> bool)
    -> bool
    @@ portable

  val exists
    :  dir       : local_ ((t, [ `Buy | `Sell ]) Field.t -> bool)
    -> quantity  : local_ ((t, int             ) Field.t -> bool)
    -> price     : local_ ((t, float           ) Field.t -> bool)
    -> cancelled : local_ ((t, bool            ) Field.t -> bool)
    -> bool
    @@ portable

  val to_list
    :  dir       : local_ ((t, [ `Buy | `Sell ]) Field.t -> 'a)
    -> quantity  : local_ ((t, int             ) Field.t -> 'a)
    -> price     : local_ ((t, float           ) Field.t -> 'a)
    -> cancelled : local_ ((t, bool            ) Field.t -> 'a)
    -> 'a list
  @@ portable

  val map_poly : local_ ([< `Read | `Set_and_create ], t, 'a) Field.user -> 'a list @@ portable

  (** Functions that take a record directly *)
  module Direct : sig

      val fold
        :  t
        -> init      : 'a
        -> dir       : local_ ('a -> (t, [ `Buy | `Sell ]) Field.t -> t -> [ `Buy | `Sell ] -> 'b)
        -> quantity  : local_ ('b -> (t, int             ) Field.t -> t -> int              -> 'c)
        -> price     : local_ ('c -> (t, float           ) Field.t -> t -> float            -> 'd)
        -> cancelled : local_ ('d -> (t, bool            ) Field.t -> t -> bool             -> 'e)
        -> 'e
        @@ portable

      val fold_right
        :  t
        -> dir       : local_ ((t, [ `Buy | `Sell ]) Field.t -> t -> [ `Buy | `Sell ] -> 'd -> 'e)
        -> quantity  : local_ ((t, int             ) Field.t -> t -> int              -> 'c -> 'd)
        -> price     : local_ ((t, float           ) Field.t -> t -> float            -> 'b -> 'c)
        -> cancelled : local_ ((t, bool            ) Field.t -> t -> bool             -> 'a -> 'b)
        -> init:'a
        -> 'e
        @@ portable

      val map
        :  t
        -> dir       : local_ ((t, [ `Buy | `Sell ]) Field.t -> t -> [ `Buy | `Sell ] -> [ `Buy | `Sell ])
        -> quantity  : local_ ((t, int             ) Field.t -> t -> int              -> int)
        -> price     : local_ ((t, float           ) Field.t -> t -> float            -> float)
        -> cancelled : local_ ((t, bool            ) Field.t -> t -> bool             -> bool)
        -> t
        @@ portable

      val iter
        :  t
        -> dir       : local_ ((t, [ `Buy | `Sell ]) Field.t -> t -> [ `Buy | `Sell ] -> unit)
        -> quantity  : local_ ((t, int             ) Field.t -> t -> int              -> unit)
        -> price     : local_ ((t, float           ) Field.t -> t -> float            -> unit)
        -> cancelled : local_ ((t, bool            ) Field.t -> t -> bool             -> unit)
        -> unit
        @@ portable

      val for_all
        :  t
        -> dir       : local_ ((t, [ `Buy | `Sell ]) Field.t -> t -> [ `Buy | `Sell ] -> bool)
        -> quantity  : local_ ((t, int             ) Field.t -> t -> int              -> bool)
        -> price     : local_ ((t, float           ) Field.t -> t -> float            -> bool)
        -> cancelled : local_ ((t, bool            ) Field.t -> t -> bool             -> bool)
        -> bool
        @@ portable

      val exists
        :  t
        -> dir       : local_ ((t, [ `Buy | `Sell ]) Field.t -> t -> [ `Buy | `Sell ] -> bool)
        -> quantity  : local_ ((t, int             ) Field.t -> t -> int              -> bool)
        -> price     : local_ ((t, float           ) Field.t -> t -> float            -> bool)
        -> cancelled : local_ ((t, bool            ) Field.t -> t -> bool             -> bool)
        -> bool
        @@ portable

      val to_list
        :  t
        -> dir       : local_ ((t, [ `Buy | `Sell ]) Field.t -> t -> [ `Buy | `Sell ] -> 'a)
        -> quantity  : local_ ((t, int             ) Field.t -> t -> int              -> 'a)
        -> price     : local_ ((t, float           ) Field.t -> t -> float            -> 'a)
        -> cancelled : local_ ((t, bool            ) Field.t -> t -> bool             -> 'a)
        -> 'a list
        @@ portable

      val set_all_mutable_fields : local_ t -> cancelled:bool -> unit @@ portable [@@zero_alloc]
    end
end

module Fields_u : sig
  (* similar to the signature of [Fields], but every [t] has replaced with a
     [t#] and [set_all_mutable_fields] doesn't mutate any fields *)

  val names : string list @@ portable

  val cancelled : (t#, bool            ) Field.t @@ portable
  val price     : (t#, float           ) Field.t @@ portable
  val quantity  : (t#, int             ) Field.t @@ portable
  val dir       : (t#, [ `Buy | `Sell ]) Field.t @@ portable

  val create
    :  dir       : [ `Buy | `Sell ]
    -> quantity  : int
    -> price     : float
    -> cancelled : bool
    -> t#
    @@ portable

  val make_creator
    :  dir       : ((t#, [ `Buy | `Sell ]) Field.t -> 'a -> ('arg -> [ `Buy | `Sell ]) * 'b)
    -> quantity  : ((t#, int             ) Field.t -> 'b -> ('arg -> int             ) * 'c)
    -> price     : ((t#, float           ) Field.t -> 'c -> ('arg -> float           ) * 'd)
    -> cancelled : ((t#, bool            ) Field.t -> 'd -> ('arg -> bool            ) * 'e)
    -> 'a -> ('arg -> t#) * 'e
    @@ portable

  val fold
    :  init      : 'a
    -> dir       : local_ ('a -> (t#, [ `Buy | `Sell ]) Field.t -> 'b)
    -> quantity  : local_ ('b -> (t#, int             ) Field.t -> 'c)
    -> price     : local_ ('c -> (t#, float           ) Field.t -> 'd)
    -> cancelled : local_ ('d -> (t#, bool            ) Field.t -> 'e)
    -> 'e
    @@ portable

  val fold_right
    :  dir       : local_ ((t#, [ `Buy | `Sell ]) Field.t -> 'd -> 'e)
    -> quantity  : local_ ((t#, int             ) Field.t -> 'c -> 'd)
    -> price     : local_ ((t#, float           ) Field.t -> 'b -> 'c)
    -> cancelled : local_ ((t#, bool            ) Field.t -> 'a -> 'b)
    -> init      : 'a
    -> 'e
    @@ portable

  val map
    :  dir       : local_ ((t#, [ `Buy | `Sell ]) Field.t -> [ `Buy | `Sell ])
    -> quantity  : local_ ((t#, int             ) Field.t -> int)
    -> price     : local_ ((t#, float           ) Field.t -> float)
    -> cancelled : local_ ((t#, bool            ) Field.t -> bool)
    -> t#
    @@ portable

  val iter
    :  dir       : local_ ((t#, [ `Buy | `Sell ]) Field.t -> unit)
    -> quantity  : local_ ((t#, int             ) Field.t -> unit)
    -> price     : local_ ((t#, float           ) Field.t -> unit)
    -> cancelled : local_ ((t#, bool            ) Field.t -> unit)
    -> unit
    @@ portable

  val for_all
    :  dir       : local_ ((t#, [ `Buy | `Sell ]) Field.t -> bool)
    -> quantity  : local_ ((t#, int             ) Field.t -> bool)
    -> price     : local_ ((t#, float           ) Field.t -> bool)
    -> cancelled : local_ ((t#, bool            ) Field.t -> bool)
    -> bool
    @@ portable

  val exists
    :  dir       : local_ ((t#, [ `Buy | `Sell ]) Field.t -> bool)
    -> quantity  : local_ ((t#, int             ) Field.t -> bool)
    -> price     : local_ ((t#, float           ) Field.t -> bool)
    -> cancelled : local_ ((t#, bool            ) Field.t -> bool)
    -> bool
    @@ portable

  val to_list
    :  dir       : local_ ((t#, [ `Buy | `Sell ]) Field.t -> 'a)
    -> quantity  : local_ ((t#, int             ) Field.t -> 'a)
    -> price     : local_ ((t#, float           ) Field.t -> 'a)
    -> cancelled : local_ ((t#, bool            ) Field.t -> 'a)
    -> 'a list
  @@ portable

  val map_poly : local_ ([< `Read | `Set_and_create ], t#, 'a) Field.user -> 'a list @@ portable

  (** Functions that take a record directly *)
  module Direct : sig

      val fold
        :  t#
        -> init      : 'a
        -> dir       : local_ ('a -> (t#, [ `Buy | `Sell ]) Field.t -> t# -> [ `Buy | `Sell ] -> 'b)
        -> quantity  : local_ ('b -> (t#, int             ) Field.t -> t# -> int              -> 'c)
        -> price     : local_ ('c -> (t#, float           ) Field.t -> t# -> float            -> 'd)
        -> cancelled : local_ ('d -> (t#, bool            ) Field.t -> t# -> bool             -> 'e)
        -> 'e
        @@ portable

      val fold_right
        :  t#
        -> dir       : local_ ((t#, [ `Buy | `Sell ]) Field.t -> t# -> [ `Buy | `Sell ] -> 'd -> 'e)
        -> quantity  : local_ ((t#, int             ) Field.t -> t# -> int              -> 'c -> 'd)
        -> price     : local_ ((t#, float           ) Field.t -> t# -> float            -> 'b -> 'c)
        -> cancelled : local_ ((t#, bool            ) Field.t -> t# -> bool             -> 'a -> 'b)
        -> init:'a
        -> 'e
        @@ portable

      val map
        :  t#
        -> dir       : local_ ((t#, [ `Buy | `Sell ]) Field.t -> t# -> [ `Buy | `Sell ] -> [ `Buy | `Sell ])
        -> quantity  : local_ ((t#, int             ) Field.t -> t# -> int              -> int)
        -> price     : local_ ((t#, float           ) Field.t -> t# -> float            -> float)
        -> cancelled : local_ ((t#, bool            ) Field.t -> t# -> bool             -> bool)
        -> t#
        @@ portable

      val iter
        :  t#
        -> dir       : local_ ((t#, [ `Buy | `Sell ]) Field.t -> t# -> [ `Buy | `Sell ] -> unit)
        -> quantity  : local_ ((t#, int             ) Field.t -> t# -> int              -> unit)
        -> price     : local_ ((t#, float           ) Field.t -> t# -> float            -> unit)
        -> cancelled : local_ ((t#, bool            ) Field.t -> t# -> bool             -> unit)
        -> unit
        @@ portable

      val for_all
        :  t#
        -> dir       : local_ ((t#, [ `Buy | `Sell ]) Field.t -> t# -> [ `Buy | `Sell ] -> bool)
        -> quantity  : local_ ((t#, int             ) Field.t -> t# -> int              -> bool)
        -> price     : local_ ((t#, float           ) Field.t -> t# -> float            -> bool)
        -> cancelled : local_ ((t#, bool            ) Field.t -> t# -> bool             -> bool)
        -> bool
        @@ portable

      val exists
        :  t#
        -> dir       : local_ ((t#, [ `Buy | `Sell ]) Field.t -> t# -> [ `Buy | `Sell ] -> bool)
        -> quantity  : local_ ((t#, int             ) Field.t -> t# -> int              -> bool)
        -> price     : local_ ((t#, float           ) Field.t -> t# -> float            -> bool)
        -> cancelled : local_ ((t#, bool            ) Field.t -> t# -> bool             -> bool)
        -> bool
        @@ portable

      val to_list
        :  t#
        -> dir       : local_ ((t#, [ `Buy | `Sell ]) Field.t -> t# -> [ `Buy | `Sell ] -> 'a)
        -> quantity  : local_ ((t#, int             ) Field.t -> t# -> int              -> 'a)
        -> price     : local_ ((t#, float           ) Field.t -> t# -> float            -> 'a)
        -> cancelled : local_ ((t#, bool            ) Field.t -> t# -> bool             -> 'a)
        -> 'a list
        @@ portable

      val set_all_mutable_fields : local_ t# -> unit @@ portable [@@zero_alloc]
    end
end
```
<!--END-->

Use of `[@@deriving fields]` in an `*.mli` will extend the signature for functions with
the above types; In an `*.ml`, definitions will be generated.

`Field.t` is defined in `Fieldslib`, including:

```ocaml skip
type ('perm, 'record, 'field) t_with_perm

type ('record, 'field) t = ([ `Read | `Set_and_create], 'record, 'field) t_with_perm

val name :  (_, _, _) t_with_perm -> string
val get  : (_, 'r, 'a) t_with_perm -> 'r -> 'a
```

## `zero_alloc` attribute

By default, `ppx_fields_conv` will generate `[@@zero_alloc]` attributes for all getters,
setters, and `set_all_mutable_fields`. It is possible, however, that the generated
implementations for a given record may actually allocate, causing the compiler to complain
(this can happen e.g. if your record contains all float fields, or you mask the generated
code with custom definitions).

```ocaml env=alloc
module Some_float_type = struct
  type t = float
end
```

```ocaml env=alloc
type t =
  { x : Some_float_type.t
  ; y : Some_float_type.t
  }
[@@deriving fields ~getters]
```
```mdx-error
Line 2, characters 7-8:
Error: Annotation check for zero_alloc failed on function TOP9.x (camlTOP9__x_172_177_code).
Hint: add [@@fields.no_zero_alloc] to disable the zero-alloc guarantees that [@@deriving fields] tries to make by default.

Line 3, characters 7-8:
Error: Annotation check for zero_alloc failed on function TOP9.y (camlTOP9__y_171_176_code).
Hint: add [@@fields.no_zero_alloc] to disable the zero-alloc guarantees that [@@deriving fields] tries to make by default.
```

In these instance you can tag your record with an
additional attribute `[@@fields.no_zero_alloc]` to disable adding the `[@@zero_alloc]`
attribute:

```ocaml env=alloc
type t =
  { x : Some_float_type.t
  ; y : Some_float_type.t
  }
[@@fields.no_zero_alloc]
[@@deriving fields ~getters]
```

# Functions over all fields

Use of the generated functions together with `Fieldslib` allow us to
define functions over t which check exhaustiveness w.r.t record
fields, avoiding common semantic errors which can occur when a record
is extended with new fields but we forget to update functions.

For example if you are writing a custom equality operator to ignore
small price differences:

```ocaml
let ( = ) a b : bool =
  let use op field = op (Field.get field a) (Field.get field b) in
  let price_equal p1 p2 = Float.(abs (p1 - p2) < 0.001) in
  Fields.for_all
    ~dir:(use Poly.equal)
    ~quantity:(use ( = ))
    ~price:(use price_equal)
    ~cancelled:(use Bool.equal)
;;
```

A type error would occur if you were to add a new field and not change
the definition of `( = )`:

```ocaml env=extended
type t = {
  dir : [ `Buy | `Sell ];
  quantity : int;
  price : float;
  mutable cancelled : bool;
  symbol : string;
} [@@deriving fields ~iterators:(for_all, fold)]
```

```ocaml env=extended
let ( = ) a b : bool =
  let use op field = op (Field.get field a) (Field.get field b) in
  let price_equal p1 p2 = Float.(abs (p1 - p2) < 0.001) in
  Fields.for_all
    ~dir:(use Poly.equal)
    ~quantity:(use ( = ))
    ~price:(use price_equal)
    ~cancelled:(use Bool.equal)
;;
```
```mdx-error
Lines 4-8, characters 5-34:
Error: This expression has type
         symbol:(([< `Read | `Set_and_create ], t, string) Field.t_with_perm ->
                 bool) @ local ->
         bool
       but an expression was expected of type bool
  Hint: This function application is partial,
  maybe some arguments are missing.
```

Or similarly you could use `fold` to create `to_string` function:

```ocaml
let to_string t =
  let conv to_s acc f =
    Printf.sprintf "%s: %s" (Field.name f) (to_s (Field.get f t)) :: acc
  in
  let fs =
    Fields.fold
      ~init:[]
      ~dir:
        (conv (function
          | `Buy -> "Buy"
          | `Sell -> "Sell"))
      ~quantity:(conv Int.to_string)
      ~price:(conv Float.to_string)
      ~cancelled:(conv Bool.to_string)
  in
  String.concat fs ~sep:", "
;;
```

Addition of a new field would cause a type error reminding you to
update the definition of `to_string`.

# Interaction with `ppx_template`

<!--
```ocaml
open! Base
```
-->

Sometimes a record type is [templated](%{root}/ppx/ppx_template/doc/README.mdx) over
the kind of its parameter. For example:

```ocaml
type%template ('a : k) nonemptylist_with_length =
  { hd : 'a
  ; len : int
  ; rest : ('a nonemptylist_with_length [@kind k]) option
  }
[@@deriving fields ~getters ~local_getters ~iterators:create ~direct_iterators:to_list]
[@@kind k = (value, bits64, float64)]
```

Getters and other functions that are derived at the top-level are generated with each name
templated the same way as the corresponding type. For example, the `hd` getter for
`list_with_length [@kind bits64]` is `hd [@kind bits64]`. Here is an example client of
these getters:

```ocaml
let%template[@kind k = (value, bits64, float64)] [@mode m = (global, local)] validate_lengths
  (l @ m)
  =
  let rec inner_loop l =
    let len_expected =
      match (rest [@kind k] [@mode m]) l with
      | Some rest -> inner_loop rest
      | None -> 1
    in
    assert ((len [@kind k] [@mode m]) l = len_expected);
    len_expected + 1
  in
  ignore (inner_loop l : int)
;;
```

Functions derived in `Fields` are not themselves templated: rather,
the whole module `Fields` is templated:

```ocaml
let%template[@kind k = (value, bits64, float64)] __ =
  let module Fields = Fields_of_nonemptylist_with_length [@kind k] in
  let _ = Fields.create in
  let _ = Fields.Direct.to_list in
  ()
;;
```

Note that `map_poly` cannot currently be used with non-value record fields. Reach out to
the maintainers if you have an example of a case where this would be useful.

```ocaml
type t = { x : int64# } [@@deriving fields ~iterators:map_poly]
```
```mdx-error
Line 1, characters 6-7:
Error: This expression has type
         ([< `Read | `Set_and_create ], t, int64#) Field.t_with_perm
       but an expression was expected of type
         ([< `Read | `Set_and_create ], t, 'a) Field.t_with_perm
       Hint: did you derive [fields ~iterators:map_poly] on a record with non-value fields?
       The layout of int64# is bits64
         because it is the unboxed version of the primitive type int64.
       But the layout of int64# must be a value layout
         because of the annotation on the wildcard _ at line 1, characters 6-7.
```

Not written in Markdown, so it's shown here as plain text — view it formatted on GitHub.

Contributors

jdimino-js

29 commits

public-release

27 commits

xclerc

27 commits

trefis

16 commits

Languages

OCaml

99.0%