Emulating Type Classes in OCaml

Hello,

While working on Raven with heterogeneous columns of a dataframe, I encountered

  let cumsum t name =
    match get_column t name with
    | Some (Col.P (dtype, tensor)) -> (
        match dtype with
        | Nx.Float32 ->
            let arr : float array = Nx.to_array tensor in
            let result = Array.copy arr in
            for i = 1 to Array.length result - 1 do
              result.(i) <- result.(i - 1) +. result.(i)
            done;
            Col.P (dtype, Nx.create dtype [| Array.length result |] result)
        | Nx.Float64 ->
            let arr : float array = Nx.to_array tensor in
            let result = Array.copy arr in
            for i = 1 to Array.length result - 1 do
              result.(i) <- result.(i - 1) +. result.(i)
            done;
            Col.P (dtype, Nx.create dtype [| Array.length result |] result)
        | Nx.Int32 ->
            let arr : int32 array = Nx.to_array tensor in
            let result = Array.copy arr in
            for i = 1 to Array.length result - 1 do
              result.(i) <- Int32.add result.(i - 1) result.(i)
            done;
            Col.P (dtype, Nx.create dtype [| Array.length result |] result)
        | Nx.Int64 ->
            let arr : int64 array = Nx.to_array tensor in
            let result = Array.copy arr in
            for i = 1 to Array.length result - 1 do
              result.(i) <- Int64.add result.(i - 1) result.(i)
            done;
            Col.P (dtype, Nx.create dtype [| Array.length result |] result)
        | _ -> failwith "cumsum: unsupported numeric type")
    | _ -> failwith "cumsum: column must be numeric"

Upon searching for something comparable to templates in C++, I came across type class in Haskell.

anyMax :: MyOrdered a => [a] -> Maybe a

where any type a instantiating MyOrdered works with anyMax.

The closest resources for OCaml I found were: Implementing type-classes as OCaml modules and Haskell type-classes in OCaml and C++ but I liked neither of them. The cleanest solution I thought of is

Edit. Added an illustration of Column motivated by Raven.

Column
type _ col =
    | I : int list -> int col
    | B : bool list -> bool col

module type Col = sig
    type t
    val print : t col -> unit
end

(* Integer instance *)
module IntCol : Col with type t = int = struct
    type t = int
    let print (I lst) = List.iter (fun x -> Printf.printf "%i " x) lst
end

(* Boolean instance *)
module BoolCol : Col with type t = bool = struct
    type t = bool
    let print (B lst) = List.iter (fun x -> Printf.printf "%B " x) lst
end

(* map witness to corresponding module *)
let colMod_of : type a. a col -> (module Col with type t = a) = function
    | I _ -> (module IntCol)
    | B _ -> (module BoolCol)

(* abstract printer *)
let anyPrint : type a. a col -> unit =
    fun col ->
        let (module Mod : Col with type t = a) = colMod_of col in
        Mod.print col

(* Example *)
let () =
    anyPrint @@ I [1; 2; 3];
    print_endline "";
    anyPrint @@ B [true; false; true];
    print_endline ""
Sorting
(* abstract order *)
module type ORDERED = sig
    type t
    val compare : t -> t -> int
end

(* integer instance *)
module IntOrd : ORDERED with type t = int = struct
    type t = int
    let compare = compare
end

(* string instance *)
module StringOrd : ORDERED with type t = string = struct
    type t = string
    let compare = compare
end

(* type witness *)
type _ ty =
    | Int : int ty
    | String : string ty

(* map witness to corresponding module *)
let ord_of : type a. a ty -> (module ORDERED with type t = a) = function
    | Int -> (module IntOrd)
    | String -> (module StringOrd)

(* abstract sorting *)
let anySort : type a. a ty -> a list -> a list =
    fun ty lst ->
        let (module Ord : ORDERED with type t = a) = ord_of ty in
        List.sort Ord.compare lst

(* example *)
let () =
    let sorted_ints = anySort Int [5; 1; 3; 2] in
    sorted_ints |> [%show: int list] |> Printf.printf "%s\n";

    let sorted_strings = anySort String ["cat"; "dog"; "apple"] in
    sorted_strings |> [%show: string list] |> Printf.printf "%s\n";

