OCaml (5.4)

This assessment applies the coupling account — coupling as access to a shared resource, and the coupling floor [1], [2] — and the knowledge-embodiment theorem [3]; the language is assessed against its official reference [4].

1 Assessment

This page assesses OCaml in five parts: core coupling — what the language’s own constructs force; ecosystem coupling — what the surrounding platform, tooling, and community cost; engineering readiness — whether the artifact can be reproduced, verified, observed, worked in quickly, connected to other languages, and governed openly; the profile — the unscored facts a score cannot express; and the scoring detail — the mechanism marks and the arithmetic behind every score. A ✅ bullet marks a construct that helps. A ⚠️ or ❌ bullet marks a cost; the scoring model does not price every cost named here.

Core coupling 58/100 · Ecosystem coupling 84/100 · Engineering readiness 61/100

1.1 Core coupling

1.1.1 Type

  • ✅ Variant types declare a checked case distinction: type shape = Circle of float | Square of float is a closed sum, and the compiler checks that a match covers every constructor
  • ✅ Types are static and inferred: The Hindley–Milner algorithm gives a principal type without annotations, and no value is untyped
  • ✅ A signature hides a type behind an interface: A .mli file can export type t as abstract, so a consumer binds to the operations and not to the representation
  • ✅ A functor makes a type a parameter: Set.Make(String) builds a set module for a given ordered type, so one algorithm is reused across types without run-time dispatch
  • ⚠️ Operators are rebound in scope rather than overloaded (T8): + is int addition and +. is float addition, and a module-qualified form such as Int64.(a + b) or a local let (+) = ... changes which operator is in scope, so the reader must track the binding
  • ⚠️ The object layer is a second type system: Classes and row-polymorphic object types sit beside the core types, so a codebase can mix two paradigms

1.1.2 Construction

  • ❌ Mutable state is the natural default container for shared data (C2): The idiomatic way to hold module state is a ref cell, an Array, or a Hashtbl, each mutable, so any caller of the interface can change what the module holds
  • ✅ A variant constructor is the only way to build a value of the type: Node (l, x, r) names the construction path, so every construction is visible in the type declaration
  • ✅ A record literal states every field: { x = 1.; y = 2. } must be complete unless with is used, so a field cannot be left unset silently
  • ✅ A signature can hide the constructor of an abstract type: With type t abstract and val create : unit -> t, construction goes through the module, so the invariant holds by construction
  • ✅ A let binding evaluates its right-hand side once: let x = expensive () evaluates at that point, and the result is a value rather than a re-evaluable expression
  • ⚠️ A mutable field relaxes the invariant: A record field declared mutable can be assigned at any time, so an invariant over the record is maintained by the code rather than by the type

1.1.3 Memory

  • ✅ Collection is automatic with no manual free: The collector reclaims an unreachable block, and a program never calls free
  • ⚠️ Collection is not tied to the last reference (M2): A block stays allocated until the collector decides, so a latency-sensitive path is coupled to a component it never calls
  • ❌ There is no RAII (M3): There is no scope-bound destructor, so a file descriptor, a Unix.file_descr, or another non-memory handle is released by an explicit call or a bracket such as Fun.protect
  • ❌ There is no ownership model (M4): No static lifetime or aliasing rule ties a value to an owner, so a mutable structure that escapes its module is shared without a trace
  • ✅ Fun.protect brackets a resource: Fun.protect ~finally:close body runs the release on the normal path and on the exceptional path, so the release need not be repeated at every exit
  • ⚠️ The major heap is measured and tuned by hand: Gc.set and Gc.stat control and report space_overhead and the major cycle, so a latency-sensitive program reads collector statistics itself
  • ⚠️ C stubs and Marshal sit outside the collector’s view: A value held in a C global must be registered with caml_register_generational_global_root, and Marshal.from_string builds a value the collector then owns, so the boundary is the programmer’s to manage

1.1.4 Error

  • ❌ Exceptions are unchecked (Er2): raise, Failure, and Not_found are not declared in the type of a function, so a caller cannot see the failure set from a signature
  • ✅ result is a composable value: ('a, 'b) result is Ok of 'a | Error of 'b, and Result.map, Result.bind, and Result.fold chain operations without a match at every step
  • ✅ A variant closes the failure set: type error = Not_found | Permission_denied makes the cases finite, so a match over the failure is checked for exhaustiveness
  • ✅ try ... with separates the abnormal path: A handler can match an exception constructor such as Sys_error _ | Unix.Unix_error _ and re-raise cheaply with raise_notrace
  • ✅ Option carries an absence without an exception: Some x and None give a missing result a value, and Option.map and Option.get compose it
  • ⚠️ A wildcard handler hides a fault: try f () with _ -> default catches every exception, including Out_of_memory and an unintended bug, so the cause disappears

