En 30 secondes
Yaron Minsky explains the perspective behind Jane Street’s use of OCaml: trading systems must be understandable enough to reason about rare, consequential events. He argues that code should favor its readers and future maintainers, while recognizing that other work also demands rapid prototyping. In this first part of his Effective ML lecture, he demonstrates uniform module interfaces, data types that encode connection-state invariants, exhaustive pattern matching, and local naming practices that make APIs clearer at their call sites.
- 1Favor the readerReadable code supports both correctness and change: in a long-lived system, code is typically read and modified far more often than it is written.
- 2Uniform interfacesReusable signature components standardize operations and help modules work with functors that expect shared capabilities.
- 3Make illegal states unrepresentableMove connection-specific data into the corresponding variants and group values that must appear together.
- 4Exhaustive matchesListing cases explicitly lets the compiler flag missing handling when a new variant is added; a wildcard can silently hide the omission.
- 5Open few modulesBroad imports make names shorter but can obscure their origins. Local aliases retain brevity while keeping references explicit.
- 6Design for the call siteLabeled arguments explain the role of values such as positions, lengths and booleans where the function is used.
À retenir
- Design long-lived shared code for the people who must read and change it.
- Uniform module signatures provide predictable conventions and enable reusable functors.
- Use variants and records to tie state-specific data to the state in which it is valid.
- Explicit match cases allow the compiler to reveal places that need updating when a type evolves.
- Limit broad module opens and keep aliases local to make the origin of names clear.
- Use labeled arguments when a value’s type alone does not explain its role.
Lire le texte remanié, chapitre par chapitre
A perspective shaped by trading
After a course introduction, Minsky explains the setting behind his advice. Trading systems need correctness, while research work also requires fast iteration.
Every talk comes from a point of view shaped by its author’s circumstances. As a reader or listener, you should ask what those constraints are. My talk is called Effective ML, modeled on Joshua Bloch’s Effective Java: a collection of small observations about writing better programs.
I admired Bloch’s advice, including limiting mutability and preferring composition to inheritance when appropriate. When I first read it, I was helping people at Jane Street write Java. After about nine years using ML in production, I have gathered a set of lessons of my own.
Jane Street is an electronic liquidity provider. Like a grocery store standing between buyers and sellers, it earns a small spread on transactions rather than primarily betting on the future price of an item. The margins are thin, so volume matters.
Billions of dollars pass through our systems each day. A small software mistake repeated in a tight loop can become disastrous very quickly, especially when many efficient systems are trading across complex venues. Correctness therefore matters enormously.
Correctness is not our only concern. Some software deserves extraordinary care because failure is expensive; other work benefits more from quick delivery. Our research infrastructure needs rapid experiments and prototypes, and we use OCaml for those applications too.
Our environment differs from a company building websites or desktop interfaces around many outside toolkits. Most of our code is built in-house, and we can change it. That degree of control has influenced our technical choices.
We think of code as a means of expression as well as a way to get work done. We need to inspect its intent and reason about its behavior. Testing alone cannot cover every unusual condition, especially when rare financial events occur more often than a simple normal-distribution picture suggests.
To be robust under those conditions, we need to reason from first principles about what the program can do. That requires code simple enough for people to understand. This concern shapes the rest of my advice.
Write for readers
Minsky argues that clarity helps programmers reason about correctness and evolve a system. Long-lived code is read and changed repeatedly.
As I shifted from writing mostly my own code to reading more code written by others, my preferences changed. Writers tend to value convenience while writing; readers value convenience when understanding. For shared, durable systems, my rule of thumb is that the readers are right.
Simplicity helps a reader construct an informal argument that the code is correct. It also helps the team change the system later. In production, a piece of code may be read and modified again and again, long after its first author has finished writing it.
Create uniform interfaces
OCaml module signatures make contracts explicit. Reusable signature components give modules consistent capabilities and support functors.
A concrete practice follows: create interfaces, and make them uniform. OCaml separates an implementation in an .ml file from a signature in an .mli file. Skipping the interface can feel convenient when a project seems temporary.
I did that myself when I first joined Jane Street, assuming the code would be thrown away. Roughly 80,000 lines later, it was still around. Looking back, the absence of interfaces was an embarrassing and costly feature of code that lived much longer than expected.
Good interfaces should also follow common patterns. A map operation over a list or array should have a consistent name and broadly consistent signature. Such regularity gives programmers useful expectations when they encounter a new module and makes collaboration easier.
You can encourage conventions with reusable interface components: standard pieces of a signature included in many modules. A Comparable component, for example, can define compare, derived operators and min/max functions, as well as Map and Set submodules using that ordering.

