Ocaml project structure and code correctness

binance-ocaml-api

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.

A few suggestions:

  • 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
1 Like

One issue I am seeing is that you are using float for quantities and prices. IEEE 754 floats (like OCaml) suffer from the binary representation issue:

# 0.1 +. 0.2;;
- : float = 0.300000000000000044

I would recommend using a decimal floating point representation instead:

#require "ppx_decimal";;
#install_printer Decimal.pp;;
Decimal.(0.1m + 0.2m);;
- : Decimal.t = 0.3
1 Like

Thanks, you are right. :white_check_mark:
Also I have some problems with conversion from float to string that I will fix with the Decimal module. :sweat_smile:

1 Like

Thanks, it makes sense. :white_check_mark:
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.