In general, it seems the standard practice for handling impure operations in OCaml is to push them to the edges of your codebase and then develop the core logic of your program in a pure way - i.e handling IO by either doing most of it near the entrypoint of the program or parameterizing the codebase over a monad.
However, it’s still not clear to me how to handle mutable components that arise within the functional core.
For an example, suppose you are interfacing with an external API with
interior mutability (there may be multiple instances of the type at any given point):
type t
val init: unit -> t
val update_state: t -> int -> unit
Do you simply ignore the external state in your data structures?
type s = { state: t; count: int }
let update (s: s) =
update_state s.state s.count;
{s with count = s.count + 1;}
Do you make the entire structure mutable to preserve a consistent interface?
type s = {state: t; mutable count: int}
let update (s: s) : unit =
update_state s.state s.count;
s.count <- s.count + 1
Do you write your types in pure form parameterized over the mutable
components, possibly at the cost of unnecessary copying?
type 'a s = {state: 'a; count: int}
let update (s: 'a s) f =
{count = s.count + 1; state = f s.state s.count}
Do you instead pull out the mutable components from the data-structure entirely requiring them as external inputs?
type s = {count: int}
let update (s: s) state =
update state s.count;
{count = s.count + 1}
What is the best practice for interfacing these impure components within your pure development? What if I have multiple distinct APIs that expose mutable operations? Is there a way to do this without sacrificing efficiency?