ppx_stable

Stable types conversions generator
README

A ppx extension for easier implementation of conversion functions between almost
identical types.

Overview

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

Details

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 and records.

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

Adding a field

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)

Modifying a field

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

Removing a field

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

Variants

Please note that for variants you have to derive stable_variant for the target type as well.

Adding a constructor

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

Modifying a constructor

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

Removing a constructor

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

Renaming a constructor

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)

Recursive types

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, and array)

  • 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 ts 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
Install
Published
21 Mar 2022
Sources
ppx_stable-v0.15.0.tar.gz
sha256=90b4e87a590c695938db2b148aeb4a6543d32525ee826432812034470c012bb3
Dependencies
ppxlib
>= "0.23.0"
dune
>= "2.0.0"
base
>= "v0.15" & < "v0.16"
ocaml
>= "4.08.0"
Reverse Dependencies
ppx_jane
>= "v0.15.0"