Partial implementations using interfaces and some currying elbow grease

Hello! I’m cleaning up the rough edges of how I’ll be using OCaml starting this year. The situation below isn’t like anything I’ve seen around here, so I thought it could be worth a post. :slight_smile:

What I’m trying to do: I have an Intf.ml in my library, containing common module signatures like Printable and Jsonable. With this scheme, I’m explicit about implementing those conventions and it avoids duplicating those declarations and doc-comments. In the case of Jsonable however, the to_json and of_json functions are usually the same, so I’d like to provide that implementation, but its implementation depends on the modules’ specific to_yojson and of_yojson:

(* intf.ml *)
open! Base

module type Printable = sig
  type t
  val pp : t -> string
end

module type Jsonable = sig
  type t
  val to_json : t -> string
  val of_json : string -> (t, [> `JsonError of string ]) Result.t
  val to_yojson : t -> Yojson.Safe.json
  val of_yojson : Yojson.Safe.json -> (t, string) Result.t
end

module Json = struct
  let to_json to_yojson a = a |> to_yojson |> Yojson.Safe.to_string
  let of_json of_yojson s =
    try
      match s |> Yojson.Safe.from_string |> of_yojson with
      | Ok v -> Ok v
      | Error e -> Error (`JsonError e)
    with
    | Yojson.Json_error e -> Error (`JsonError e)
  ;;
end

(* stuff.mli *)
type t
include Intf.Jsonable with type t := t
include Intf.Printable with type t := t
val of_int : int -> t

(* stuff.ml *)
type t = int [@@deriving yojson]  (* Adds of_yojson and to_yojson *)
let to_json = Intf.Json.to_json to_yojson
let of_json = Intf.Json.of_json of_yojson
let pp s = Int.to_string s
let of_int i = i

Currying to_json and of_json is a very usable solution, but I’m disappointed with two aspects:

  1. Is there a way around creating that second Json module in the namespace for stashing those two functions? I see nothing obvious.

  2. I wish I could “include” something in stuff.ml t avoid declaring those two bindings entirely. I looked into learning about functors for this (especially with flambda promising to reduce/eliminate their run-time cost), but they seem to work backwards from what I’m trying to achieve: I want my various modules like Stuff to implement several interfaces as needed, some of which may include some helpful implementation bits. Functors would have to be used by the users of my modules to possibly achieve this, but I actually want to provide them with Stuff ready to use.

Am I missing something, or have I already reached the simplest solution with these compromises?

I think you’re looking for something like:

module type Arg : sig
  type t
  val to_yojson : t -> Yojson.Safe.json
  val of_yojson : Yojson.Safe.json -> t
end

module Make_jsonable(M : Arg) = struct
  let to_json t = t |> M.to_yojson |> Yojson.Safe.to_string
  let of_json = ...
end

and then, in stuff.ml:

module T = struct
  type t = int [@@deriving yojson]
end
include T
include Intf.Make_jsonable(T)

Or something like that.

1 Like

Thanks @bcc32 I hadn’t thought of including a generated module like that. Definitely cleaner than currying functions by hand. :slight_smile: