I'm trying to understand and/or find examples of how robust OCaml applications deal with a configuration environment. I'm coming from a world of Haskell, where I would use the Reader monad to solve this problem. In particular, I want to define a top-level configuration which I may pass throughout my application, but only some of the functions in my application will need to use the configuration.
To motivate this, consider a simple executable which queries a database. I would want to define a top-level main file which might do things like setup a connection pool, or create a logger which would be shared throughout the application.
I'm assuming OCaml has some pattern for dealing with this, but I cannot find any great examples. I'd prefer not define my own Reader monad and functorize the entire application if I don't have to.
That being said, this is also ugly
main.mli
module Config : sig
type t = {
fooConfig : Foo.Config.t;
dbClientConfig : DB_client.Config.t;
}
val make : Foo.Config.t -> DB_client.Config.t -> t
end
val main : Config.t -> unit
foo.mli
module Config : sig
type t = {
dbConfig : DB_client.Config.t;
}
end
(** fooFunc will need to pass the dbConfig, but it does not actually explicitly need anything in the config **)
val fooFunc : Config.t -> string -> unit
db_client.mli
module Config : sig
type t = {
connPool : SomeConnPool
}
end
(** Finally, I need to use the config to grab a connection out of the pool **)
val writeToDb : Config.t -> string -> (string, string) Lwt.result
I don't want to make the explicit arguments of function "higher in the execution stack" depend explicitly on a configuration they will never use.
Is there a nice functional pattern to deal with this nested dependency coupling? I'd appreciate any code examples that someone can point towards so I can study a better approach.