package ppx_fields_conv

  1. Overview
  2. Docs
Generation of accessor and iteration functions for ocaml records

Install

Dune Dependency

Authors

Maintainers

Sources

ppx_fields_conv-v0.16.0.tar.gz
sha256=b098fab38b5204114623a791e7034ff1316e676ba984ca70cadb4084dfc61898

Description

Part of the Jane Street's PPX rewriters collection.

Published: 02 Jun 2023

README

ppx_fields_conv

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, and additional routines, to get and set record fields, iterate and fold over all fields of a record and create new record values.

Basic Usage

If you define a type as follows:

type t = {
  dir : [ `Buy | `Sell ];
  quantity : int;
  price : float;
  mutable cancelled : bool;
} [@@deriving fields]

then code will be generated for functions of the following type:

(* getters *)
val cancelled : t -> bool
val price     : t -> float
val quantity  : t -> int
val dir       : t -> [ `Buy | `Sell ]

(* setters *)
val set_cancelled : t -> bool -> unit

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

  val names : string list

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

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

  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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

      val set_all_mutable_fields : t -> cancelled:bool -> unit
    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:

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

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:

let ( = ) a b : bool =
  let use op = fun 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 (=)) ~quantity:(use (=))
    ~price:(use price_equal) ~cancelled:(use (=))
;;

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

type t = {
  dir : [ `Buy | `Sell ];
  quantity : int;
  price : float;
  mutable cancelled : bool;
  symbol : string;
} [@@deriving fields]

...
Error: This expression has type
         symbol:(([< `Read | `Set_and_create ], t, string) Field.t_with_perm ->
                 bool) ->
         bool
       but an expression was expected of type bool

Or similarly you could use fold to create to_string function:

let to_string t =
  let conv to_s = fun acc f ->
    (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.

Selecting definitions

The [@@deriving fields] clause allows options to specify which definitions it provides. 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:

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.

Opt-in function: fold_right

By default, [@@deriving fields] derives all available functions except Fields.fold_right and Fields.Direct.fold_right. These functions can be selected via ~iterators:fold_right and ~direct_iterators:fold_right. Their signatures (when selected) are as follows.

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

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

Dependencies (5)

  1. ppxlib >= "0.28.0"
  2. dune >= "2.0.0"
  3. fieldslib >= "v0.16" & < "v0.17"
  4. base >= "v0.16" & < "v0.17"
  5. ocaml >= "4.14.0"

Dev Dependencies

None

Used by (22)

  1. ask
  2. ask-integrator
  3. base_quickcheck >= "v0.16.0"
  4. bin_prot >= "v0.16.0"
  5. bio_io >= "0.5.1"
  6. cohttp >= "0.20.1" & < "4.0.0"
  7. conformist < "0.2.1"
  8. cookies
  9. core_bench >= "v0.16.0"
  10. frenetic >= "5.0.0" & < "5.0.5"
  11. ibx >= "0.8.1"
  12. learn-ocaml-client >= "0.13.0"
  13. oci
  14. opium >= "0.15.0" & < "0.19.0"
  15. opium_kernel
  16. ppx_bap < "v0.14.0"
  17. ppx_csv_conv >= "v0.16.0"
  18. ppx_jane >= "v0.16.0"
  19. ppx_xml_conv >= "v0.16.0"
  20. sihl < "0.2.0" | >= "0.3.0~rc2"
  21. sihl-core
  22. zanuda

Conflicts

None