I’m new to writing Ocaml code and have some design and code style issues.
The main problem is that I don’t think I’m very good at OCaml and I need some advice on how to design a big project like this and what mistakes I’ve made so far. The point is, if I can correct my design problems now, when the project is early on, there will be less to fix in the future.
Some of the questions are:
Working with Lwt is it ok for the API to return a value of type "a Lwt.t option? I make a module for each endpoint, is there a better way to structure those endpoints? I also need to know what kinds of mistakes I made now, like code style, correctness, and if there are parts of the code that don’t follow some rules? Using modules some of the endpoints have a lot of parameters, creating a module for every endpoint and fill all those parameters becomes harder for the user. Should I implement just the functions for the most used parameters?
I try to make the code more functional by using pure functions, higher order functions and reducing side effects as much as possible.
Any other advice is welcomed.
I tried to make the library as easy as possible to work with but as the project scales it becomes harder to work with.
I would enable ocamlformat. You can have your editor do the formatting, or you can run dune build @fmt.
Using pure functions is a good instinct.
I would avoid the use of functors in your endpoints. For example, I’d rewrite margin_account_borrow.mli to match the following interface:
open Variants
type parameters = {
url : string;
api_key : string;
secret_key : string;
asset : Symbol.t;
recv_window : int;
}
val borrow : parameters -> float -> Wallet_transfer_direction.t -> int option Lwt.t
val isolated_borrow : parameters -> Symbol.t -> float -> Wallet_transfer_direction.t -> int option Lwt.t
This will work nicely when most endpoints take the same parameters. If you want to use subtyping for the parameters, I would reach for first-class modules instead of functors:
open Variants
module type Parameters = sig
val url : string
val api_key : string
val secret_key : string
val asset : Symbol.t
val recv_window : int
end
val borrow : (module Parameters) -> float -> Wallet_transfer_direction.t -> int option Lwt.t
val isolated_borrow : (module Parameters) -> Symbol.t -> float -> Wallet_transfer_direction.t -> int option Lwt.t
Thanks, it makes sense.
It became harder to always make a new module when I could call the function directly with parameter from a specific API module.
I will start to refactor these modules and also add some optional parameters.