package ppx_deriving_melange
Install
dune-project
Dependency
Authors
Maintainers
Sources
sha256=2f1ce7252387cade8fa60c591ddcdc96834745b496ccbd7004652bf8db72c0dd
sha512=02522414cb0286fe14953a642f40446a2493a6dd15dcd8f6d9d89ea7c89375e28df0c29522dca4c2abed62dea4c79305d84d5b40331b315429a0575cb373940c
doc/README.html
ppx_deriving_melange
ppx_deriving_melange is intended to be a Melange-compatible subset of ppx_deriving.
Supported derivers: eq, iter, 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.
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.
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- a
foldderiver sharing the same traversal style asiterandmap
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.