[@@deriving unboxed_option] generates an allocation-free Option module for
unboxed scalar types and unboxed records in OxCaml.
'a option allocates a heap block on every Some x, and 'a or_null
requires 'a : value so it can't wrap unboxed types like float# or
#{ ... }. ppx_uopt does neither: some x is #(true, x) in tagged mode,
or x itself with one reserved sentinel value in sentinel mode.
Requires an OxCaml switch.
opam pin add ppx_uopt git+https://github.com/xl4624/ppx_uopt.git
(library
(name mylib)
(preprocess (pps ppx_uopt)))module Option : sig
type value = t
type t (* = value or #(bool * value), depending on representation *)
val none : t
val some : value -> t
val is_none : t -> bool
val is_some : t -> bool
val value : t -> default:value -> value
val value_exn : t -> value (* raises Invalid_argument on none *)
val unsafe_value : t -> value (* skip the is_none check *)
module Optional_syntax : sig
module Optional_syntax : sig
val is_none : t -> bool [@inline] [@zero_alloc]
val unsafe_value : t -> value [@inline] [@zero_alloc]
end
end
endThe nested Optional_syntax.Optional_syntax module wires this type up to Jane
Street's [%optional] syntax
extension, if you use it.
Scalars: float#, float32#, int32#, int64#, nativeint#, int8#,
int16#, int#, char#.
Unboxed tuples of scalars, e.g. #(int8# * float#) (see Unboxed-tuple
payloads).
Unboxed records, with any mix of scalars, immediates (int, bool, char,
float), contract types M.t (see Contract fields), or any
other value-layout field. A field whose type is syntactically an unboxed tuple
is rejected with a clear error instead: it isn't value-layout, so it can't
stand in for the placeholder tagged mode needs. Give it a name via its own
[@@deriving unboxed_option] type and use it as a contract field instead.
When no none = ... is given, the deriver uses an unboxed bool-tagged pair,
Option.t = #(bool * value):
type token = float# [@@deriving unboxed_option]
(* Option.none = #(false, 0.) *)
(* Option.some v = #(true, v) *)is_none only inspects the bool tag, so the payload of none doesn't have to
be a special value. Tagged mode works on any record, including ones with
string, array, or other heap-allocated fields.
When you supply a none = ... override, Option.t = value directly - no extra
tag, no padding. You're reserving one specific value to mean "none":
type token = int8# [@@deriving unboxed_option { none = #0s }]
(* Option.none = #0s; Option.some is the identity *)For records, write none = #{ ... }. Listed fields are is_none
discriminators; omitted fields are payload-only and is_none doesn't inspect
them. A single discriminator gives the simplest, fastest predicate.
type packed_pair = #{ x : int8#; y : int32# }
[@@deriving unboxed_option { none = #{ x = #12s; y = #0l } }]
(* is_none v = (v.#x = #12s) && (v.#y = #0l) - both must match *)type packed_pair = #{ x : int8#; y : float# }
[@@deriving unboxed_option { none = #{ x = #15s } }]
(* is_none v = (v.#x = #15s); y can be anything - including nan *)Opaque fields (anything that isn't a recognised scalar, immediate, or contract
type) are allowed too - omit them (payload-only) or list them (compared with
Stdlib.( = )):
type record = #{ id : int; tag : string }
[@@deriving unboxed_option { none = #{ id = -1 } }]
(* is_none v = (Stdlib.Int.equal v.#id (-1)); tag is payload-only *)type t = #(ty1 * ty2 * ...) works like a scalar payload, generalized to
several components. There are no field names, so - unlike unboxed records -
there's no partial override: a none = #( ... ) sentinel must supply every
component, and every component is always an is_none discriminator.
type packed_pair = #(int8# * int32#)
[@@deriving unboxed_option { none = #(#12s, #0l) }]
(* is_none v = match v with #(x, y) -> x = #12s && y = #0l - both must match *)Tagged mode (no override) works the same way as for scalars and records:
type packed_pair = #(int8# * char#)
[@@deriving unboxed_option]
(* Option.t = #(bool * #(int8# * char#)) *)Components must all be recognised unboxed scalars; a mix of scalar, immediate, or contract components isn't supported - use an unboxed record instead, which also gives you field names and partial overrides.
A field typed M.t is supported when M.Option provides the right interface.
This lets you nest types that already have their own Option module without
re-deriving.
val M.Option.none : M.Option.t
val M.Option.unsafe_value : M.Option.t -> M.t
val M.Option.is_none : M.t -> bool (* required only in sentinel mode *)When M is itself [@@deriving unboxed_option], this is automatic:
module Inner = struct
type t = float# [@@deriving unboxed_option]
end
module Outer = struct
type t = #{ inner : Inner.t; count : int# }
[@@deriving unboxed_option]
endFor types that don't derive unboxed_option, hand-roll a compatible module (see
test/test_contract_field.ml for a worked
example with a custom float wrapper).
type t = M.t [@@deriving unboxed_option] re-exports M.Option under the new
name. The alias path doesn't take a none = ... (delegation only).
type my_token = Some_module.t [@@deriving unboxed_option]
(* My_token.Option.t = Some_module.Option.t, all functions delegate. *)See test/ for more examples.