Modeling Rust trait objects subtyping relation via OCaml polymorphic variants

I’m trying to model Rust subtyping around trait objects in OCaml. I have made a universal smart pointer for ocaml-rs (repo here if someone is interested), which allows to store Rust objects or Rust trait objects within single OCaml Value, and pass it back to Rust stubs. The thing comes with dynamic type information registry which allows Rust side to coerce smart pointer to concrete type into smart pointer to trait object (only object-safe traits can be used for this). With some magic sprinkled on top, I can get a list of polymorphic variant constructors that represent which traits certain boxed object provides. What I’m trying to do is to improve developer experience and provide all methods that come from implemented traits right into final module, e.g. in example below I want to_string method from Display to be available directly within Error module, as Error trait includes Display trait from Rust side. It’s possible to call Display.to_string on Error.t with explicit coercion, but that is hard to discover when glancing at methids available in Error module in one’s IDE - one would have to go read the constructors in type definition.

module Display = struct
  type nonrec t = [ `Std_fmt_display | `Core_marker_send ] Ocaml_rs_smartptr.Rusty_obj.t

  external to_string : t -> string = "rstdlib_display_to_string"
end

module Error = struct
  type nonrec t =
    [ `Ocaml_stdlib_error_wrapper_error_wrapper
    | `Core_marker_send
    | `Std_error_error
    | `Std_fmt_display
    | `Core_fmt_debug
    ]
      Ocaml_rs_smartptr.Rusty_obj.t

  (* Attempt to include functions from Display as variant contractors are
     compatible fails :(
     
    In this `with' constraint, the new definition of t
    does not match its original definition in the constrained signature:
    Type declarations do not match:
      type t = t
    is not included in
      type t =
          [ `Core_marker_send | `Std_fmt_display ]
          Ocaml_rs_smartptr.Rusty_obj.intf
    The type
      t/2 =
        [ `Core_fmt_debug
        | `Core_marker_send
        | `Ocaml_stdlib_error_wrapper_error_wrapper
        | `Std_error_error
        | `Std_fmt_display ] Ocaml_rs_smartptr.Rusty_obj.intf
    is not equal to the type
      [ `Core_marker_send | `Std_fmt_display ] Ocaml_rs_smartptr.Rusty_obj.intf
    The second variant type does not allow tag(s)
    `Core_fmt_debug, `Ocaml_stdlib_error_wrapper_error_wrapper, `Std_error_errorocamllsp
  *)
  include (Display : module type of Display with type t := t)

  external source : t -> t option = "rstdlib_error_source"
end

let () =
  let error : Error.t = Obj.magic () in
  (* Explicit coercion below works *)
  let _ : string = Display.to_string (error :> Display.t) in
  ()
;;

Rusty_obj.intf is defined as follows in Rusty_obj.mli:

type -'tags intf
type 'a t = ([> ] as 'a) intf

Probably something is wrong with that covariant/contravariant modifiers for tags? Unfortunately I can’t wrap my head around this… Is there some way to achieve inclusion of Display module into Error module given that constructors are compatible? I could of course write a bunch of wrapper functions by hand, but that won’t scale. Set of constructors along with external method signatures are actually generated by ocaml-gen crate and I can’t modify it to produce something more complex from Rust side. Thus I wanted to include generated modules into my own, potentially add some convenience functions on top of generated ones, and add those include relations for convenience. Any help on this would be greatly appreciated.

1 Like

A simple fix might be to ensure that to_string accept all object with at least the right traits:

module Display = struct
  type cap = [`Std_fmt_display | `Core_marker_send ]
  type 'a t = ([> cap ] as 'a) Ocaml_rs_smartptr.Rusty_obj.t
  external to_string : _ t -> string = "rstdlib_display_to_string"
end
module Error = struct
  include Display
  type cap =
    [ `Ocaml_stdlib_error_wrapper_error_wrapper
    | `Std_error_error
    | `Core_fmt_debug
    | Display.cap
    ]
   type 'a t = ([> cap] as 'a) Ocaml_rs_smartptr.Rusty_obj.t
  external source : 'a t -> 'a t option = "rstdlib_error_source"
end
1 Like

Thanks @octachron , that seems to do the trick!

So now I have to tweak my stubs generator to produce Stubs.ml as follows:

module Display = struct
  type nonrec 'cap t =
    ([> `Std_fmt_display | `Core_marker_send ] as 'cap) Ocaml_rs_smartptr.Rusty_obj.t

  external to_string : _ t -> string = "rstdlib_display_to_string"
end

module Error = struct
  module Wrapper = struct
    type nonrec 'cap t =
      ([> `Ocaml_stdlib_error_wrapper_error_wrapper
       | `Core_marker_send
       | `Std_error_error
       | `Std_fmt_display
       | `Core_fmt_debug
       ]
       as
       'cap)
        Ocaml_rs_smartptr.Rusty_obj.t
  end

  type nonrec 'cap t =
    ([> `Std_error_error | `Core_marker_send ] as 'cap) Ocaml_rs_smartptr.Rusty_obj.t

  external source : _ t -> _ Wrapper.t option = "rstdlib_error_source"
end

which I can then wrap in higher level API like this:

module Display = struct
  include Stubs.Display
end

module Error = struct
  include Stubs.Error

  module Wrapper = struct
    include Display
    include Stubs.Error.Wrapper
  end
end

And use Display method right inside Error.Wrapper without any coerciens!

let () =
  let error : _ Error.Wrapper.t = Obj.magic () in
  let _ : string = Error.Wrapper.to_string error in
  ()
;;