Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Function to create one of two record types dynamically

Tags:

ocaml

I have this OO situation I'm trying to implement in Ocaml: Two classes X1 and X2, both subtyping X (X1 <: X and X2 <: X) and I want to write a function that dynamically returns an X that will either be an X1 or an X2.

However I heard it's usually good to avoid classes in Ocaml and use modules instead, so I'm trying to represent my problem like this (overly simplified but still makes the point): Two modules X1 and X2, and I want my function to dynamically decide between returning either an X1.t or an X2.t.

module type X = sig
  type choice
  type t
  (* some methods we don't care about in this instance, like
     val modifySomething : t -> t *)
end

module Xbase = struct
  type choice = Smth | SmthElse
end

module X1 = (
struct
  include Xbase
  type t = { foo : int; bar : int }
end : X)

module X2 = (
struct
  include Xbase
  type t = { foo : int; star : int }
end : X)

module XStatic =
struct
  (* construct either an X1.t or X2.t from the string *)
  let read : string -> 'a =
    function
    | "X1" -> { X1.foo = 0, bar = 0 }
    | "X2" -> { X2.foo = 1, star = 1 }
end

But this fails with Error: Unbound record field label X1.foo in the read function. I've tried different ways of arranging it, such as using let open X1 in { foo = 0, ... } but to no avail.

Is my approach to this fundamentally wrong (i.e. should I be using classes because this is impossible/unpractical with modules) or am I just missing something trivial?

Edit: Clarified the problem I'm trying to solve and renamed module X to module XBase to differentiate it from module type X.

like image 790
Dan Avatar asked Feb 04 '26 06:02

Dan


1 Answers

The simplest approach is to use a sum type (disclaimer: I didn't try to compile the code):

module X1 = struct
  type t = { foo : int; bar : string }
  let modify_foo = ...
end
module X2 = struct
  type t = { foo : int; star : bool }
  let modify_foo = ...
end
type x1_or_x2 =
  | Left of X1.t
  | Right of X2.t

let read = function
  | "X1" -> Left { X1.foo = 1; bar = "bar" }
  | "X2" -> Right { X2.foo = 1; star = true }

let modify_foo = function
  | Left x1 -> Left (X1.modify_foo x1)
  | Right x2 -> Right (X2.modify_foo x2)

If you want to take advantage of the fact that X1.t and X2.t share some common structure, you can factorize the types. The idea is that they are isomorphic to product types, respectively common_part * specific_to_x1 and common_part * specific_to_x2. The x1_or_x2 type is therefore (common * specific_to_x1) + (common * specific_to_x2), which is equivalent to common * (specific_to_x1 + specific_to_x2).

type common = { foo : int }
let modify_foo_common : common -> common = ...

type specific_x1 = { bar : string }
type specific_x2 = { star : bool }

type x1_or_x2 = common * specific_x1_or_x2
and specific_x1_or_x2 =
  | Left of X1.t
  | Right of X2.t

let read = function
  | "X1" -> { foo = 1 }, Left { bar = "bar" }
  | "X2" -> { foo = 1 }, Right { star = true }

let modify_foo (common, specific) = (modify_foo_common common, specific)

This way, definitions that act on the common part are not duplicated but can be declared once.

PS: also see this very related question in which you could be interested and which has a nice answer (lenses!): Ptival: Statically “extend” a record-ish data type without indirection hassle

like image 109
gasche Avatar answered Feb 06 '26 13:02

gasche