A ppxlib-based OCaml PPX that lets you compose module-type signatures inline
using a [@@mixins ...] attribute on a type declaration.
OCaml's include M with type t := foo idiom is the standard way to "mix in"
the interface of a module type while substituting the primary type. Writing it
by hand is repetitive, especially when several mixins share the same t :=
substitution. ppx_mixins lets you express this intent concisely:
module type S = sig
type my_type [@@mixins Printable + Comparable]
endexpands to:
module type S = sig
type my_type
include Printable with type t := my_type
include Comparable with type t := my_type
endAdd ppx_mixins to your project's dependencies and preprocessors in dune:
(library
(name my_lib)
(preprocess (pps ppx_mixins)))
type <name> [@@mixins <mixin> [+ <mixin> ...]]
where each <mixin> is:
ModuleTypeName
ModuleTypeName (param [; param ...])
and each param is one of:
| Syntax | Meaning |
|---|---|
name = Type |
equality constraint (with type name = Type) |
name := Type |
destructive substitution (with type name := Type) |
Type can be any of:
| Form | Example |
|---|---|
| plain constructor | string, My_module.t |
unit |
unit |
| single-param applied | int list, string option |
| multi-param applied | (int, string) result |
Multiple mixins are separated by +. Multiple params within a mixin's
parentheses are separated by ;.
Mixin module types conventionally name their primary type t. If you do not
provide any constraint that binds t (either t = ... or t := ...),
ppx_mixins automatically prepends type t := <name> for you. To suppress
this, supply an explicit t constraint:
type my_type [@@mixins Weird (t := my_type; u := my_type)]
(* no extra t := is injected *)module type S = sig
type my_type [@@mixins Printable]
end
(* expands to:
type my_type
include Printable with type t := my_type *)module type S = sig
type my_type [@@mixins Printable + Comparable]
end
(* expands to:
type my_type
include Printable with type t := my_type
include Comparable with type t := my_type *)module type S = sig
type my_type [@@mixins Mappable (key = string; value = int)]
end
(* expands to:
type my_type
include Mappable with type key = string
and type value = int
and type t := my_type *)module type S = sig
type my_type [@@mixins Mappable (key = string; value = int; t := my_type)]
end
(* expands to:
type my_type
include Mappable with type key = string
and type value = int
and type t := my_type *)module type S = sig
type my_type [@@mixins Container (elt = int list)]
end
(* expands to:
type my_type
include Container with type elt = int list and type t := my_type *)Multi-parameter constructors use the same parenthesised tuple syntax as in type expressions:
type my_type [@@mixins Mapper (result_ = (int, string) result)]When the primary type in the expanded signature should simply be named t,
you can skip the sig ... end wrapper entirely and write:
module type S = [%mixins Printable + Comparable]which expands to:
module type S = sig
type t
include Printable with type t := t
include Comparable with type t := t
endAll the same mixin syntax (params, constraints, +-separated lists) works
identically to the [@@mixins] form. The only difference is that the
implicit primary type is always t -- there is no way to choose a different
name with the shorthand. If you need a different primary type name, use the
full [@@mixins] attribute form.
The rewriter is implemented as a global Ast_traverse.map registered via
Driver.V2.register_transformation. It walks every sig block in both .ml
and .mli files and runs a three-phase pipeline on each annotated type
declaration:
- Parse — flatten the
Pexp_apply "+"expression tree into amixin list. - Inject — prepend
t := <type_name>to any mixin that has notbinding. - Desugar — emit
psig_type(without the attribute) followed by onepsig_includeper mixin.
See lib/ppx_mixins.ml for the full documented source.
- Tuple types (
int * string) and function types (int -> string) on the RHS of a constraint are not supported. Wrap them in a type alias if needed. - Only
sigitems are rewritten;[@@mixins]on a type inside astructis not meaningful and will be left untouched (ppxlib will warn about an unused attribute).