Yaron Minsky on Effective ML: Writing OCaml for Code Readers

Practical lessons from Jane Street on using types, interfaces and abstraction to keep a large OCaml codebase understandable and correct.

YMYaron MinskyVortrag · Jane Street
Kanal
Jane Street
Veröffentlicht
5. Apr. 2016
Video
1:09:24
Lesezeit
11 Min.
Folien
3
Original
Englisch
Auf YouTube öffnen
Beim Scrollen schrumpft das Video in die rechte untere Ecke und läuft weiter. Untertitel folgen Ihrer Lesesprache.
LesespracheIn 2 Sprachen verfügbar
Diesen Artikel gibt es noch nicht auf Deutsch, daher wird das Original (Englisch) angezeigt.
ZUSAMMENFASSUNG

In 30 Sekunden

In a 2010 Harvard CS51 lecture, Jane Street's Yaron Minsky explains why readability is central to correctness in trading software and demonstrates practical OCaml techniques. He covers uniform interfaces, modeling invariants, exhaustive matches, error conventions, abstraction, side effects and phantom types. His central test is whether a technique reduces complexity for the people who must understand and change the code; powerful type-level tricks can help, but can also go too far.

  1. 1Correctness through readable codeTrading systems encounter rare, high-impact conditions that are difficult to test exhaustively. Minsky argues for understanding a system's intent and internals through careful code review.
  2. 2Standard interfacesReusable OCaml signatures make module contracts consistent and allow functors to work across modules that satisfy those contracts.
  3. 3Make illegal states unrepresentableA connection's state-specific data belongs in the relevant variant; values that must appear together can be grouped into one optional pair.
  4. 4Exhaustiveness as a refactoring toolExplicit match cases let the compiler expose places that need updating when a new variant is added; a wildcard can hide the change.
  5. 5Complexity is the enemyAn advanced type or abstraction that makes code harder to understand can undermine the correctness it was intended to improve.
  6. 6Phantom types as capabilitiesA type parameter absent from the runtime representation can distinguish read-only and writable references while allowing polymorphic reads.

Das Wichtigste

  1. Optimize long-lived shared code for its readers and future maintainers.
  2. Use uniform module interfaces to make expectations predictable and reusable.
  3. Structure types so invalid states cannot be represented where practical.
  4. Write exhaustive matches without catch-all cases to make the compiler reveal missing cases as types evolve.
  5. Make error behavior explicit and factor out repetitive structure without sacrificing clarity.
  6. Use purity and advanced type techniques where they simplify reasoning, rather than treating either as an absolute goal.
VOLLTEXT

Den bearbeiteten Text nach Kapiteln lesen

Nur das Transkript
01

Why Jane Street cares about understandable code

Minsky introduces the constraints of a trading firm and the role code review plays in correctness. He explains why his advice is shaped by Jane Street's environment.

I borrowed the title of this talk from Joshua Bloch's Effective Java, a book I liked enough to give copies to colleagues when I joined Jane Street. After roughly eight years using ML in industry, I want to share some small programming practices and larger lessons about maintaining a real codebase.

Jane Street is a proprietary trading firm and electronic market maker. Like a shopkeeper between buyers and sellers, we make small amounts on many transactions. The volume means billions of dollars of nominal value move through our systems, so technological risk matters enormously.

There is no faster way to set yourself on fire than to write software that makes the same bad decision repeatedly in a tight loop. Faster systems and a larger scope can amplify a mistake, so correctness is a central concern in our work.

Our approach emphasizes code that can be understood and a strong practice of reading it. We do test, but I sometimes think of our process as code-review-driven development. Unusual financial events can be hard to test and occur more often than a simple normal-distribution picture might suggest.

We want to understand a system's internals and intent, not merely its normal behavior. Code is a way to express what a system is supposed to do as well as a way to get it to do something.

Not every piece of software deserves the same level of care. Research prototypes benefit from speed, while systems administration has so many possible sources of failure that layers of independent checks may matter more than constructing one exceptionally solid executable.

Our situation also differs from that of teams that integrate many external systems. Much of our software is bespoke, and we have the source and can change it. We also hire programmers with strong computer science backgrounds, so we can focus on techniques that make them effective rather than deciding whether every programmer would find ML easy.

02

Readers and uniform interfaces

