Is idomatic in Ocaml to take configuration and return a module that uses it?

Hello,

Something I do a lot in JS it’s taking some options, catching them on a closure and return a module with a set of methods that uses such options. This is useful for example when you are creating an api client and you want to take the target url, user and pass, and use them all for all the requests/operations.

Is this considered a bad practice in Ocaml ? and, if it is, what is the proper way of doing it in Ocaml ? Taking all the parameters all the time ? Using a monadic structure ?

Thanks and regards

I’ve seen functors used this way. For your particular use case however, wouldn’t it be easier to just create a record for you client:

type client =
  { target_url: string
  ; user: string
  ; pass: string
  }

And then just pass that all at once.

I’ve been doing this (with functors) in my code recently, and it’s fairly convenient. It’s nice to avoid passing around context parameters to all the functions that need them, particularly when the contexts are stateful, e.g. database connections.

I’ve run into a few downsides with this pattern:

  • If a configured module A uses another configured module B, then you have two options. You can construct B inside A, which means that the config module for A needs to have the fields and types needed by B. Or, you can construct B and pass it to A. This exposes the fact that A depends on B. If B has further dependencies then the calling code has to build an entire tree of modules.
  • You need to write signatures for all the configured modules so that client code can construct them as first-class modules.
  • It breaks merlin’s go to definition. You just end up in the module signature. Reasonable enough for functorized code, but irritating when there really is only one possible definition.

On the other hand, if you follow this pattern you get dependency injection more or less for free, which is great for testing.

I’d be interested to hear about other ways that ocaml users are dealing with this problem.

2 Likes

I’d say that there are 3 common patterns used for that problem. You can:

  • have a configuration type
  • return a record with function fields
  • use a functor which returns a module with all the functions you need

The configuration type is light and flexible. You only deal with data and functions. If there is some setup code (the kind that would be at the top of your module), I recommend making the configuration type abstract and do the setup in the make function (say, if you’re manipulating URLs, store a Uri.t in the config type, and build it from the string components in make).

The record solution will have a familiar syntax if you’re used from an object language. If you make the record type abstract, you can have code that looks exactly as in the config type solution (f r args gets translated into r.f args). One nice thing you could do with this is override some functions, for example to test some parts, but I don’t think that this is a good idea in general (if you need to override or mock a single method, usually this means that this method should be a collaborator instead, and passed to the constructor).

Functors and first class modules are weird. The syntax is heavy and type inference is only partial (so you need to annotate a lot). You can often refactor these to plain functions. My rule of thumb is that there is no abstract type in the output signature, you’re better served by a plain function.

You can use dependency injection in all these cases. I would say that the configuration type is the most flexible since you can inject the exact dependencies, not just the arguments of your constructor (in the above example, it means you can inject the Uri.t that the code will be using, not only the strings used to build it).

TL;DR functors look like they’re your friends but they will tend to functorize your whole application. If there are no abstract types in them, refactoring them to plain functions will often pay off. Using a configuration type will keep things easy to maintain.

8 Likes

I’ve been using essentially your second option: passing B to A but also to A’, A’’ etc. It felt a bit heavier than it ought to.

The pattern became a bit more messy when I needed to loop over different parameter values defined in B. I ended up making first-class modules to pass as different B versions in each iteration. This felt even heavier and somehow like using the wrong language feature.

Finally, I’m always unsure about performance implications. What if B.param is read inside a tight loop? Is there a penalty for accessing B.param rather than for instance, a record field?

Wow,
Thanks for all your answers. My target platform is javascript through Bucklescript, not sure if I mentioned before.
For now I’ll be using the simpler approach, provide a function that creates a special config record and accept it on all the functions that require it. This seems very common on functional world, and at the end there is not much differennce between client.request(a) and request(client, a).
Seems that javascript is one of the few languages that uses closures for con parameters. Functor seems a bit overkill as anyone seems to agree

@emillon could you please put some code for those words ? I’m intrigued about the Uri.t thing… is that a type or an actual value ?

Regards

Sure! Uri.t is a type, that was just an example where this is the type that’s used in the underlying functions. Here in my example we build a small client that hits https://$HOST/a and https://$HOST/b and returns the body as a string (with no error checking).

With a client type (use this)

module With_client_type : sig
  type t

  val make : host:string -> t

  val get_a : t -> string Lwt.t

  val get_b : t -> string Lwt.t
end = struct
  type t = Uri.t

  let make ~host =
    Uri.make ~scheme:"https" ~host ()

  let get_path t path =
    let open Lwt in
    let uri = Uri.with_path t path in
    Cohttp_lwt_unix.Client.get uri >>= fun (_, body) ->
    Cohttp_lwt.Body.to_string body

  let get_a t =
    get_path t "/a"

  let get_b t =
    get_path t "/b"
end

With a record

module With_record : sig
  type t =
    { get_a : unit -> string Lwt.t
    ; get_b : unit -> string Lwt.t
    }

  val make : host:string -> t
end = struct
  type t =
    { get_a : unit -> string Lwt.t
    ; get_b : unit -> string Lwt.t
    }

  let get ~host path () =
    let open Lwt in
    let uri = Uri.make ~scheme:"https" ~host ~path () in
    Cohttp_lwt_unix.Client.get uri >>= fun (_, body) ->
    Cohttp_lwt.Body.to_string body

  let make ~host =
    { get_a = get ~host "/a"
    ; get_b = get ~host "/b"
    }
end

With a functor

module With_functor : sig
  module type S = sig
    val get_a : unit -> string Lwt.t
    val get_b : unit -> string Lwt.t
  end

  module type PARAMS = sig
    val host : string
  end

  module Make(P:PARAMS) : S
end = struct
  module type S = sig
    val get_a : unit -> string Lwt.t
    val get_b : unit -> string Lwt.t
  end

  module type PARAMS = sig
    val host : string
  end

  module Make(P:PARAMS) = struct
    let base = Uri.make ~scheme:"https" ~host:P.host ()

    let get path =
      let open Lwt in
      let uri = Uri.with_path base path in
      Cohttp_lwt_unix.Client.get uri >>= fun (_, body) ->
      Cohttp_lwt.Body.to_string body

    let get_a () = get "/a"

    let get_b () = get "/b"
  end
end
6 Likes