1.1.5 Concurrency

  • ✅ OCaml 5 runs OCaml code on several cores: Domain.spawn starts a second domain, so the global runtime lock that serialised all code in earlier versions no longer applies
  • ✅ Effects express a scheduler in the language: An effect handler such as Effect.Deep.try_with or Effect.Shallow defines what an effect does, so a concurrency library is ordinary OCaml
  • ✅ Eio supplies structured concurrency: Eio_main.run gives a scheduler with fibers, cancellation, and capability-scoped resources, so a program names a scope rather than a thread
  • ✅ Atomic, Mutex, and Condition are primitives of the runtime: A shared cell of type 'a Atomic.t has defined operations, and Mutex.lock guards a critical section
  • ✅ A channel passes values between domains: Domainslib.Chan with Domain.spawn gives a message-passing design instead of a shared one
  • ⚠️ A library written for one domain must be revisited: Code that relied on the single runtime’s atomicity now shares state across domains, so the migration is a review rather than a recompile

1.1.6 Platform

  • ✅ The native compiler emits machine code for many targets: ocamlopt produces a native executable for x86-64, ARM64, RISC-V, and Power, and ocamlc produces bytecode wherever the virtual machine runs
  • ✅ A C stub reaches any system library: An external declaration plus a small C stub links a C library, so the language’s reach follows the reach of C
  • ✅ MirageOS links an OCaml program into a unikernel: A set of modules builds a freestanding image, so the target needs no operating-system process
  • ✅ The JavaScript backends target the browser: js_of_ocaml compiles bytecode to JavaScript, and Melange compiles OCaml to JavaScript modules
  • ⚠️ The foreign-function interface is manual: An external declaration and its C stub must be kept in step by hand, so the boundary carries the correctness burden of both sides

1.1.7 Trade-offs (not coupling)

  • The object layer is opt-in: Classes and objects are separate from the module system, so a project can ignore them entirely
  • Inheritance instead of a functor is a design decision: Using class inheritance where a functor would do is a choice the language permits and does not require
  • Terse syntax is a stated preference: let f x = x + 1 and the ;; toplevel separator are stylistic, and the adjustment period is not a forced dependency
  • Persistent structures trade space for sharing: A version-keeping data structure retains the older versions, which is a space cost rather than a coupling mechanism
  • Interface files are a convention: A project can omit the .mli file, and the abstraction benefit follows from the discipline rather than from the language
  • Effect handlers are opt-in: A program can ignore effects and write plain code, so the paradigm adds no dependency unless it is used

1.2 Ecosystem coupling

1.2.1 Performance

  • ✅ ocamlopt compiles ahead of time to native code: There is no interpreter and no warm-up, so a program starts at full speed
  • ✅ The core type system erases at compile time: An abstract type and a generic function carry no run-time dispatch, so the abstraction costs nothing to execute
  • ✅ A functor is specialised at compile time: Set.Make(String) produces a module whose calls are direct, so the reuse costs no dynamic dispatch
  • ✅ An unboxed representation is available per declaration: An all-float record and an [@@unboxed] declaration avoid a heap indirection, so a numeric structure can be dense
  • ⚠️ A mixed record boxes its float field: type t = { n : int; x : float } stores x in a box, and only an all-float record or an annotation removes it, so the representation is a per-declaration detail
  • ⚠️ Allocation rate drives the collection schedule: A high allocation rate fills the minor heap often, and the program cannot schedule that collection itself

1.2.2 Ecosystem

  • ❌ The library ecosystem is small next to Haskell or Rust (Ec3): Fewer general-purpose packages exist, so some work starts from a blank module
  • ✅ opam is a mature package manager: opam resolves versions, pins a package to a revision, and keeps per-project switches, and the opam repository index is curated
  • ✅ dune builds, tests, and documents a project: One file describes a library, an executable, and a test, so no external build system is needed
  • ✅ merlin and ocaml-lsp give editor support: Type-directed completion, jump-to-definition, and error reporting are available in Emacs, VS Code, and others
  • ✅ Substantial systems are written in it: The Rocq (Coq) proof assistant, the Frama-C analyzer, Jane Street’s trading systems, and the Tezos blockchain are OCaml codebases
  • ⚠️ A niche language means fewer answers: The community is smaller, so a team depends on the members it can find