Long-lived code should favor its readers. OCaml module signatures provide consistent contracts and make reusable functors easier to apply.

My first piece of advice comes from watching writers and readers of code disagree. As my own work shifted from writing to reading more of other people's code, I came to think that for shared, durable systems, the readers are generally right.

Readers push toward clarity, simplicity and the ability to change a system later. Most real code is read and changed many more times than it is written. If a script will immediately be thrown away, the tradeoff may be different, but long-lived shared code should favor the reader.

Create interfaces, and make them uniform. In OCaml an .mli file constrains a module's interface. Skipping interfaces can feel convenient while you know all the details, but six months later even you may be the reader who cannot remember what the code meant.

Shared standards give a team reliable expectations when navigating an unfamiliar part of the codebase. A module signature can describe a reusable interface component, rather than being tied to one implementation.

A comparable module signature
FOLIE 1 / 3A comparable module signatureThe interface groups comparison operations with map and set modules, providing a reusable contract.

For example, a Comparable signature can collect comparison operators and functions along with Map and Set submodules based on that ordering. A module can include that signature and connect the comparable type to its own type, although the syntax requires some extra type unification.

This does more than recommend a convention: if an implementation does not satisfy the interface, it will not compile. Uniform interfaces also make functors more useful. A functor maps modules to modules and declares what it requires of its input and provides in its output.

When modules follow the same conventions, they can satisfy those requirements without being repackaged for every functor. The Map and Set modules themselves can be generated by functors, so the same implementation need not be rewritten for each comparable type.

03

Make illegal states unrepresentable

A connection-state example shows how records and variants can encode invariants. The approach uses algebraic data types to express ordinary case analysis.

Make illegal states unrepresentable. An ML type system checks useful properties for us, but it is still possible to design types that express only weak guarantees. Good type design places practical invariants in the data structure itself.

Consider a connection with three states: connecting, connected and disconnected. An initial record contains the state, server, last ping time and ID, session ID, connection start time and disconnection time. Most of these fields are optional.

Connection information with optional fields
FOLIE 2 / 3Connection information with optional fieldsThe original record permits combinations of fields that violate the connection's state invariants.

The options are better than discovering an unexpected null at runtime, but the record still permits combinations that make no sense. A ping time should appear with a ping ID; a session ID belongs to a connected session; the start and disconnect times each apply only in particular states.

A restartable connection makes this particularly risky: an old initiation time might survive into the wrong state. Every construction and update must preserve several unwritten relationships, so readers have to check the logic and remember the invariants.

Refactor the type by keeping the server in the common record and moving state-specific values into the corresponding variant. The server applies in every state; the connecting, connected and disconnected cases each carry the data relevant to that case.

Pair the ping time and ID inside a single option. They may both be absent, but when one is present the other must be present too. The resulting type rules out those inconsistent combinations instead of relying on every caller to remember them.

This technique cannot encode every possible invariant, and sometimes careful runtime checks are still necessary. But algebraic data types combine products and sums in ways that express a surprising number of everyday constraints.

Variant types are often introduced with binary trees, which can make them look like a specialized data-structure feature. Their everyday value is representing collections of different cases and then analyzing those cases explicitly, especially when combined with records.

04

Exhaustive matches and evolution

Explicit cases let the compiler find places affected by a new message type. Catch-all branches can conceal a dangerous omission.

Write code to benefit from exhaustiveness checks. OCaml can warn when a match omits a case; I would configure the compiler to reject such matches. The check is especially powerful when refactoring because it tells you what needs attention as a type evolves.

Imagine a trading message type with order, cancel and execution cases. A position-change function returns the change for an execution and zero for the other cases: placing or canceling an order does not itself change the position.

Now add a correction message: an exchange may tell you that a previously reported trade was wrong. A correction can change the position too. If the original match used a wildcard for every non-execution case, the code still compiles but silently gives the wrong answer for corrections.

Had we named the order and cancel cases explicitly, adding correction would make the match incomplete. The compiler could point to the missing case. I use these checks every day; they are one of the language features I would be most reluctant to lose.

05

Names, errors and boilerplate

Limit open namespaces, make error behavior visible in names, and abstract repetitive code. These choices help readers locate meaning and reason about failures.

Open few modules. Opening a namespace makes the writer's expressions shorter, but opening many modules makes it difficult for a reader to determine where a value comes from. A name such as int might come from a flag module, a command module or a standard library.