Including that component gives a module a standard comparison interface. If the implementation violates the contract, it does not compile. Other components can describe capabilities such as string conversion or hashing, with a conventional type t inside the module.
Consistency also makes functors easier to use. A functor that expects a comparable and string-convertible module can accept any module satisfying those standard interfaces, instead of requiring an ad hoc wrapper for every input.
Make illegal states unrepresentable
A connection-state record with several optional fields has hidden invariants. Moving state-specific data into variants and grouping related fields makes some invariants structural.
Another principle is to make illegal states unrepresentable. New ML programmers sometimes see the type checker only as a constraint that stops a program from running. Instead, treat the type system as an engineering tool for expressing invariants and helping future readers and maintainers.
Those constraints can prevent mistakes in the original implementation, communicate assumptions, and guard against invalid changes years later. They are especially helpful when the person who has forgotten how the code works is your future self.
Consider connection information for a network protocol. The original record includes a connection state, server address, last ping time and ID, session ID, initiation time and disconnection time. Many fields are optional because the information is not available in every state.

An option explicitly marks possible absence, unlike treating every object reference as potentially null. That helps, but independent options still permit inconsistent combinations. A ping time should normally appear with a ping ID; a session ID should exist once a connection is established; other timestamps only make sense in particular states.
With those relationships outside the type, every function that transforms the record must remember and preserve them. Comments and care are necessary, but it is easy to make a mistake. We can restructure the data so some of those checks are no longer manual.

Define separate data for connecting, connected and disconnected states, and let each variant carry the information appropriate to that state. Keep the server in the outer connection record because it is relevant in every state. Information that depends on the state moves into the corresponding branch.
The last ping remains optional, but its time and ID become one optional tuple. They are now either both present or both absent. The data structure captures their relationship rather than asking each caller to keep two independent fields synchronized.
This cannot encode every invariant. There will still be cases where functions need explicit checks and careful documentation. But algebraic data types are surprisingly effective at expressing many important relationships in ordinary programs.
The basic building blocks are product types and sum types. A record combines this and that; a variant represents this or that. Layering these two forms gives a rich vocabulary for describing program data and its valid cases.
Code for exhaustiveness
A trading-message example shows how a wildcard can hide a missing case when a type changes. Explicit matching lets the compiler guide updates.
A related technique is to code for exhaustiveness. OCaml checks match expressions and can warn about missing, duplicate or impossible cases. Those checks help with initial correctness, but they are especially useful as a refactoring aid when a type changes.
You can defeat the protection if you write the match in a way that hides cases from the compiler. Think about the guarantees the compiler offers and structure the code to preserve them.
Imagine messages sent to an exchange: an order expresses a willingness to trade, a cancel withdraws an order, and an execution reports a completed trade. A position-change function should return a change for executions and zero for orders and cancels.

Now add a correction message because an exchange can revise a previous execution. A correction may alter the position. If the original function handled executions and used a wildcard to return zero for everything else, it continues to compile even though its answer for corrections may now be wrong.
The function may be far from the type definition, so a programmer changing the type might not notice it. If the original match explicitly named order and cancel instead of using a catch-all, the new correction case would make the match incomplete and the compiler could point to the required update.
Avoiding a wildcard is not an absolute rule, but an underscore at the bottom of a match should make you pause. In most cases, explicit alternatives give better protection as the program evolves.
Names, scope and call sites
Opening many modules obscures the source of names. Local aliases and labeled arguments can make code easier to read without excessive verbosity.
Open few modules. Opening a module brings its names into the current namespace and makes expressions shorter, but it trades explicitness for brevity. With many opened modules, a reader may not know where a function or value came from.
Editor tools can help locate a definition, and we have such tools. Even so, clear code can be read without hovering over every name and is easier to move because it depends less on surrounding context. The cost in extra text need not be as large as it first appears.
My example is an enhanced implementation of the Unix command true, with a command-line library that parses an exit code and a message. The original opens Core, Command and Flag. Names such as int and string are values from Flag describing how arguments should be parsed, but their origin is not obvious on the page.
Rather than open those modules at the top, I can use a local module alias for Command.Flag and qualify the relevant names. That makes their origin explicit without spelling out the entire module path every time.

A local alias also has a short lifetime. Readers have limited working memory: when a name is introduced in a small scope, they know when they can stop remembering it. The same principle helps with variable names: short names can work in short scopes; long-lived names should carry enough meaning to remind readers later.
An audience member asks about tildes in the example. They mark labeled arguments. Jane Street often uses labeled arguments when the type does not explain the role of a value, such as a string range’s position and length or a boolean flag whose meaning would otherwise be ambiguous.
When designing an API, look at how it reads at the call site, not just in the interface or documentation. Readers often stop at the source code in front of them. A clear label can tell them what an argument means without making them look elsewhere.
OCaml also supports label punning: if a variable has the same name as a label, writing the label can stand for passing that variable. It keeps the syntax light and encourages consistent names across contexts, contributing to a more coherent codebase.
Sur la même chaîne
1:34:29EN
1:34:36EN
15:48EN
1:20:57EN
1:12:58EN
58:47EN