Exactly, I am talking about stuff like “setting up the db connection pool” and “initializing the logger” which sometimes involves managing some mutable state. I’d be fine with both, eager or lazy initialization.
I have a MakeDatabaseService functor that I use to create a DatabaseService:
module MakeDatabaseService
(ConfigurationService : CONFIGURATION_SERVICE)
(LogService : LOG_SERVICE) : DATABASE_SERVICE = struct
let query = ...
end
In that code I say “DatabaseService depends on ConfigurationService and LogService”.
Now all of these 3 services have lifecycles, meaning they can be started and stopped. When started, they start managing some “hidden” internal mutable state. In order to start the services in the correct order I need the dependency graph, so I have to add the dependencies again in a way, that I can read them at runtime:
module MakeDatabaseService
(ConfigurationService : CONFIGURATION_SERVICE)
(LogService : LOG_SERVICE) : DATABASE_SERVICE = struct
let query = ...
let start ctx = ctx |> add_pool |> Lwt.return
let stop _ = Lwt.return ()
let lifecycle =
Core.Container.Lifecycle.make "db"
~dependencies:[ ConfigurationService.lifecycle; LogService.lifecycle ]
~start ~stop
end
I could easily forget to list all services as dependencies here and it feels like I am expressing the same thing twice.
My approach might be not idiomatic at all, so please let me know if this could be done in some other way.
Been playing around with an alternative strategy for managing dependencies that does not rely on using module-functors. This post contains the details.
To get a quick idea of what user-code might look like. Here’s how to define an accessor for a dependency, in this case user-id:
let user_id () : (int, [> `User_id of int Context.t]) t =
fetch ~tag:(fun ctx -> `User_id ctx)
Dependencies may also involve modules:
module type Logging = sig val log : string -> unit end
(* val log : string -> (unit, [> `Logging of (module Logging) Context.t ]) t) *)
let log s =
let* lm = fetch ~tag:(fun ctx -> `Logging ctx) in
let module L = (val lm : Logging) in
return @@ L.log s
A top-level program that depends on both user-id and logging:
(*
* val program :
* unit ->
* (unit,
* [> `Logging of (module Logging) Context.t
* | `User_id of int Context.t ]) t
*
*)
let program () =
let* () = log "Start app" in
let* uid = user_id () in
..
return ()
For the type-checker to allow running the program, both resources need to be supplied:
module Logger = struct ... end
let prod_program : (unit, void) t =
program ()
|> provide (function
| `Logging ctx -> Context.value (module Logger : Logging) ctx
| `User_id ctx -> Context.value 123 ctx)
let _ = run prod_program
The same program can be run for testing purposes, by providing a different configuration:
module MockLogger = struct let log = print_endline end
let test_program : (unit, void) t =
program ()
|> provide (function
| `Logging ctx -> Context.value (module MockLogger : Logging) ctx
| `User_id ctx -> Context.value 0 ctx)
let _ = run test_program
thanks @kantian and @orbitz , this clarified some of the differences between modules and classes (don’t get me wrong I am still conceptualising that difference in my head )
but the issue I had with it is that dependencies are implicit - each function just has a dependency on “context”, but you don’t know from the type signature what types do you really need to provide
in this case the dependencies are part of the type;
OCaml and these types are still pretty new to me though, so once my head stops hurting, I’ll try to understand how this can be used with other monads like Lwt
OCaml and these types are still pretty new to me though, so once my head stops hurting, I’ll try to understand how this can be used with other monads like Lwt
@mudrz in order to wrap this around Lwt, you’d need to create a new type, like:
type ('a, 'r) t = 'r Context.t -> 'a Lwt.t
And lift all combinators you care about to operate on this type. This basically involves designing a new library with a different semantics and may not be what you’re looking for here. For some more general notes on an alternative to Lwt promises, see this post.
type ('a, 'e, 'r) t = 'r Context.t -> ('a, 'e) Lwt_result.t
Which is more or less what they have in Scala-land with ZIO (and Haskell with RIO).
Incidentally, this also gives you a lazy effect type, unlike eager Lwt promises: no effects will be run until the entire final application effect is run.