Command-line flag code
FOLIE 3 / 3Command-line flag codeThe example illustrates how opening modules can make it harder to see where names originate.

In a command-line example, I can remove broad opens and still keep the code manageable. A local alias for a long module path makes repeated references concise while preserving a clear connection to their origin.

The local scope matters. If an abbreviation is defined at the top of a large file, a reader must carry it in memory for a long time. Define it close to where it is used so that the reader can understand it and then forget it.

Make common errors obvious. Whether a missing item is exceptional depends on the caller: in one context it is ordinary, and in another the item should always exist. Both an option-returning function and an exception-raising function can be useful.

A consistent naming convention can distinguish the two. For example, an ordinary lookup can return an option, while a version named with an _exn suffix makes the possibility of an exception visible at the call site. The point is to help readers reason about error behavior rather than hide it.

Avoid boilerplate: repeated or nearly repeated code that follows a common pattern with small changes. Developers often copy such patterns because they need the same structure in several places or because the language makes the abstraction awkward.

ML has strong abstraction tools, so repeated structure is often a sign that we can factor something out. As code evolves, a common structure may need to change, and a developer is unlikely to remember every copied instance.

Boilerplate is also hard to review. Even when a tiny difference, such as a sign in a buy-versus-sell calculation, matters enormously, readers tend to skim text that looks repetitive. No amount of telling them to pay attention completely removes that human tendency.

A local function can often capture the common pattern and make the variation explicit. The result is not always shorter, but readers can see what stays the same and what changes rather than comparing several near-identical blocks.

06

Complexity and side effects

Abstraction and static guarantees are useful only when they improve clarity. Pure code helps isolate effects, but mutable operations may be the simpler or faster choice.

An audience member asks whether the resulting higher-order abstraction can itself be harder to understand. That is a real concern. Most repetitions are straightforward to remove, but some abstractions demand elaborate machinery or a fight with the type system.

The temptation with a powerful language is to encode ever more guarantees in types simply because it is possible. But the enemy of correctness is complexity. If an experienced ML developer cannot understand the resulting types, the cleverness has gone too far.

There is beautiful academic work demonstrating how much can be encoded in a type system, including very elaborate examples. It can be interesting without being a good choice for production code. The right question is whether a technique makes the system easier to understand.

The same balance applies to purity. Code without side effects and immutable data structures are generally easier to reason about, but purity is not a religion. Local mutation can sometimes make an implementation simpler, and performance-sensitive systems may need it.

Our automated trading systems process large volumes of market data and must be fast. Programs also have to do things: send messages, write files and interact with the world. Useful software cannot avoid side effects altogether.

A language that supports pure programming lets us isolate side effects in clear, controlled places, making the rest easier to reason about, especially with concurrency. If achieving a pure and sufficiently efficient solution requires excessive complexity, a side effect may be the better choice.

07

Phantom types for read-only access

Minsky builds a reference example with read-only and read-write capabilities. A phantom parameter supports static restrictions and polymorphic reads without a distinct runtime representation.

After warning against excessive type-level tricks, I want to show one: phantom types. They let us add type-level information so the compiler can check extra properties. The information used for these checks need not have a corresponding runtime representation.

Start with a simple integer reference supporting create, get and set. I want a second handle that can read the same reference but cannot write it. The original holder can still change the value, and the read-only holder can see those changes.

One direct solution is a separate module with a more constrained signature. Its abstract type is implemented using the original reference, but its interface exposes creation from a reference and a get operation, not a set operation.

Although both handles may represent the same underlying value, their exposed types differ. The compiler therefore prevents code holding only the read-only handle from using the writable interface. The wrapper is essentially an identity operation, though whether a call disappears can depend on compiler inlining.

The drawback is polymorphism. Because the read-only and writable references have different types, I cannot write one simple function that sums a list of either kind, even though both support reading. We want a common read operation while retaining the write restriction.

Introduce two empty types as tags, one for read-only and one for read-write, and use a type parameter on the reference. The tags have no values to construct. They express a property of the handle without becoming part of the underlying reference representation.

The parameter appears in the name of the type but is absent from its implementation: that is the phantom. The reference operations themselves are ordinary; the important constraints live in the module signature.

