Is it possible simulate or use dynamic dispatch this way in OCaml?

I want to write a function that can take a list (or some other structure) of functions as an argument, and another argument whose type will be used to determine which function to use from the list.

For example, let add1 = (fun (x : int) → x + 1), and let f1 = (fun (x : string) → x), and I want to write a function func : some_function_list → arg2_type → output_type, where if its second argument is a string, it gives the argument itself, and if the second argument is an integer, it returns the argument plus 1.

The reason I say a list of function is that the list can be changed dynamically, so if the function func meets an arg2 that matches certain criteria, it might recursively call it self with a different first argument (i.e. func (some_new_function :: some_function_list) arg2).

Is it possible to do this with any of the advanced features of OCaml, or do I have to use some more dynamic language?

1 Like

Hello! Did you consider using OOP and polymorphy? OCaml supports this, also.

3 Likes

Types don’t exist at runtime in OCaml, and thus they cannot be used to change the behaviour of functions.
However, I have the impression that what you want might be an ordinary variant type

type t = 
| Int of int
| Float of float

Then your list of functions is just a function which pattern match on the variant type

let f = function
| Int x -> Int (x+1)
| String x -> String x

It is possible to use more advanced part of the type system (Generalized Abstract Data Types for instance) to enforce the invariant that Int _ constructors are transformed to int or Int _, but it is not clear if the increased complexity would be worth it without more information on your use case.

1 Like

I’m not sure what your goal is, but I think @olleharstedt and @octachron had very good suggestions (and I would try using pattern matching first to see if that technique works).

There is another simple way (a bit more complicated than pattern matching) to simulate dynamic dispatch which involves creating a record of closures/lambdas. It might fit your use case but I would try pattern matching first.

Here’s an example of creating a record of functions with internal state:

type 'a counter = {
  get: unit -> 'a;
  double: unit -> unit;
}

let make_int_counter initial_num =
  let internal_counter = ref initial_num in
  (* internal functions to manipulate state *)
  let get () =
    !internal_counter
  in
  let double () =
    internal_counter := !internal_counter * 2
  in
  { get; double; }

let make_string_counter initial_string =
  let internal_counter = ref initial_string in 
  let get () = 
    !internal_counter 
  in
  let double () =
    internal_counter := !internal_counter ^ !internal_counter
  in
  { get; double; }
  
(* make counter with initial count of 1 *)
let int_counter = make_int_counter 1
(* double counter: 1 * 2 = 2 *)
let () = int_counter.double ()
(* get current count and print it which is 2 *)
let () = print_int (int_counter.get())

(* make string counter with initial count of "hello" *)
let str_counter = make_string_counter "hello"
(* double counter so string is: "hello" ^ "hello" *)
let () = str_counter.double ()
(* print current count which is "hellohello" *)
let () = print_string (str_counter.get())

@octachron ADTs wouldn’t really work because the number of types should be able to be added later on dynamically. I want to be able to add new types and new functions supported by the dispatching function at runtime. For example I want to create a new type at runtime while the dispatching function is running, add a function on this new type to the list of functions carried by the dispatching function, and the dispatching function should be able to invoke that new function on arguments with that new type.

I don’t know much about Generalized Abstract Data Types. Can they do what I described above?

You can use GADTs combined with extensible types:

type ('input,'output) ty = ..

type ('a,'b) eq = Refl : ('a,'a) eq

type ('i1,'o1) isty = { isty : 'i2 'o2. ('i2,'o2) ty -> ('i1 * 'o1, 'i2 * 'o2) eq option }

type func = Func : ('input,'output) isty * ('input -> 'output) -> func

let func_at (type i o) (ty : (i,o) ty) (f:func) : (i -> o) option =
  let Func ({isty}, f) = f in
  match isty ty with
  | None -> None
  | Some Refl -> Some f

let find_func (type i o) (ty:(i,o) ty) (funcs : func list) : (i -> o) option =
  List.find_map (func_at ty) funcs

type (_,_) ty += Int2Int : (int,int) ty

let is_int2int (type i o) (f:(i,o) ty) : (int * int, i * o) eq option =
  match f with Int2Int -> Some Refl | _ -> None

let add_func = Func ({isty = is_int2int}, (fun x -> x + 1))


type (_,_) ty += String2String : (string,string) ty

let is_string2string (type i o) (f:(i,o) ty) : (string * string, i * o) eq option =
  match f with String2String -> Some Refl | _ -> None

let string_id_func = Func ({isty = is_string2string}, (fun x -> x))


let funcs = [add_func; string_id_func]

let () = assert (match find_func Int2Int funcs with
    | None -> false
    | Some f -> f 2 = 3)

let () = assert (match find_func String2String funcs with
    | None -> false
    | Some f -> f "x" = "x")

Note that if you extend ty multiple times at the same type you will get separate constructors, and isty defined using one constructor won’t recognize the others.

2 Likes

There is some confusion here: types are static descriptions of the behavior of programs. They don’t exist at runtime, and thus cannot be created at runtime.

What you can create at runtime are values of a given type. It is perfectly possible to have a triplet of a value type, pattern type and a list of function to call for DSL.

You might be also looking for extensible variant types, which are a more first-class way to create values that can be deconstructed in a pattern-match:

type t = ..
type t += Int of int

let print_int = function
| Int n -> Format.printf "%d@." n;  true
| _ -> false

type t += Float of float
let print_float = function
| Float f -> Format.printf "%g@." f; true
| _ -> false

let rec print printers x = match printers with
| a :: q -> if not (a x) then print printers x
|  [] -> Format.printf "<unknown>@."

let () = print  [print_float;print_int] (Int 5) 
2 Likes

I suspect we may be dealing with an x/y problem, so if you are actually trying to solve a particular problem you may want to describe that, in case there are idiomatic ways of solving it we could point to.

1 Like

GADT is overkill if OOP is good enough, I’d argue…

Here is a solution using the hmap library:

#require "hmap";;

let k1 = Hmap.Key.create ()
let k2 = Hmap.Key.create ()
let f1 x = x + 1
let f2 x = x ^ "I"
let m = Hmap.empty |> Hmap.add k1 f1 |> Hmap.add k2 f2

let y1 = Option.get (Hmap.find k1 m) 2
let y2 = Option.get (Hmap.find k2 m) "II"
;;

The hmap library uses GADTs internally, similar to what’s suggested already, but it’s fully hidden behind an abstract interface.

4 Likes