A ppx extension for easier implementation of conversion functions between almost identical types.
It's very common in the stable types idiom to have types like these:
module Stable = struct
module V1 = struct
type t =
{ x0 : X0.t
; x1 : X1.t
; ...
; xn : Xn.t
}
end
end
module V2 = struct
type t =
{ x0 : X0.t
; x1 : X1.t
; ...
; xnp1 : Xnp1.t
}
end
end
end
Implementing a conversion function is quite easy but the length of the implementation is linear to the size of record instead of linear to the number of differences between the records, i.e.
let v1_of_v2 { V2. x0 ; x1 ; ... ; xn ; xnp1 } =
ignore (xnp1 : Xnp1.t); (* forget the new value *)
{ V1. x0 ; x1 ; ... ; xn }
If there are more changes to the type the conversion function get unnecessarily complicated.
This extension automatically generates the appropriate conversion function given a description of the difference between the types:
module V2 = struct
type t =
{ x0 : X0.t
; x1 : X1.t
; ...
; xnp1 : Xnp1.t
} [@@deriving stable_record ~version:V1.t ~remove:[ xnp1 ]]
end
end
It supports records with conversion from the current type to the previous and from the previous type to the current type. It works for potentially-recursive variants, polymorphic variants, and records.
It supports all the possible ways you can change a record:
- adding new fields
- modifying existing fields, i.e. changing its type
- removing some fields
module V1 = struct
type t =
{ x0 : X0.t
; x1 : X1.t
}
end
module V2 = struct
type t =
{ x0 : X0.t
} [@@deriving stable_record ~version:V1.t ~add:[x1]]
end
let convert_to_v1 (v2 : V2.t) : V1.t =
V2.to_V1_t v2 ~x1:(X1.of_int 1234)
module V1 = struct
type t =
{ x0 : X0a.t
}
end
module V2 = struct
type t =
{ x0 : X0b.t
} [@@deriving stable_record ~version:V1.t ~modify:[x0]]
end
let convert_of_v1 (v1 : V1.t) : V2.t =
V2.of_V1_t v1 ~modify_x0:X0b.of_x0a
or
module V1 = struct
type t =
{ x0 : X0a.t
}
end
module V2 = struct
type t =
{ x0 : X0b.t
} [@@deriving stable_record ~version:V1.t ~set:[x0]]
end
let convert_of_v1 (v1 : V1.t) : V2.t =
V2.of_V1_t v1 ~x0:X0b.value
module V1 = struct
type t =
{ x0 : X0.t
}
end
module V2 = struct
type t =
{ x0 : X0.t
; x1 : X1.t
} [@@deriving stable_record ~version:V1.t ~remove:[x1]]
end
let convert_to_v1 (v2 : V2.t) : V1.t =
V2.to_V1_t v2
Please note that for variants you have to derive stable_variant
for the target type as well.
module V1 = struct
type t =
| X0 of X0.t
| X1 of X1.t
[@@deriving stable_variant]
end
module V2 = struct
type t =
| X0 of X0.t
[@@deriving stable_variant ~version:V1.t ~add:[X1]]
end
let convert_to_v1 (v2 : V2.t) : V1.t =
V2.to_V1_t v2
module V1 = struct
type t =
| X0 of X0a.t
[@@deriving stable_variant]
end
module V2 = struct
type t =
| X0 of X0b.t
[@@deriving stable_variant ~version:V1.t ~modify:[X0]]
end
let convert_of_v1 (v1 : V1.t) : V2.t =
V2.of_V1_t ~modify_X0:(fun v -> X0 (X0b.of_x0a v)) v1
module V1 = struct
type t =
| X0 of X0.t
[@@deriving stable_variant]
end
module V2 = struct
type t =
| X0 of X0.t
| X1 of X1.t
[@@deriving stable_variant ~version:V1.t ~remove:[X1]]
end
let convert_to_v1 (v2 : V2.t) : V1.t =
V2.to_V1_t ~remove_X1:(fun v -> X0 (X0b.of_int v)) v2
module V1 = struct
type t =
| Do
| Re
| Mi of int
[@@deriving stable_variant]
end
module V2 = struct
type t =
| Do
| Re
| Ew of int
[@@deriving stable_variant ~version:V1.t ~remove:[ Ew ] ~add:[ Mi ]]
end
let v2_of_v1 : V1.t -> V2.t = V2.of_V1_t ~remove_Mi:(fun i -> V2.Ew i)
If your types have parameters, e.g. 'a Foo.t
ocaml you won't be able to write
[@@deriving stable_variant ~version:('a V1.t) ...]
due to ocaml AST shape. We have a special, slightly more verbose syntax for it:
[@@deriving stable_variant ~version:[%stable: ('a V1.t)] ...]
If your type is recursive, the invocation of t
looks like it didn't
change but it actually did. That t
means V1.t
in the old type but
V2.t
in the new type. Fortunately, we already know the conversion
from V1.t
to V2.t
recursively, so we invoke the constructed
conversion function automatically in each place that contains a t
.
module V1 = struct
type t = One | S of t
[@@deriving stable_variant]
end
module V2 = struct
type t = Zero | S of t
[@@deriving stable_variant ~version:V1.t ~remove:[ Zero ] ~add:[ One ]]
end
let v2_of_v1 : V1.t -> V2.t = V2.of_V1_t ~remove_One:(fun () -> S Zero)
In order to figure out where to apply the conversion function, we inspect the type of the arguments to the constructor. We are able to discover recursive invocations inside
- pervasives (
lazy_t
,ref
,list
,option
, andarray
) - tuples (e.g.
t * int
) - containers that implement the stable module interface (e.g.
t Or_error.t
). They need to have a map function
It should be easy to add support for polymorphic variants and object types.
Unfortunately, we can't implement mapping for t
s inside other abstract
polymorphic types, so those will constructors will need to be added to the
modify
section.
module Container_without_map = struct
type 'a t = 'a option
end
module V1 = struct
type t = { bad : t Container_without_map.t }
end
module V2 = struct
type t = { bad : t Container_without_map.t }
[@@deriving stable_record ~version:V1.t ~modify:[ bad ]
end
Polymorphic variants can be treated identically to normal variants. You can add, modify, remove, and rename constructors between polymorphic variants. Support is included for parametric and recursive polymorphic variants as well. Two cases are not supported: polymorphic variants that inherit from some other type, and polymorphic variants with conjuctive/intersection types.
You must derive stable_variant
for the target type like with normal variants.
You cannot derive stable_variant
on a type that uses a type alias for some or all of
its constructors:
module V1 = struct
type t = [ `A ] [@@deriving stable_variant]
end
module V2 = struct
type t =
[ V1.t
| `B ] (* You cannot derive stable variant on V2.t *)
end