OCaml JMAP Development Patterns#
Quick Reference for Production Implementation#
These patterns were proven during the December 2024 systematic compilation fixes and should be used consistently for all future development.
1. JSON Parsing Pattern (JSONABLE Interface)#
Always use this pattern for of_json implementations:
let of_json json =
try
match json with
| `Assoc fields ->
let field1 = match List.assoc_opt "field1" fields with
| Some (`String s) -> s
| _ -> failwith "Missing required 'field1' field"
in
let optional_field = match List.assoc_opt "optionalField" fields with
| Some (`Bool b) -> Some b
| Some `Null | None -> None
| _ -> failwith "Invalid 'optionalField' field"
in
Ok { field1; optional_field }
| _ -> Error "Expected JSON object"
with
| Failure msg -> Error ("JSON parsing error: " ^ msg)
| exn -> Error ("JSON parsing exception: " ^ Printexc.to_string exn)
Key Points:
- Always return
(t, string) resultfor JSONABLE compliance - Use
try/withto catch failwith and other exceptions - Provide descriptive error messages with context
- Handle optional fields with explicit None cases
2. Type Inference Disambiguation Pattern#
Use explicit type annotations for complex constructions:
(* For record field assignments that might be ambiguous *)
let filter : Filter.t option = match json_opt with
| None -> None
| Some json -> Some (Jmap.Methods.Filter.condition json)
let position : uint option = match pos_json with
| None -> None
| Some (`Int i) when i >= 0 -> Some i
let sort : Comparator.t list option = match sort_json with
| None -> None
| Some (`List items) -> Some (List.map parse_comparator items)
When to use:
- Complex record constructions with multiple option types
- When OCaml reports type inference conflicts
- Functions with multiple similar-looking fields
3. Module Qualification Pattern#
Use full module paths for potentially ambiguous types:
(* Instead of *)
Filter.condition json
(* Use *)
Jmap.Methods.Filter.condition json
(* Instead of *)
Comparator.of_json json
(* Use *)
Jmap.Methods.Comparator.of_json json
When to use:
- When you get "unbound value" errors
- Multiple modules define similar functions
- Type conflicts between local and imported modules
4. Stub Function Parameter Pattern#
Acknowledge unused parameters appropriately:
(* For completely unused parameters *)
let stub_function _unused_arg _unused_ctx ~used_param ~unused_param:_ () =
(* Use used_param normally *)
Error "Not implemented yet"
(* For parameters used in commented code or TODOs *)
let future_function env ctx ~account_id ~email_ids:_ () =
(* TODO: Implement with email_ids when ready *)
process_account env ctx account_id
(* For debugging/development *)
let debug_function env _ctx ~account_id () =
Printf.printf "Processing account: %s\n" account_id;
process_with_env env account_id
Pattern Rules:
_param: Completely unused, no intention to use~param:_: Named parameter that's explicitly unusedparam: Actually used in function body- Preserve parameters that will be used in future implementations
5. Result Error Handling Pattern#
Consistent error propagation and handling:
(* Chain Results properly *)
let complex_parsing json =
match Base.of_json json with
| Error e -> Error ("Base parsing failed: " ^ e)
| Ok base ->
match Enhanced.of_json base.extra_data with
| Error e -> Error ("Enhanced parsing failed: " ^ e)
| Ok enhanced -> Ok { base; enhanced }
(* Use Result.bind for cleaner chaining *)
let complex_parsing_v2 json =
Base.of_json json
|> Result.bind (fun base ->
Enhanced.of_json base.extra_data
|> Result.map (fun enhanced -> { base; enhanced }))
|> Result.map_error (fun e -> "Complex parsing failed: " ^ e)
6. Interface Implementation Checklist#
Before implementing any JSONABLE interface:
- Does
of_jsonreturn(t, string) result? - Does
to_jsonreturnYojson.Safe.t? - Are all required fields validated?
- Are optional fields handled with explicit None cases?
- Is error handling comprehensive (try/with)?
- Are error messages descriptive and contextual?
7. Build Verification Commands#
Always run these after making changes:
# Full compilation check
opam exec -- dune build @check
# Clean rebuild (if you suspect caching issues)
opam exec -- dune clean && opam exec -- dune build @check
# Documentation generation (checks for doc warnings)
opam exec -- dune build @doc
# Run with release profile (ignores warnings, for final verification)
opam exec -- dune build @check --profile=release
8. Common Issues and Solutions#
Issue: "Values do not match" interface error#
Cause: Function signature doesn't match interface declaration Solution: Check return type, add Result wrapper, verify parameter types
Issue: "This variant expression is expected to have type X"#
Cause: Type inference conflict in pattern matching Solution: Add explicit type annotation to the bound variable
Issue: "Unbound value" errors#
Cause: Module not properly opened or qualified
Solution: Use full module path or add appropriate open statement
Issue: "Unused variable" warnings in stub functions#
Cause: Parameters not acknowledged as intentionally unused Solution: Apply parameter acknowledgment patterns consistently
9. Development Workflow#
Recommended sequence for implementing new functionality:
- Design Types: Define the core types following RFC specifications
- Add Interfaces: Create
.mlifiles with proper JSONABLE signatures - Stub Implementation: Create minimal
.mlfiles with proper parameter acknowledgment - Verify Compilation: Ensure
dune build @checkpasses - Implement Incrementally: Replace stubs with real implementations one at a time
- Test Each Step: Verify compilation after each function implementation
- Add Documentation: Include RFC references and usage examples
This systematic approach ensures that compilation remains clean throughout development and reduces debugging time significantly.
Note: These patterns were validated during the December 2024 systematic fixes where 59+ functions across 10 files were successfully corrected. They represent battle-tested approaches for OCaml JMAP development.