Value guaranteed to be set after initialization

type-system

#1

I keep seeing this problem, chances are you might also have seen it, it often happens when initializing something, and is not specific to OCaml. The problem can be summarized as this:

When the application is starting, you often need some values to proceed to the next step, (these values can come from a config file, or from a HTTP request, etc.), if these values are not there, then there is no strictly better alternatives than terminating the application. (an example: you will need the password to the database if your application is database-focused and non-interactive).

Solution 1, is to use a ref to an option, it’s doable, but as far as I can see, it’s a bit of stretch, because the value will always be Some _ after the first time you turn it from None to Some _, and all codes that use this value has to match on None, you ends with lots of impossible branches if this value gets used a lot.

Solution 2, is to wrap solution 1 with a function like this:

  let get_value r =
    match !r with
    | Some x -> x
    | None   -> failwith "yay! null reference"

but this solution kinds of introducing the null back into runtime.

I guess what I’m looking for is some kind of “inference over time”: after a certain point in the execution of a program, some nullable values are guaranteed to be non-null.

Does something for this kind of purpose exist? It’s painful when you don’t know what to type into the Google search box.

Sometime I can rearrange the code flow to avoid this, (ending with more code), sometime I have no better alternatives than the solutions above.


#2

lazy may be the right answer for some situations: https://caml.inria.fr/pub/docs/manual-ocaml/libref/Lazy.html.

Otherwise, I commonly write a function for requiring optional values with type require : 'a option ref -> 'a, that raises an exception if the optional value is None. Sometimes I give it a string first parameter, that I partially apply, to be able to trace more easily what happened.


#3

This suggests that you need a function. Specifically, ‘next step’ needs to be a function that takes the value as an input. This is the most surefire way of statically guaranteeing that the value will be available.


#4

Just tried refactoring with lazy, some other issue prevents me from adopting this method.

(Guess it’s not quite the same with what I imagined, which is a placeholder, holding zero or one value, and the compiler or the library guarantees that by the time the value gets used, it has already been set, (actively by the programmer, not set in the way lazy sets it, which is, oh, that value is a thunk, I, the lazy, should evaluate and cache it).

It’s more like a nullable value with a proof says that, ok, I see sometimes the value can be null, but when this thing is null, it’s never “read from”, so it’s ok, I, the compiler, gonna allow this.
)

I think require is not that elegant but perfection is not what this world made of, and it’s practical and gets the job done.


#5

You mean, if I need a config value in f config, I should write f (config ())?


#6

This idiom comes up often enough that Core_kernel has a module for it, called Set_once.

y


#7

Nice try, the genuine Yaron Minsky would have chosen the user name “Yaron_minsky”, details man, details.


#8

f config should be enough.

E.g.,

let main config =
  let db =
    Db.connect
      ~url:(Config.url config)
      ~user:(Config.user config)
      ~password:(Config.password config) in

  (* ... your program ... *)

let () =
  let config = Config.get () in
  main config

The point is, your requirement that you have the config value before you can proceed to the next step, your main program, can be encoded as a parameter to the main program (which can then be a function). Any time you have this kind of requirement, you can use this technique.


#9

This is a good point. Things like Set_once are definitely useful, but they come up rarely: sometimes when there are complex server states with mutual references that are hard to initialize simultaneously. But most of the time, you can and should write your code to avoid this kind of thing.