OCaml Code Style
Core Philosophy
- •Interface-First: Design
.mlifirst. Clean interface > clever implementation. - •Modularity: Small, focused modules. Compose for larger systems.
- •Simplicity (KISS): Clarity over conciseness. Avoid obscure constructs.
- •Explicitness: Explicit control flow and error handling. No exceptions for recoverable errors.
- •Purity: Prefer pure functions. Isolate side-effects at edges.
- •NEVER use Obj.magic: Breaks type safety. Always a better solution.
Naming Conventions
| Element | Convention | Example |
|---|---|---|
| Files | lowercase_underscores | user_profile.ml |
| Modules | Snake_case | User_profile |
| Types | snake_case, primary type is t | type user_profile, type t |
| Values | snake_case | find_user, create_channel |
| Variants | Snake_case | Waiting_for_input, Processing_data |
Function naming:
- •
find_*returnsoption(may not exist) - •
get_*returns value directly (must exist)
Avoid: Long names with many underscores (get_user_profile_data_from_database_by_id).
Refactoring Patterns
Option/Result Combinators
(* Before *) match get_value () with Some x -> Some (x + 1) | None -> None (* After *) Option.map (fun x -> x + 1) (get_value ())
Prefer: Option.map, Option.bind, Option.value, Result.map, Result.bind
Monadic Syntax (let*/let+)
(* Before - nested matches *) match fetch_user id with | Ok user -> (match fetch_perms user with Ok p -> Ok (user, p) | Error e -> Error e) | Error e -> Error e (* After *) let open Result.Syntax in let* user = fetch_user id in let+ perms = fetch_perms user in (user, perms)
Pattern Matching Over Conditionals
(* Before *) if x > 0 then if x < 10 then "small" else "large" else "negative" (* After *) match x with | x when x < 0 -> "negative" | x when x < 10 -> "small" | _ -> "large"
Function Design
Keep functions small: Under 50 lines. One purpose per function.
Avoid deep nesting: Max 4 levels of match/if. Extract helpers.
High complexity signal: Many branches = split into focused helpers.
(* Bad - high complexity *) let check x y z = if x > 0 then if y > 0 then if z > 0 then ... else ... else ... else ... (* Good - factored *) let all_positive x y z = x > 0 && y > 0 && z > 0 let check x y z = if not (all_positive x y z) then "invalid" else ...
Error Handling
Use result for recoverable errors. Exceptions only for programming errors.
Never catch-all:
(* Bad *) try f () with _ -> default (* Good *) try f () with Failure _ -> default
Don't silence warnings: Fix the issue, don't use [@warning "-nn"].
Library Preferences
| Instead of | Use | Why |
|---|---|---|
Str | Re | Better API, no global state |
Printf | Fmt | Composable, type-safe |
yojson (manual) | jsont | Type-safe codecs |
Module Hygiene
Abstract types: Keep type t abstract. Expose smart constructors.
(* Good - .mli *) type t val create : name:string -> t val name : t -> string val pp : t Fmt.t
Avoid generic names: Not Util, Helpers. Use String_ext, Json_codec.
API Design
Avoid boolean blindness:
(* Bad *) let create_widget visible bordered = ... let w = create_widget true false (* What does this mean? *) (* Good *) type visibility = Visible | Hidden let create_widget ~visibility ~border = ...
Red Flags
- •Match that just rewraps:
Some v -> Some (f v) | None -> None - •Nested Result/Option matches → use let*/let+
- •Deep if/then/else → pattern matching
- •Missing
ppfunction on types - •Unlabeled boolean parameters
- •
Obj.magicanywhere