简体   繁体   中英

Is there an idiomatic way to do implicit local state in OCaml?

I want to write some code that builds a thing using some local state. For example, consider the following code that uses local state to generate sequential integers:

type state = int ref 

let uniqueId : (state -> int) = 
fun s -> incr s; !s

let makeThings : ((state -> 'a) -> 'a) =
fun body -> body (ref 0)

let () =

  let x1 = makeThings(fun s ->
    let i = uniqueId s in     (* 1 *)
    i 
  ) in
  print_int x1; print_newline (); (* Prints 1 *)

  (* Each makeThings callback gets its own local state.
     The ids start being generated from 1 again *)
  let x2 = makeThings(fun s ->
    let i = uniqueId s in     (* 1 *)
    let j = uniqueId s in     (* 2 *)
    i + j
  ) in
  print_int x2; print_newline (); (* Prints 3 *)

  ()

I'm curious if there is a way to make that s state parameter inside the makeThings callback implicit, so that I don't need to type it over and over and so its guaranteed that all the uniqueId calls get passed the same state prameter. For example, in Haskell you could use monads and do-notation to end up with code along the lines of

makeThings $ do
  i <- uniqueId
  j <- uniqueId
  return (i + j)

In Ocaml, the only things that come to my mind are making s a global variable (very undesirable) or trying to emulate Haskell's monadic interface, which I fear is going to be a lot of work and end up with slow code tahts also ugly due to a lack of do-notation. Is there an alternative I haven't thought of?

Monads work in OCaml too. You can even have a do-notation, thanks to pa_monad_custom syntax extension. Although, in most cases having just an infix bind operator, ie >>= will be enough to write a fancy code.

You code looks like a strange mixture of monadic style + reference... If you want to restrict your local states changing only via specific ways, you should hide them in local contexts:

let make_unique_id init = 
  let s = ref (init - 1) (* :-) *) in
  fun () -> 
    incr s;
    !s

s is now hidden in closures. Thus you can create counters independent each other:

let () =
  let x1 =
    let unique_id = make_unique_id 1 in
    let i = unique_id () in
    i
  in
  print_int x1; print_newline (); (* Prints 1 *)

  let x2 = 
    let unique_id = make_unique_id 1 in 
    let i = unique_id () in     (* 1 *)
    let j = unique_id () in     (* 2 *)
    i + j
  in
  print_int x2; print_newline () (* Prints 3 *)

A slight variation on what you already have. Instead of using a continuation, just provide a function to generate a fresh state:

module State : sig

  type t

  val fresh : unit -> t

  val uniqueId : t -> int

end = struct

  type t = int ref 

  let fresh () = ref 0

  let uniqueId s = incr s; !s

end

let () =
  let x1 =
    let s = State.fresh () in
    let i = State.uniqueId s in
      i
  in
    print_int x1;
    print_newline () (* Prints 1 *)

let () =
  let x2 =
    let s = State.fresh () in
    let i = State.uniqueId s in     (* 1 *)
    let j = State.uniqueId s in     (* 2 *)
      i + j
  in
    print_int x2;
    print_newline () (* Prints 3 *)

This is a common approach to handling environments in compilers, which looks a lot like what you are trying to do. It doesn't thread the state through implicitly, since OCaml doesn't support implicit parameters. However if you only require a single such "environment" parameter then it is not too onerous to add it to all the appropriate functions.

I guess what you wish to achieve is:

  1. A function f (make_things) which has a state .
  2. Everytime you call f , the state gets reset
  3. But during inside one call of f , the state can be automatically change

If I am correct, then we don't need Monald, instead, we can use memo .

let memo_incr_state () =
    let s = ref 0 in
    fun() -> s := !s + 1; !s

let make_things f = 
    let ms = memo_incr_state() in
    f ms

let f1 ms = 
    let i = ms() in
    let j = ms() in
    i+j

let x1 = make_things f1 (* x1 should be 3 *)

The basic idea is we use thunk to remember the state.

More knowledge about memorise can be obtained from https://realworldocaml.org/v1/en/html/imperative-programming-1.html#memoization-and-dynamic-programming

It sounds like you want a global variable

let currentId = ref 0

let uniqueId () = 
  incr currentId;
  !currentId

You suggest that a global variable is undesirable, but the behaviour you specify ("all calls to uniqueId get passed the same state parameter") is precisely the behaviour of a global variable.

If you are worried about other code accessing the global variable then simply do not expose currentId in the signature ( .mli file) of your module.

If you are concerned about other code within the same module accessing currentId then you can restrict its scope by placing it within the definition of uniqueId :

let uniqueId =
  let currentId = ref 0 in
    fun () -> 
      incr currentId;
      !currentId

or create a sub-module which does not expose currentId in the signature:

module M : sig

  val uniqueId : unit -> int

end = struct

  let currentId = ref 0

  let uniqueId () = 
    incr currentId;
    !currentId

end

include M

Personally, I would go with the first solution (global variable hidden by the .mli file). It is not hard to ensure that the other code within the same module does not abuse currentId and the module system protects you from code elsewhere.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM