package ppx_deriving_melange
Install
dune-project
Dependency
Authors
Maintainers
Sources
sha256=1ff2e1d503be5aedba21b7078e56e95e526d968f6ccb315365eb05317e5b33e8
sha512=b8ce66ec5a72794985586e24a6455b13f29e8dfa90e1b785c6e1840689a0989cedaf0e52d0596daccd5da7e7f074e507a2a9b328e78e430bff9627c4d0abffb8
doc/README.html
ppx_deriving_melange
ppx_deriving_melange is intended to be a Melange-compatible subset of ppx_deriving.
Supported derivers: eq, fold, iter, make, map, ord, and show.
eq
type t =
| A
| B
[@@deriving eq]This generates:
val equal : t -> t -> boolFor non-t type names, the generated function follows the usual ppx_deriving.eq convention:
type filter_id =
| FirstSeen
| Volume
[@@deriving eq]
val equal_filter_id : filter_id -> filter_id -> booleq scope
The initial version supports classic variants with:
- constructors without payloads
- tuple payload constructors
- primitive payloads:
string,int,bool,float,char,bytes,int32,Int32.t,int64,Int64.t, andunit - list payloads, e.g.
string listandFoo.t list - option payloads, e.g.
string optionandFoo.t option - array payloads, e.g.
string arrayandFoo.t array - result payloads, e.g.
(string, error) result - tuple payloads, e.g.
int * string - custom equality on payload types via
[@equal ...] - simple type aliases whose target type is supported
- record types with supported field types
- record payload constructors with supported field types
- type parameters and generic type applications whose arguments are supported
- recursive type groups whose members use supported shapes
- closed polymorphic variants with supported payloads
For custom payload types, generated code follows the usual ppx_deriving.eq name convention:
type t = Wrap of Foo.t [@@deriving eq]uses:
Foo.equalCustom equality can be provided on payload or field types with [@equal ...]. For compatibility with native ppx_deriving.eq, the namespaced form [@deriving.eq.equal ...] is also accepted.
iter
iter generates a function that applies a callback to every value sitting at a type-parameter position:
type 'a tree =
| Leaf
| Node of 'a tree * 'a * 'a tree
[@@deriving iter]This generates:
val iter_tree : ('a -> unit) -> 'a tree -> unitNaming follows the same convention as eq: type t generates iter, type status generates iter_status, and a reference to Foo.t uses Foo.iter.
iter scope
iter supports the same shapes as eq: variants (including tuple and inline-record payloads), records, tuples, simple aliases, type parameters, generic type applications, recursive type groups, closed polymorphic variants, and list, option, array, and result payloads.
Matching native ppx_deriving.iter, a type expression with no free type variables iterates as a no-op:
type t = Foo.t [@@deriving iter]generates a val iter : t -> unit that does nothing — in particular, Foo.iter is not required to exist (unlike eq, which needs Foo.equal). This rule covers all primitive payloads, so iteration is only observable for parameterized types.
Also matching native ppx_deriving.iter, there is no [@iter ...] custom-function attribute. Functor-applied type paths (Make(Arg).t) and polymorphic variant row inheritance are rejected with a clear error when they are reached; monomorphic occurrences collapse to the no-op first, as in native ppx_deriving.
map
map generates a function that rebuilds a value with every type-parameter position transformed — the structural counterpart of iter:
type 'a tree =
| Leaf
| Node of 'a tree * 'a * 'a tree
[@@deriving map]This generates:
val map_tree : ('a -> 'b) -> 'a tree -> 'b treeNaming follows the same convention as the other derivers: type t generates map, type status generates map_status, and a reference to Foo.t uses Foo.map. Each type parameter takes its own callback, with a fresh result variable per parameter:
type ('a, 'b) t = Left of 'a | Right of 'b [@@deriving map]
val map : ('a -> 'c) -> ('b -> 'd) -> ('a, 'b) t -> ('c, 'd) tResult variable names are the first free letters not used by the declared parameters (so ('k, 'v) t maps with ('k -> 'a) -> ('v -> 'b) -> ...).
map semantics
Matching native ppx_deriving.map, a type expression with no free type variables maps as the identity:
type t = Foo.t [@@deriving map]generates a val map : t -> t that returns its argument unchanged — in particular, Foo.map is not required to exist (unlike eq, which needs Foo.equal). Constructors, record fields, and containers are rebuilt with their shape preserved: List.map/Array.map for lists and arrays, None/Some, Ok/Error, and tuples element by element.
map scope
map supports the same shapes as iter: variants (including tuple and inline-record payloads), records, tuples, simple aliases, type parameters, generic type applications, recursive type groups, closed polymorphic variants, and list, option, array, and result payloads.
Also matching native ppx_deriving.map, there is no [@map ...] custom-function attribute. Functor-applied type paths (Make(Arg).t) and polymorphic variant row inheritance are rejected with a clear error when they are reached; monomorphic occurrences collapse to the identity first, as in native ppx_deriving.
fold
fold threads an accumulator through every type-parameter position, in lexical order:
type 'a tree =
| Leaf
| Node of 'a tree * 'a * 'a tree
[@@deriving fold]This generates:
val fold_tree : ('b -> 'a -> 'b) -> 'b -> 'a tree -> 'bso fold_tree (+) 0 sums an int tree. Naming follows the usual convention: type t generates fold, type status generates fold_status, and a reference to Foo.t uses Foo.fold. Callbacks are fold_left-style (acc -> element -> acc), one per type parameter, all sharing a single accumulator type variable — the first free letter not used by the declared parameters:
type ('a, 'b) t = Left of 'a | Right of 'b [@@deriving fold]
val fold : ('c -> 'a -> 'c) -> ('c -> 'b -> 'c) -> 'c -> ('a, 'b) t -> 'cfold semantics
Matching native ppx_deriving.fold, a type expression with no free type variables folds as the accumulator passthrough:
type t = Foo.t [@@deriving fold]generates a val fold : 'a -> t -> 'a that returns the accumulator unchanged — Foo.fold is not required to exist. Values at parameter positions are visited in lexical order: record fields and constructor payloads in declaration order, list/array elements left to right (List.fold_left/Array.fold_left), tuple components left to right; None and constant constructors return the accumulator as-is.
fold scope
fold supports the same shapes as iter and map: variants (including tuple and inline-record payloads), records, tuples, simple aliases, type parameters, generic type applications, recursive type groups, closed polymorphic variants, and list, option, array, and result payloads.
Also matching native ppx_deriving.fold, there is no [@fold ...] custom-function attribute. Functor-applied type paths (Make(Arg).t) and polymorphic variant row inheritance are rejected with a clear error when they are reached; monomorphic occurrences collapse to the passthrough first.
ord
ord generates a compare function — the same convention native ppx_deriving.ord uses, so the result drops straight into Map.Make / Set.Make and List.sort:
type t =
| Red
| Green
| Blue
[@@deriving ord]This generates:
val compare : t -> t -> intNote the function is named compare, not ord: type t generates compare, type status generates compare_status, and a reference to Foo.t uses Foo.compare. Parameterized types take a comparison callback per type parameter, e.g. val compare : ('a -> 'a -> int) -> 'a t -> 'a t -> int.
ord semantics
Comparison matches native ppx_deriving.ord:
- variants and polymorphic variants are ordered by declaration order; within the same constructor, payloads are compared lexicographically
- records and tuples compare fields/elements lexicographically in declaration order
None < Some _;Ok _ < Error _- lists compare element-by-element (a prefix is smaller); arrays compare by length first, then elements
- primitives use a typed
Stdlib.compare
ord scope
ord supports the same shapes as eq (variants with tuple and inline-record payloads, records, tuples, simple aliases, type parameters, generic type applications, recursive type groups, closed polymorphic variants, and list, option, array, result, and unit). Custom comparison can be provided on a payload or field type with [@compare ...]; for compatibility with native ppx_deriving.ord, the namespaced form [@deriving.ord.compare ...] is also accepted.
As with eq and iter, ref, lazy_t, nativeint, functor-applied type paths, and polymorphic variant row inheritance are rejected with a clear error (native ppx_deriving.ord supports these, but they are out of scope here).
show
show generates a structural debug printer: a Format-based pp plus a show function that renders a value to a string in OCaml-like syntax. show builds the string directly (without Format) whenever the type allows it — see show and Melange bundle size:
type t =
| Red
| Green
| Blue
[@@deriving show]This generates:
val pp : Stdlib.Format.formatter -> t -> unit
val show : t -> stringNaming follows the usual convention: type t generates pp/show, type status generates pp_status/show_status, and a reference to Foo.t uses Foo.pp. Parameterized types take a printer callback per type parameter, e.g. val show : (Stdlib.Format.formatter -> 'a -> unit) -> 'a t -> string.
show output
Output matches native ppx_deriving.show, with one intentional exception: values longer than Format's margin (80 columns) print on a single line from show, where native wraps them (pp wraps exactly like native):
- constructors print as
Zero,(One 5),(Pair (1, "a")); inline-record payloads asItem {rank = 1; label = "x"} - records print as
{ name = "a"; count = 1 } - primitives print in OCaml syntax: strings and chars quoted and escaped, floats via
%F,int32as7l,int64as9L,bytesthroughBytes.to_string, unit as() - containers:
[1; 2],[|3|],(Some 1)/None,(Ok 1)/(Error "e") - polymorphic variants print as
`Alland`Name ("a") - functions print as
<fun>
By default (with_path = true, native behavior) constructor names and the first record field are qualified with the module path (Main_module.Sub.Red); pass { with_path = false } to drop it:
type t = Red [@@deriving show { with_path = false }]
(* show Red = "Red" *)A re-exported definition (type u = M.s = A | B) prints the manifest's module path (M.A), also matching native behavior.
show attributes
A custom printer can be provided on a payload or field type with [@printer ...]; the printer body may use a bare fprintf, which is aliased to Stdlib.Format.fprintf as in native ppx_deriving.show. The namespaced form [@deriving.show.printer ...] is also accepted:
type t = Named of (string[@printer fun fmt -> fprintf fmt "name=%s"])
[@@deriving show { with_path = false }]
(* show (Named "x") = "(Named name=x)" *)[@printer] is also accepted on a constructor declaration; the printer receives fmt and the payload packed as one value ((), the single payload, or a tuple), or one argument per field for inline-record payloads:
type t =
| First [@printer fun fmt _ -> Format.pp_print_string fmt "first"]
| Second of int [@printer fun fmt i -> fprintf fmt "second: %d" i]
[@@deriving show]
(* show First = "first", show (Second 42) = "second: 42" *)[@opaque] (or [@deriving.show.opaque]) prints <opaque> without traversing the value:
type t = { secret : (string[@opaque]) } [@@deriving show]show and Melange bundle size
Melange compiles Stdlib.Format to a large amount of JavaScript. To keep frontend bundles small, show builds its string directly (string_of_int, String.escaped, ^) whenever the type allows it, composing through other types' show functions — code that only calls show does not need Format at all. pp stays Format-based for native parity, %a composition, and [@printer] support.
show falls back to Format (asprintf "%a" pp) when a formatter is really needed: type parameters (the callbacks are printers), custom [@printer] attributes, or applications of parameterized types such as int Box.t.
Bundler note: the generated module still imports Format because of pp, and melange marks Format as having side effects, so bundlers keep the import even when pp is unused. To let the bundler drop it, mark melange stdlib modules side-effect-free, e.g. with an esbuild plugin:
build.onResolve({ filter: /^melange\// }, (args) => ({
path: resolveMelangePath(args.path),
sideEffects: false,
}));show scope
show supports the same shapes as eq/ord (variants with tuple and inline-record payloads, records, tuples, simple aliases, type parameters, generic type applications, recursive type groups, closed polymorphic variants, and list, option, array, result, and unit), plus arrow types (printed as <fun>).
As with the other derivers, ref, lazy_t, nativeint, functor-applied type paths, and polymorphic variant row inheritance are out of scope (native ppx_deriving.show supports these). [@polyprinter] and [@nobuiltin] are not supported either.
make
make generates a smart constructor for a record type — a function that takes each field as an argument and returns the record. Unlike the other derivers it does not traverse types; it is records-only.
type status = {
id : int;
note : string option;
tags : string list;
retries : int; [@default 3]
}
[@@deriving make]This generates:
val make_status : id:int -> ?note:string -> ?tags:string list -> ?retries:int -> unit -> statusso make_status ~id:1 () builds { id = 1; note = None; tags = []; retries = 3 }. Naming follows the usual convention: type t generates make, type status generates make_status.
make field mapping
Each field becomes an argument, in declaration order:
field | argument |
|---|---|
| required labelled |
| optional |
| optional |
| optional |
| final positional (unlabelled) argument |
| required |
A trailing unit argument is added when the record has optional arguments and no [@main] field (it lets the optionals be applied); a [@main] field closes the function instead; a record with neither gets no trailing unit (make_pair ~first ~second). The [@default], [@main], and [@split] attributes are also accepted in the namespaced form ([@deriving.make.default], etc.).
make scope
Records only; every other shape (variants, abstract, open, tuple/alias, and polymorphic-variant manifests) is rejected with a clear error. A mutually recursive group is handled leniently, matching native ppx_deriving (issue #272): make is generated for the record members and non-record members are skipped. Two notes on the semantics that differ from native: builtin option/list detection matches on the bare Lident only, so a Stdlib.option-typed field becomes a required argument rather than an optional one; and a [@default] expression is inlined directly, so it can capture an argument that shares an earlier field's name.
Unsupported for now
- polymorphic variant row inheritance
enumand the rest ofppx_deriving.std
Roadmap From ppx_deriving
Native ppx_deriving supports more cases than this package. These are useful future milestones:
- standard containers:
ref,lazy_t - type aliases whose target type is not otherwise supported
- custom
[@nobuiltin]handling - expression extension support, e.g.
[%eq: t]/[%show: t] show's[@polyprinter]attribute
Attribution
ppx_deriving_melange reimplements a subset of ppx_deriving by whitequark and contributors. The derivers were written fresh for Melange, but their semantics, the documentation examples, and the test scenarios are closely modeled on the upstream plugins. ppx_deriving is distributed under the MIT license; its copyright notice is reproduced in this repository's LICENSE file.