1.2.3 Evolution

  • ✅ OCaml 5 keeps the single-domain case working: A program that ran on 4.x recompiles on 5.x, and the multicore runtime is the same source language
  • ✅ Version changes have been additive in practice: The core syntax and the standard library have stayed stable across decades
  • ✅ A switch pins a compiler version per project: opam switch keeps an old compiler and a new one side by side, so a project upgrades on its own schedule
  • ✅ The multicore change has a migration path: Code that used Threads can move to Domain and a scheduler library such as Eio, with Domainslib bridging the two
  • ⚠️ A single-domain library needs a review for OCaml 5: A global mutable structure that was safe under one runtime becomes shared state when several domains run

1.2.4 Domain

  • ✅ Excellent fit for compilers and static analysis: Algebraic data types, pattern matching, and a module system match a syntax tree and a typed pass directly, and the OCaml compiler itself is written in OCaml
  • ✅ Proven in proof assistants and verification: Rocq (Coq) and Frama-C are OCaml programs, and their term representations are OCaml variants
  • ✅ Proven in finance: Jane Street’s trading systems are OCaml, and the language is used for typed, low-latency systems
  • ✅ A strong fit for parsers and interpreters: menhir generates a parser, and a variant-based abstract syntax tree keeps the grammar’s cases explicit
  • ⚠️ Numeric and tensor work needs a library: Dense array and tensor programming depends on a package such as Owl or on a C binding, not on the core language
  • ⚠️ The object layer is rarely the right tool: Most OCaml code uses modules and variants, and the object system is a separate, optional design

1.2.5 Learning

  • ❌ The abstraction layer is steep even when the syntax is not (L1): A newcomer learns inference, the module and functor system, and pattern matching together, although the language is generally considered easier to approach than Haskell
  • ✅ Inference removes annotation work: let rec fact n = if n = 0 then 1 else n * fact (n - 1) is accepted without a type written by hand
  • ✅ The error messages point at the mismatch: The compiler prints the inferred type and the conflicting type, and Merlin shows the type under the cursor
  • ❌ The module language is a second language: Functors, signatures, and module type of are a topic of their own beyond the expression language
  • ⚠️ Reading an inferred signature is a skill: val f : ('a -> 'b) -> 'a list -> 'b list must be read fluently before an interface is easy to write

1.2.6 Syntax

  • ✅ let, match, and type share one shape: A binding, a case analysis, and a declaration all need no braces, and the toplevel ends a phrase with ;;
  • ✅ Pattern matching is written as data: match xs with [] -> 0 | x :: _ -> x states the cases directly
  • ✅ The keyword set is small and consistent: The same |-case form serves a variant declaration, a match, and an exception handler
  • ✅ Terse for the type information it carries: One line declares a record, and inference fills in the annotations
  • ⚠️ Integer and float operators differ by a character: + is not +., and = is structural while == is physical, so a sign or a repetition changes the meaning
  • ⚠️ Extensions extend the notation: let* binding operators, ppx rewrites, and [@@deriving] attributes are not core syntax, so a reader must know which extensions a file uses

1.2.7 Testing

  • ✅ A test framework lives in the package repository: alcotest and ounit2 provide assertions and runners, and dune runtest runs the suite
  • ✅ Inline and expect tests sit beside the code: let%test and let%expect_test (ppx_inline_test and ppx_expect) keep the expected output in the source and diff it on a run
  • ✅ The type checker catches faults before a test does: An exhaustiveness check reports a missing match case at compile time, which removes a whole class of test
  • ✅ bisect_ppx measures coverage: An instrumented build reports which points were executed
  • ⚠️ A hand-written module is the usual stand-in: There is no dynamic mocking of a module, so a seam must be a functor parameter or a signature
  • ⚠️ An effectful path needs its handler installed: A test of a scheduler runs the effect handler, which is a different setup from a test of plain code

