Use of functors to approximate F# statically resolved type parameters

Hi,

I am learning OCaml coming from F#. In F#, to calculate the average of an array whose element type supports addition and division, one can write

let inline average (arr: 'a[]) : 'a
    when ^a : (static member DivideByInt : ^a * int -> ^a)
    and  ^a : (static member (+) : ^a * ^a -> ^a)
    and  ^a : (static member Zero : ^a)
    =
    if Array.length arr = 0 then (LanguagePrimitives.GenericZero) else
    LanguagePrimitives.DivideByInt (Array.fold (+) (LanguagePrimitives.GenericZero) arr) (Array.length arr)

My understanding is that in OCaml, one would have a module type like so:

module type Averagable = sig
  type 'a t

  val divide_by_int : 'a -> int -> 'a
  val plus : 'a -> 'a -> 'a
  val zero : 'a
end

My question is how the corresponding function would be written:

let average arr =
  ???

First, Averagable should look like this:

module type Averagable = sig
  type t
  val divide_by_int : t -> int -> t
  val plus : t -> t -> t
  val zero : t
end

Then average will look something like this:

let average (type t) (module A : Averagable with type t = t) (arr : t array) : t = 
  Array.fold ~init:A.zero ~f:A.plus arr

(The code above uses Jane Street’s Base/Core library.)

3 Likes

While @smolkaj’s answer is a correct and direct implementation of your F# code, it might be nicer if your code can interplay with existing abstractions in the OCaml infrastructure. For example,

 open Base

let average (type a) (module T : Floatable.S with type t = a) xs =
  Array.fold ~init:0. ~f:(fun s x -> s +. T.to_float x) xs /.
  Float.of_int (Array.length xs)

and now it could be used with any existing numeric data in Base/Core

average (module Int) [|1;2;3;4|];;
- : Base.Float.t = 2.5

and even adapted to non-numbers,

let average_length = average (module struct
    include String
    let to_float x = Float.of_int (String.length x)
    let of_float _ = assert false
  end)

The latter example shows that we requested more interface than need, a cost that we have to pay for using an existing definition. In cases when it matters, you can specify the specific interface, e.g.,

module type Floatable = sig
  type t
  val to_float : t -> float
end

let average (type a) (module T : Floatable with type t = a) xs =
  Array.fold ~init:0. ~f:(fun s x -> s +. T.to_float x) xs /.
  Float.of_int (Array.length xs)

But we reached the point where using first class modules is totally unnecessary. Our interface has only one function, so the following definition of average, is much more natural

let average xs ~f =
  Array.fold ~init:0. ~f:(fun s x -> s +. f x) xs /.
  Float.of_int (Array.length xs)

it has type 'a array -> f:('a -> float) -> float and computes an average of f x_i for all elements in the array.

4 Likes