I can’t find a way to tag function parameters as constant in OCaml. Something like const in C++.
If I call a function that takes an array as a parameter, I have no guarantee that it will or will not mutate my array? I just have to trust it?
OCaml is often praised for its error-proof type system so I find it weird that a function cannot indicate if it is read-only or read-write. It seems to make mutable data even more dangerous to handle than in mainstream OO languages? Is there a reason for it, and what are the common practices around it?
One thing you might be able to try is giving your array a new abstract type, and only exposing the “immutable” parts of the Array module on that type:
module ImmutableArray : sig
type 'a t
val get: 'a t -> int -> 'a
val of_array: 'a array -> 'a t
end = struct
type 'a t = 'a array
let get = Array.get
let of_array = Fun.id
end
let () =
let arr = [| 1; 2; 3 |] in
let imm_arr = ImmutableArray.of_array arr in
Array.set imm_arr 0 (-1) (* doesn't compile *)
In OCaml, the read-only or write-only quality of a value is encoded as part of its type. Typically a read-only type would not expose any mutation operations and a write-only type would not expose any access operations.
In imperative languages, all data is mutable, and read-only, write-only annotations are used to restrict this mutability locally. In OCaml, almost all data is immutable, and only mutable record fields and arrays (+ its friends: byte arrays, flat float arrays, big arrays) can be mutated. The amount of mutable data in a typical OCaml program is thus much less than in your typical program written in an imperative language, so these kind of annotations are not as relevant.
As already mentioned in @zbaylin’s response, if you want a restrict the mutability of a type, you can do this typically by introducing a separate type that does not expose the operations that you want to restrict (eg writing or reading operations), but the simplest is not to use mutable data to begin with. If you must must use mutable data, you can either clearly specify your invariants (“trust it”), or introduce a suitable type to restrict the operations possible with it.
As an addition to @zbaylin’s response, you can encode readability and writability in phantom types.
module Ref : sig
type ('a,'b) t
val create : 'a -> ('a,[`RW]) t
val set : ('a,[<`WO|`RW]) t -> 'a -> unit
val get : ('a,[<`RO|`RW]) t -> 'a
val readonly : ('a,'b) t -> ('a,[`RO]) t
val writeonly : ('a,'b) t -> ('a,[`WO]) t
end = struct
type ('a,'b) t = 'a ref
let create x = ref x
let set t' x = t' := x
let get t' = !t'
let readonly x = x
let writeonly x = x
end
let () =
let v1 = Ref.create 20 in
let v2 = Ref.readonly v1 in
let v3 = Ref.writeonly v1 in
Printf.printf "%d\n" (Ref.get v1) ;
Printf.printf "%d\n" (Ref.get v2) ;
(* won't compile *)
(* Printf.printf "%d\n" (Ref.get v3) ; *)