When to use iarray instead of array?

The following isn’t meant as a suggestions how to idiomatically write code, but to understand the underlying nature of “I accept an array that I won’t modify” in an as-abstract-as-possible way (i.e. without demanding additional guarantees made by the caller as pointed out in my previous post). I think the answer is abstract types. I would probably express it as follows:

module type Parray = sig
  type 'a t

  val of_array : 'a array -> 'a t
  val of_iarray : 'a Iarray.t -> 'a t
  val to_array : 'a t -> 'a array
  val to_iarray : 'a t -> 'a Iarray.t
  val get : 'a t -> int -> 'a
end

module Parray1 : Parray = struct
  type 'a t = 'a array

  let of_array = Fun.id
  let of_iarray = Iarray.to_array
  let to_array = Fun.id
  let to_iarray = Iarray.of_array
  let get = Array.get
end

module Parray2 : Parray = struct
  type 'a t = 'a Iarray.t

  let of_array = Iarray.of_array
  let of_iarray = Fun.id
  let to_array = Iarray.to_array
  let to_iarray = Fun.id
  let get = Iarray.get
end

module M (Parr : Parray) = struct
  let algorithm : int Parr.t -> int =
    fun arr ->
      Parr.get arr 0 + Parr.get arr 1
end

module M1 = M (Parray1)
module M2 = M (Parray2)

let _ =
  let a1 : _ array = [| 12; 13; 102 |] in
  assert (M1.algorithm (Parray1.of_array a1) = 25);
  assert (M2.algorithm (Parray2.of_array a1) = 25);
  let a2 : _ Iarray.t = [| 12; 13; 102 |] in
  assert (M1.algorithm (Parray1.of_iarray a2) = 25);
  assert (M2.algorithm (Parray2.of_iarray a2) = 25)

Here, the algorithm (algorithm) is provided as part of a functor, allowing it to work both with array and Iarray.t, depending on what the caller provides.

In particular, if a caller already happens to have a mutable array, we can instantiate the functor such that converting the mutable array into the abstract type is a no-op: Parray1.of_array a1 is simply a1. Accordingly, if a caller happens to have an immutable array, we can instantiate the functor such that converting the immutable array into the abstract type is a no-op: Parray2.of_iarray a2 is simply a2.

Now I don’t want to propose providing a generic interface like Parray above. But what I wonder is: Perhaps I should consider whether I’m using array or Iarray.t as an implementation detail that should not be exposed in my API anyway and instead use abstract types (as also suggested to me in another context).

Getting back to my Gauss solver, perhaps an equation should be an abstract type that can be constructed by lists, mutable arrays, immutable arrays, etc. And my implementation decides when or if a conversion is required. Something like:

module type Number = sig
  type t
  (* ... *)
end

module Equation (Num : Number) : sig
  type t

  val make : Num.t Seq.t -> Num.t -> t
  val make_array : Num.t array -> Num.t -> t
  val make_iarray : Num.t Iarray.t -> Num.t -> t
end = struct
  (* The choice for [t] might have an effect on performance,
     depending on whether users of the API primarily use
     [make_array] or [make_iarray]. *)
  type t = Num.t Iarray.t * Num.t

  let make lhs rhs = (Iarray.of_seq lhs, rhs)
  let make_array lhs rhs = (Iarray.of_array lhs, rhs)
  let make_iarray lhs rhs = (lhs, rhs)
end

I feel like choosing array vs Iarray.t is not really a matter of designing interfaces but rather a matter of what kind of implementation of arrays I want to (or should) use in a specific scenario. Any thoughts on that?


P.S.: Note that in my example above, make_array could or could not demand in its documentation/contract that the array is held constant while the resulting Equation.t is used.