Page
Library
Module
Module type
Parameter
Class
Class type
Source
ppx_deriving_melange is intended to be a Melange-compatible subset of ppx_deriving.
Supported derivers: eq, iter, map, ord, and show.
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 -> boolThe initial version supports classic variants with:
string, int, bool, float, char, bytes, int32, Int32.t, int64, Int64.t, and unitstring list and Foo.t liststring option and Foo.t optionstring array and Foo.t array(string, error) resultint * string[@equal ...]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 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 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 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) -> ...).
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 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.
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.
Comparison matches native ppx_deriving.ord:
None < Some _; Ok _ < Error _Stdlib.compareord 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 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.
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):
Zero, (One 5), (Pair (1, "a")); inline-record payloads as Item {rank = 1; label = "x"}{ name = "a"; count = 1 }%F, int32 as 7l, int64 as 9L, bytes through Bytes.to_string, unit as ()[1; 2], [|3|], (Some 1)/None, (Ok 1)/(Error "e") `All and `Name ("a")<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.
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]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 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.
enum and the rest of ppx_deriving.stdppx_derivingNative ppx_deriving supports more cases than this package. These are useful future milestones:
ref, lazy_t[@nobuiltin] handling[%eq: t] / [%show: t]show's [@polyprinter] attributefold deriver sharing the same traversal style as iter and mapppx_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.