Interfacing mutable operations within a functional core - program design

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?

1 Like

I would opt for the mutable state. All the other signature would be confusing in a public library :

For example the signature

val update: t -> int -> t

Falsly give the impress that a new object is created, and that we can build an history by keeping each successive state :

type history = t list
let  previous
  : history -> (t * history) option
  = function
  | [] -> None
  | t::tl -> Some (t, tl)

Of course, this will not work as expected, as each state contains the same informations.

If you choose a signature which reflect the mutation (by using unit as return value in val update_state: t -> int -> unit), any user of your library will be informed that the state cannot be copyed, and will not be lead to wrong utilisation of you function.

I agree that this would probably be the most accurate type that I could give this interface, but it also means that the mutability will propagate throughout the codebase, rather than being restricted to the edges of the program. This is particularly problematic if this data-structure happens to be a central part of the core logic, in which case the entire project would then become imperative.

I was hoping there might be some smarter way of designing the system such that I could both capture the mutability of this type while also using it in a functional way - for example, a simple approach might be to provide a monadic IO-like wrapper around the type t to capture the semantics of its external state in the types, but then this raises problems of how to handle multiple mutable types - do I make a monad wrapper for each one? I could see this quickly becoming impractical.