Discussion.

  • What is your subjective opinion of my solution?
  • How far does it align with the design philosophy of OCaml?
  • How do OCaml programmers deal with the problem I faced?
  • Do you recommend any resource for design patterns in OCaml?

Edit. The discussion is about the design pattern in general, not sorting or Raven.

2 Likes

You don’t need IntOrd and StringOrd. The standard library modules Int and String already fulfill the constraints of your module type ORDERED.

In fact you don’t even need the module type ORDERED, the standard library already has Set.OrderedType and Map.OrderedType which exactly match the signature you need.

Thank you.
I am giving ORDERED code as an example of the design pattern I might use with Raven or other OCaml code. My question is about the design pattern itself.

The design pattern obviously works but since without implicits you have to specify your implementation for the type in question, you would need a case where the additional procedure enabling you to write ‘let sorted_ints = anySort Int [5; 1; 3; 2]’ is worth it over the simpler ‘let sorted_ints = List.sort Int.compare [5; 1; 3; 2]

1 Like

Thank you. I did learn from you the nice built-in functions in OCaml library.
My question is around the design pattern but in general contexts, like the Raven code snapshot I quoted.

I don’t know Raven. Note that you said you were looking for “something comparable to templates in C++” whereas your solution used runtime dispatch via a pattern match. For compile time consider functors (or for your sorting example just open by hand the module providing the type based implementation).

Thank you for the note @cvine. Do you recommend Functors over Runtime Dispatch? What are the pros or cons of each? Any recommended resource?

A key difference between your pattern and Haskell’s or Scala’s type classes is that they are open to extension (I can implement Ordered for any type and all associated functions will continue to work). In your example you need to match on the type to map it to the correct module, thus making it closed for extension.

Yeah, the type system of Haskell is stronger. Is there a better solution, more native to OCaml’s design? Any caveat of matching types as I did?

See https://www.cl.cam.ac.uk/\~jdy22/papers/modular-implicits.pdf

For array-like data, my opinion is that you are unlikely to have more than a small algebra of types. Thus, it is probably better to lean on the data side of the expression problems and have a type representative.

For instance, re-using few functions from GitHub - Octachron/datagrade: Prototype for gradually typed dataframes :

type _ typ =
  | Int: (int * int array) typ
  | Float: (float * Float.Array.t) typ
  | Bool: (bool * bool array) typ
  | String: (string * string array) typ
  | Char: (char * bytes) typ

let pp (type x y ) (ty:(x*y) typ) ppf (x:x) = match ty with
  | Int -> Format.pp_print_int ppf x
  | Float -> Format.pp_print_float ppf x
  | Bool -> Format.pp_print_bool ppf x
  | String -> Format.pp_print_string ppf x
  | Char -> Format.pp_print_char ppf x

...

module Dynarray = struct

  type t = A: ('a * 'b) typ * 'b -> t


  let pack (type a b) (ty: (a * b) typ) (x:b) = A(ty,x)
  let unpack (type a b) (ty:(a * b) typ) (A(ty',x)): b =
    match  ty === ty' with
    | Type.Equal -> x

  let len (A(ty,x)) = match ty with
    | Float -> Float.Array.length x
    | Char -> Bytes.length x
    | Int -> Array.length x
    | Bool -> Array.length x
    | String -> Array.length x

  let create (type a b) (ty:(a*b) typ) n: b =
    match ty with
    | Float -> Array.Floatarray.create n
    | Char -> Bytes.create n
    | Int -> Array.make n 0
    | Bool -> Array.make n false
    | String ->Array.make n ""

...

end

If you want to have a more open family of types, you can switch the type typ to an open variant, and have a registry of functions with default functions operating on the predefined variants.

1 Like

See https://www.cl.cam.ac.uk/\~jdy22/papers/modular-implicits.pdf

Thank you!

If the escape character \~ did not work with anyone, they may try: https://www.cl.cam.ac.uk/~jdy22/papers/modular-implicits.pdf

It is a nice reading.

I discovered an ongoign effort for implementing type-class like behavior in #13275. It has a good background also.

Thank you for the suggestion, @octachron. If you have any other recommended practices or use-cases using open variants, kindly let us know.

Ironically, I just noticed the dual of this thread over at the Haskell Discourse:

7 Likes