Coding guidelines#
1 Overall philosophy#
- Strive for small, orthogonal modules that can be composed freely.
- Depend only on the standard library (and occasionally on other “micro-libs”, e.g.
Fmt,Rresult). - Preserve purity and determinism in the core; confine effects to thin I/O layers.
- Offer total functions whenever feasible and expose explicit failure via
'a result. - Maintain API stability: once an identifier is public, avoid breaking changes; add instead of mutate.
2 Project layout#
my_lib/
├─ dune-project
├─ README.md – high-level overview & minimal example
├─ CHANGES.md – human-written change-log
├─ lib/ – library source code (alternative to src/)
│ ├─ my_lib.mli – canonical public interface
│ └─ my_lib.ml – implementation
├─ bin/ – executable entry points
└─ test/ – Alcotest or inline-tests
Put the public interface in a single .mli whenever practical. Split only when the conceptual surface really warrants distinct compilation units. Using lib/ and bin/ directories instead of src/ is acceptable for projects with both libraries and executables.
3 Modules and sub-modules#
| Purpose | Idiom |
|---|---|
| Canonical type | type t (abstract) |
| Pretty printer | val pp : t Fmt.t |
| Conversions | val to_string : t -> stringval of_string : string -> (t, error) result (when meaningful) |
| Equality & order | val equal : t -> t -> boolval compare : t -> t -> int (when meaningful) |
| Constructors | val v : ... -> (t, error) result (or pure t if infallible) |
| Low-level view | module Unsafe : sig … end (optional) |
Keep the root module flat; introduce nested modules only for clearly separable concerns (e.g. My_lib.Path, My_lib.Map).
4 Naming conventions#
- Modules / files – lower-case, underscore-separated file names (
my_lib.ml), capitalised module names (My_lib). - Values – short but descriptive (
pp,v,find,with_…). - Labels – prefer them only when they disambiguate (
~dir,~mode); avoid gratuitous labels on simple data. - Boolean arguments – almost never positional; wrap in a variant or use a labelled argument (
?dry_run:bool). - Infix operators – expose only if widespread (
>|=) or central to the library (Fpath.( / )). Put them in a dedicated sub-moduleOp.
5 Types#
- Keep representation hidden (
type t) and provide construction/destruction helpers. - Introduce phantom parameters to encode invariants if it avoids run-time checks.
- Use private types when callers may inspect but not forge values (
type t = private string). - For enumerations, use a closed variant; supply
val all : t listandval to_string. - Reserve exceptions for programming errors (
Invalid_argument, assertion failures). Operational errors travel in the'errorpart of('a, error) result. - Formatting: Use
Fmtconsistently for all output. Never mixPrintfandFmt- useFmt.prfor user-facing messages, keepPrintf.printfonly for TTY progress display. - Regular expressions: Use the Re DSL instead of Re.Perl. Prefer
Re.(compile (seq [...]))overRe.Perl.compile_pat.
6 Error handling pattern (Rresult)#
Base Error Types#
type error = [ `Msg of string | `Build_error of context ]
val pp_error : error Fmt.t
val v : ... -> (t, error) result
Error Helper Functions Pattern#
Start implementation files with error helper functions (err_*) for consistent error messages:
(* Error helper functions *)
let err fmt = Fmt.kstr (fun e -> Error (`Msg e)) fmt
let err_file_read file msg = err "Failed to read %s: %s" file msg
let err_file_write file msg = err "Failed to write %s: %s" file msg
Use %a with pretty printers for complex formatting:
let pp_build_error ppf ctx = Fmt.pf ppf "%s" ctx.output
let err_build_failed ctx = err "Build failed:@.%a" pp_build_error ctx
For structured errors with context:
let err_build_error ctx = Error (`Build_error ctx)
Library vs Main Separation#
Critical Rule: Library code never calls exit directly - only returns errors for main.ml to handle.
(* In library code - ALWAYS return structured errors *)
let handle_build_failure ctx =
(* Display specific context *)
Fmt.pr "Loop detected - stopping to prevent infinite iterations.@.";
(* Return error for main.ml to handle *)
err_build_error ctx
(* In main.ml - handle all exit logic *)
match analyze mode root_dir files with
| Ok result -> result
| Error (`Build_error ctx) ->
System.display_build_failure_and_exit ctx (* Exits with build's code *)
| Error e ->
Format.eprintf "Error: %a@." pp_error e;
exit 1 (* Generic error code *)
Error Display Consistency#
Use unified display functions for consistent UX:
(* In system.ml *)
let display_build_failure_and_exit ctx =
let all_warnings = (* parse build output *) in
Fmt.pr "%a with %d %s - full output:@."
Fmt.(styled (`Fg `Red) string) "Build failed"
(List.length all_warnings)
(if List.length all_warnings = 1 then "error" else "errors");
Fmt.pr "%a@." pp_build_output ctx;
let exit_code = get_build_exit_code ctx in
exit exit_code
Guidelines#
- Accept a caller-provided buffer (
Buffer.t) or formatter when the operation might produce extensive diagnostics. - Re-use
Fmt.failwith/Fmt.invalid_argfor internal invariants; never expose rawPrintexctraces. - Never mix exit logic in library code - always return structured errors that main.ml can handle appropriately.
7 Public API surface#
- Minimal yet composable. Do not wrap the entire Unix API—wrap just enough to remove boilerplate.
- Expose first-class values rather than functors where a closure suffices.
- Keep effectful helpers separate (
My_lib_unix,My_lib_lwt) to avoid dragging unwanted dependencies.
8 Interface documentation (.mli, .mld)#
Document every public item in the .mli. Place docstrings after the item unless it spans multiple lines, using code-first voice.
8.1 Structure and style#
(** {1 Overview}
One concise paragraph explaining the abstraction. Link to the README
for a tutorial.
{2 Types}
{3 Constructors}
{4 Accessors}
{5 Derived combinators}
{6 Pretty-printing}
*)
- Use section headings (
{1 …},{2 …}) to group logically related functions. - Provide usage examples in
[{[ … ]}]blocks that compile underodoc-latency-level:normal. - Annotate complexity (
@since,@deprecated,@raise,@see) rigorously. - Keep comments declarative: what and guarantees, never implementation detail.
9 Testing & auxiliary artefacts#
- Unit tests with Alcotest (
test/) mirroring the public API sections. - Cram tests: Use directory-based cram tests (
test/name.t/) instead of inlinecat EOFcommands for complex file creation. This is cleaner and easier to maintain. - Provide a
tool/directory of executable samples that double as integration tests. - Ship an
odigmetadata file soodig odocbuilds HTML docs out-of-the-box.
10 Versioning & release process#
- Tag releases with
vN.N.N; follow semantic versioning. - Maintain
CHANGES.mdwith user-visible entries only. - Publish through
topkgor Dune’s built-in release workflow; push docs togh-pagesviaodoc.
11 Style Checklist#
| Step | Done |
|---|---|
src/<lib>.mli written first, fully documented |
☐ |
All errors returned as 'a result with Rresult helpers |
☐ |
pp, equal, compare, hash supplied where meaningful |
☐ |
| No hidden global side-effects; pure core separated | ☐ |
Example snippets compile under dune build @doc |
☐ |
| Dependency list reviewed: stdlib ± {Fmt, Rresult, Alcotest} | ☐ |
test/ provides Alcotest suites mirroring API sections |
☐ |
Tick every box before publishing.