The set operation requires a reference tagged read-write, so a read-only handle fails the type check. Get is polymorphic in the tag and can read either kind. The create operation is also polymorphic until its use imposes more specific constraints.

The compiler infers that a sum function only reads references and therefore can work with either tag. A function that doubles the values must use set, so its input must be writable. We do not have to write all those conclusions down by hand.

In a larger example, two lists created in the same way can acquire different types from later use. Placing one into a record field declared read-only and passing the other to a function requiring writable references provides constraints that type inference combines.

If the constraints conflict, compilation fails. Type inference is not just a matter of taking the constructor's return type; information flows through the program until the compiler has reconciled the uses. In this example it protects the capability boundary while preserving useful polymorphism.

This is a modest use of phantom types. More elaborate encodings are possible, but the value here is practical: the program can distinguish capabilities while sharing reading code. Other techniques, including type-indexed values and macros, offer further tools when the added complexity is justified.

08

Questions: scale and tradeoffs

He discusses modules and abstractions in a large codebase, then acknowledges limits of the OCaml ecosystem and its contemporary UI tooling.

In questions, I would emphasize modules as building blocks for large systems. An object often naturally abstracts one thing; a module can describe a collection of related types and operations and state which types are shared or distinct.

Modules, functors, parametric polymorphism and higher-order functions help factor a large system. At the time of the talk, Jane Street had between one and two million lines of ML code—large for that ecosystem, even if modest compared with the biggest corporate codebases.

Lower-level languages need abstractions and memory management at scale too. A large C library may build its own object machinery and memory-management conventions. In some places dropping down to C makes sense, but much of our system can be written in OCaml.

The ecosystem does impose costs. Fewer users mean fewer libraries and more work writing bindings. In 2010, I would not choose OCaml for web programming or cross-platform graphical interfaces because the available libraries were not strong enough for those tasks.

Some of our interfaces even use curses-style text displays. Generic facilities such as printing and serialization can also be awkward without additional techniques; we use macros to generate some of that code. These are real tradeoffs, although our team has addressed many of them for its own needs.

Mehr aus diesem Kanal

1:34:29EN
JSJane Street· 24. Juni 2026Jacob Baskin on Building Superstore and Scheduling the HiveYaron Minsky, Jacob Baskin · Jane StreetThe Jane Street engineer traces the design choices behind a distributed analytical database and the challenge of allocating a growing research cluster.Infrastruktur & DeploymentSoftware engineeringDecision Making+1
1:34:36EN
JSJane Street· 1. Juni 2026Nate Foster: Treating the Network as a ProgramYaron Minsky, Nate Foster · Jane StreetFrom programmable switches to BGP, a programming-languages researcher explains how higher-level policies and formal models can make networks easier to change.Infrastruktur & DeploymentProgramming LanguagesSoftware engineering+2
5 Folien15:48EN
JSJane Street· 15. Mai 2026Inside Jane Street’s Texas AI Data CenterDwarkesh Patel, Yaron Minsky, Daniel Pontecorvo · Dwarkesh PatelDwarkesh Patel tours a 4,032-GPU training facility with Ron Minsky and Dan Pontecorvo, examining liquid cooling, power distribution and operational safeguards.Infrastruktur & DeploymentAI cost & performance
7 Folien1:20:57EN
JSJane Street· 23. Dez. 2025Matt Godbolt: Inside Skylake’s Out-of-Order PipelineMatt GodboltA guided tour of instruction decoding, register renaming, execution and memory ordering, grounded in reverse engineering and careful measurements.SemiconductorsSoftware engineeringBenchmarks
1:12:58EN
JSJane Street· 4. Sept. 2025Chris Lattner on Mojo and the Search for Portable GPU PerformanceYaron Minsky, Chris Lattner · Jane StreetRon Minsky and Chris Lattner discuss the language and compiler design behind Modular’s effort to make accelerator programming both portable and fast.Programming LanguagesGPU programming & kernelsSoftware engineering+1
58:47EN
JSJane Street· 28. Juli 2025Daniel Pontecorvo on Cooling Trading Infrastructure and WorkspacesYaron Minsky, Daniel Pontecorvo · Jane StreetFrom dense GPU racks to movable desks, Jane Street’s physical engineering team designs around heat, power, failure and human comfort.Infrastruktur & DeploymentEnergyObservability & Zuverlässigkeit+1