1.2.8 Security

  • ✅ Memory safety is the default: The collector and the type system prevent use-after-free, and an out-of-range a.(i) raises Invalid_argument instead of reading adjacent memory
  • ✅ A signature hides the representation: With type t abstract in the .mli, a consumer can neither forge a value nor read its internals
  • ✅ A typed function cannot receive a value of another type: A value that reaches a typed parameter is of that type, so a string cannot become a program structure at that boundary
  • ✅ The runtime bounds an array access: The bounds check is part of the language, so an off-by-one index is a defined exception
  • ⚠️ Marshal.from_string trusts its input: A crafted byte string can build a value that the type system then trusts, so untrusted input must not be unmarshalled
  • ⚠️ Obj.magic and a C stub break the type system: Obj.magic reinterprets a value without a check, and a stub can return an ill-typed value

1.3 Engineering readiness

1.3.1 Reproducibility

  • ❌ Builds are not byte-reproducible by default.
  • ❌ Dependencies resolve without a lock by default.

1.3.2 Supply chain

  • ⚠️ Published packages carry no signed provenance by default.
  • ⚠️ The package registry is a flat name space.

1.3.3 Observability

  • ⚠️ No stable tracing and metrics standard.
  • ⚠️ The standard library has no structured logging.

1.3.4 Feedback

  • ✅ No charged mechanism in this dimension.

1.3.5 Interop

  • ✅ No charged mechanism in this dimension.

1.3.6 Governance

  • ✅ No charged mechanism in this dimension.

1.4 Profile

Unscored, and kept separate from every score on purpose. The profile records what a mechanism price cannot express: what a running program costs, how well a coding model and an agent work in the language, and whether a safety-critical path exists.

1.4.1 Energy and cost

What this axis asks: what does a running program cost in energy and time?

  • ✅ Compiles to native code: the fastest class in the language energy and time benchmarks [5], [6]. A causal re-analysis attributes the difference to execution time [7].

1.4.2 AI and agent readiness

What this axis asks: how well does a coding model and an agent work in this language?

  • ❌ Sparse training data: models write it unreliably [8].

1.4.3 Safety certification

What this axis asks: is there a certified or standards-qualified path to a safety-critical system?

  • ⚠️ No certified or standards-qualified path for safety-critical systems.

1.5 Scoring detail

1.5.1 Core coupling Dimensions

Dimension Mechanism Description Present Cost
Type T1 dynamic no static check — 0
T2 weak static — 0
T3 dynamic/duck dispatch — 0
T4 no sum types — 0
T5 no generics — 0
T6 no namespaces — 0
T7 type erasure/reflection — 0
T8 operator overloading ✅ −4
T9 inheritance required for polymorphism — 0
T10 no null-safety — 0
T11 no module boundary — 0
T12 no effect tracking ✅ −8
T13 async/sync colouring — 0
T14 unchecked foreign boundary ✅ −4
Type subtotal −15 (−16)
Construction C1 global mutable state by default — 0
C2 no immutability by default ✅ −4
C3 constructors coupled to allocation — 0
C4 no constructor/validation — 0
Construction subtotal −4
Memory M1 manual deallocation — 0
M2 GC non-deterministic pauses ✅ −8
M3 no RAII ✅ −4
M4 no ownership model ✅ −8
Memory subtotal −15 (−20)
Error Er1 no structured path — 0
Er2 unchecked exceptions only ✅ −8
Er3 errors ignored/non-exhaustive — 0
Er4 no composable result type — 0
Error subtotal −8
Concurrency K1 GIL/no parallel — 0
K2 shared mutable state default — 0
K3 manual sync no race detection — 0
K4 no structured concurrency — 0
K5 no primitives — 0
Concurrency subtotal 0
Platform P1 cannot function outside one vendor/domain — 0
P2 single-vendor but other uses — 0
P3 legacy/discontinued platform — 0
Platform subtotal 0

Core Dimensions score: 58/100 — total cost = −42 (per-dimension subtotals are capped at −15; an uncapped cost is shown in parentheses where the cap bites).

1.5.2 Ecosystem coupling Dimensions

Dimension Mechanism Description Present Cost
Performance R1 interpreted no AOT/JIT — 0
R2 GC pauses — 0
R3 JIT warmup — 0
Performance subtotal 0
Ecosystem Ec1 no package manager — 0
Ec2 fragmented tooling — 0
Ec3 small/niche/discontinued ✅ −8
Ec4 proprietary — 0
Ec5 platform-locked tooling — 0
Ec6 monoculture — 0
Ec7 legacy burden — 0
Ecosystem subtotal −8
Evolution V1 breaking changes — 0
V2 frozen — 0
V3 discontinued — 0
V4 glacial standards — 0
Evolution subtotal 0
Domain D1 domain/vendor lock-in — 0
D2 poor domain fit — 0
D3 framework forces architecture — 0
Domain subtotal 0
Learning L1 steep ✅ −8
L2 large surface/multi-paradigm — 0
L3 sparse docs — 0
Learning subtotal −8
Syntax S1 verbose — 0
S2 inconsistent — 0
S3 punctuation-heavy — 0
Syntax subtotal 0
Testing G1 weak tooling — 0
G2 poor debugging — 0
G3 runtime-only errors — 0
Testing subtotal 0
Security Q1 memory-unsafe — 0
Q2 dynamic/weak typing risk — 0
Q3 known vulnerability/injection — 0
Security subtotal 0

Meta Dimensions score: 84/100 — total cost = −16 (per-dimension subtotals are capped at −15; an uncapped cost is shown in parentheses where the cap bites).

1.5.3 Engineering readiness Dimensions

Dimension Mechanism Description Present Cost
Reproducibility Y1 build not reproducible ✅ −8
Y2 dependencies not pinned ✅ −8
Reproducibility subtotal −15 (−16)
Supply chain Y3 no signed provenance ✅ −8
Y4 registry without namespacing ✅ −4
Supply chain subtotal −12
Observability Y5 no stable tracing standard ✅ −8
Y6 no structured logging ✅ −4
Observability subtotal −12
Feedback Y7 slow build — 0
Y8 no fast in-editor analysis — 0
Feedback subtotal 0
Interop Y9 no foreign-function interface — 0
Y10 no portable target — 0
Interop subtotal 0
Governance Y11 single-vendor governance — 0
Y12 closed specification — 0
Governance subtotal 0

Readiness Dimensions score: 61/100 — total cost = −39 (per-dimension subtotals are capped at −15; an uncapped cost is shown in parentheses where the cap bites).

Verdict: OCaml’s design yields low core coupling (72/100) because its abstraction is checked at compile time. A variant type closes a case set, an abstract type in a .mli hides its representation, a functor makes a type a module parameter, and the inference engine reports a mismatch before the program runs. The coupling that remains is mostly about state and lifetime: there is no ownership rule and no block-scoped release, exceptions are unchecked, and mutable containers are the natural way to hold module state. On the platform side, the small library ecosystem and the concepts of the module language are the real cost, while observation of the collector is a matter of tuning rather than of correctness.


The formal development. The definitions, theorems, and proofs behind this page are published in:

All published papers

References

[1]
Y. Loth, “On the nature of coupling: All coupling as shared resource access.” Zenodo, Aug. 2026. doi: 10.5281/zenodo.21925122.
[2]
Y. Loth, “Coupling as a consequence of cohesion: Decomposition, the coupling floor, and domain-imposed coupling.” Zenodo, Aug. 2026. doi: 10.5281/zenodo.21925201.
[3]
Y. Loth, “On the nature of cohesion: Cohesion as correctness relative to a partitioning rule.” Zenodo, Jul. 2026. doi: 10.5281/zenodo.21703193.
[4]
T. Oc. Team, “OCaml language manual.” OCaml, 2024. Available: https://ocaml.org/manual/
[5]
R. Pereira et al., “Energy efficiency across programming languages: How do energy, time, and memory relate?” in Proceedings of the 10th ACM SIGPLAN international conference on software language engineering (SLE), 2017, pp. 256–267. doi: 10.1145/3136014.3136031.
[6]
R. Pereira et al., “Ranking programming languages by energy efficiency,” Science of Computer Programming, 2021, doi: 10.1016/j.scico.2021.102609.
[7]
N. van Kempen, H.-J. Kwon, D. T. Nguyen, and E. D. Berger, “It’s not easy being green: On the energy efficiency of programming languages,” in Proceedings of the 40th IEEE/ACM international conference on automated software engineering (ASE), 2025. doi: 10.1109/ASE63991.2025.00131.
[8]
S. Joel, J. J. Wu, and F. H. Fard, “A survey on LLM-based code generation for low-resource and domain-specific programming languages,” ACM Transactions on Software Engineering and Methodology, 2025, doi: 10.1145/3770084.