Composing Contracts, Part 1: Ten Combinators for Every Financial Contract

scaladsl

This series works through “Composing Contracts: An Adventure in Financial Engineering” by Simon Peyton Jones, Jean-Marc Eber and Julian Seward (ICFP 2000) — a ten-page paper that takes an industry with thousands of named products and shows the whole thing is ten combinators and a compositional semantics. The design is theirs; the Scala 3 is mine.

It’s the best applied DSL paper I’ve read, and it went on to have a second life: Eber founded LexiFi on these ideas and the company is still trading on them twenty-five years later. That’s a rare thing for an ICFP paper.

🔗The catalogue problem

Financial contracts come with an enormous vocabulary. Swaps. Futures. Caps. Floors. Swaptions. Spreads. Straddles. Captions. European options, American options, Bermudan options. The list keeps going, and each entry is a named, understood, separately-implemented product.

The paper’s diagnosis is one that will be familiar to anyone who has maintained a component library: a catalogue of prefabricated parts is fine until someone wants a part that isn’t in it. And in finance, someone always does — inventing a new contract is how you make money.

The alternative is to find a small, fixed set of primitives such that every contract in the catalogue is a composition of them. Then a new contract isn’t a new implementation, it’s a new expression. And — this is the part that matters commercially — because every contract is built from the same fixed vocabulary, you can systematically analyse it. Value it. Generate its payment schedule. Compute its risk. Draw it.

The authors quote an anonymous financial engineer’s response to a draft, describing the lack of a precise way to describe complex contracts as the bane of their working lives. Which is roughly what any of us would say about a codebase with no abstractions.

If this smells like algebra-driven design, it should. The moves are the same — find the primitives, give them a compositional semantics, derive everything else — arrived at independently and twenty years earlier.

🔗What a contract is

Start with the thing the paper is careful about and everyone else is sloppy about.

A contract is an agreement between two parties: the holder and the counter-party. By default the holder receives the payments and makes the choices. Two ideas govern everything:

The acquisition date. A contract describes rights and obligations, but what those are worth depends on when you acquire it. “Receive $100 on 1 Jan 2025 and $100 on 1 Jan 2026” is worth considerably less if you acquire it in 2025 — anything falling due before the acquisition date is simply discarded.

The horizon. The latest date at which a contract can be acquired. It may be infinite. Crucially, a contract’s rights can extend far beyond its horizon: “the right to decide, on or before 1 Jan 2026, whether to take contract C” has a horizon of 1 Jan 2026, but if you acquire it in time, C’s consequences may run for decades.

The horizon is a property of the contract. The acquisition date is not. That distinction is load-bearing and I got it wrong twice while writing this.

🔗Dates, and a use for opaque types

We need absolute times and time differences, and we need not to confuse them.

opaque type Date = Double   // days since the epoch
opaque type Days = Double   // a difference, in days — fractions matter

object Date:
  def apply(s: String): Date = parse(s)
  extension (d: Date)
    def diff(other: Date): Days = d - other
    def add(n: Days): Date = d + n
    infix def isBefore(other: Date): Boolean = d < other

Two opaque types over Double, zero runtime cost, and it’s a compile error to add two dates or to pass a duration where an instant is expected. This is the textbook case for the feature and it costs nothing.

One happy accident: because Date is a Double, an infinite horizon is just Double.PositiveInfinity, and min/max over horizons work without special-casing. The paper has to say “we extend min and max in the obvious way to such infinities”; we get it free.

🔗The ten primitives

Here they are. I’ll describe each in my own words — the paper’s Figure 2 gives careful English definitions and is worth reading directly.

enum Contract:
  case Zero
  case One(currency: Currency)
  case Give(c: Contract)
  case And(c1: Contract, c2: Contract)
  case Or(c1: Contract, c2: Contract)
  case Truncate(t: Date, c: Contract)
  case Thereafter(c1: Contract, c2: Contract)
  case Scale(o: Obs[Double], c: Contract)
  case Get(c: Contract)
  case Anytime(c: Contract)

Zero — no rights, no obligations, infinite horizon. The unit.

One(k) — acquire it and you immediately receive one unit of currency k. Infinite horizon.

Give(c)c with rights and obligations swapped. When two parties agree a contract, one acquires c and the other simultaneously acquires Give(c).

And(c1, c2) — acquire both (skipping either that has expired).

Or(c1, c2) — acquire exactly one, your choice, immediately. An expired branch can’t be chosen.

Truncate(t, c)c with its horizon trimmed to the earlier of t and c’s own horizon. Note carefully: this limits when c can be acquired. It does not truncate c’s rights, which may run long past t.

Thereafter(c1, c2) — acquire c1 if it hasn’t expired, otherwise c2.

Scale(o, c) — acquire c, with every payment multiplied by the value of observable o sampled at the moment of acquisition. Part 2 is about observables.

Get(c) — acquire c at c’s horizon, i.e. at the last possible moment, regardless of when you acquired the compound.

Anytime(c) — you must acquire c, but you may choose when, any time up to c’s horizon.

Ten cases in an enum. That’s the whole language.

The paper calls Thereafter simply then. That’s a keyword in Scala 3 — it’s the then in if cond then x else y — so a method called then needs backticks at every call site: c1 `then` c2. Legal, and I decided it wasn’t worth it. thereafter reads about as well and doesn’t make every use site look like an escape hatch.

🔗The horizon function

Every contract’s horizon is computable from its syntax alone:

def horizon(c: Contract): Date = c match
  case Zero | One(_)          => Date.infinite
  case Give(c)                => horizon(c)
  case And(c1, c2)            => horizon(c1) max horizon(c2)
  case Or(c1, c2)             => horizon(c1) max horizon(c2)
  case Thereafter(c1, c2)     => horizon(c1) max horizon(c2)
  case Truncate(t, c)         => t min horizon(c)
  case Scale(_, c)            => horizon(c)
  case Get(c)                 => horizon(c)
  case Anytime(c)             => horizon(c)

Nine lines, total, no model needed. Truncate is the only case that shortens anything — which is exactly the point of having it as a separate primitive.

🔗Building a bond out of four of them

The simplest real contract in finance is the zero-coupon discount bond: receive $100 on 1 January 2030. Surely that’s primitive?

It is not. It’s four primitives stacked:

val bond = Scale(konst(100), Get(Truncate(Date("1 Jan 2030"), One(GBP))))

Read it inside out, and each layer does exactly one thing:

  1. One(GBP) — receive £1, whenever you acquire it. Infinite horizon.
  2. Truncate(t, _) — now its horizon is t. It can’t be acquired later than that.
  3. Get(_) — acquire it at its horizon. Since the horizon is now t, that means: receive £1 at t, and no earlier, whatever the acquisition date of the whole thing.
  4. Scale(konst(100), _) — multiply every payment by 100.

So:

def zcb(t: Date, x: Double, k: Currency): Contract =
  Scale(konst(x), Get(Truncate(t, One(k))))

The obvious question is why. zcb is the fundamental building block of fixed income; why not make it primitive and save three layers?

The paper’s answer is the argument for the whole approach, and it’s worth quoting the shape of it: because scale, get, truncate and one are each independently useful, and separating them dramatically simplifies the semantics. A primitive zcb would need its own valuation rule bundling four unrelated concerns. Four primitives need four simple rules, and zcb’s semantics then falls out of composing them. Part 3 is where that promise gets paid, and it pays in full: the semantics of the entire language fits on one page.

This is precisely the argument from the tile algebra — pick a minimal set of primitives, derive everything else, and your semantics shrinks to something you can actually reason about.

🔗Deriving the vocabulary

With ten primitives and smart constructors, the industry’s catalogue becomes a library of one-liners. Extension methods make it read the way finance people talk:

extension (c: Contract)
  infix def and(d: Contract): Contract        = And(c, d)
  infix def or(d: Contract): Contract         = Or(c, d)
  infix def thereafter(d: Contract): Contract = Thereafter(c, d)
  def give: Contract                          = Give(c)
  def scaleBy(x: Double): Contract            = Scale(konst(x), c)

def zcb(t: Date, x: Double, k: Currency): Contract =
  Get(Truncate(t, One(k))).scaleBy(x)

// Receive £100 at t1, pay £200 at t2
val c4 = zcb(t1, 100, GBP) and zcb(t2, 200, GBP).give

and, or and give are ordinary identifiers in Scala — no backticks needed, unlike then. And because these are just functions, defining a new combinator is indistinguishable from using a built-in one. The paper notes that this is entirely routine for functional programmers and not at all routine for financial engineers, which I think is the most quietly damning sentence in it.

A European option — the right, at one specific date, to decide whether to take an underlying contract — is three primitives:

def european(t: Date, underlying: Contract): Contract =
  Get(Truncate(t, underlying or Zero))

underlying or Zero is “take it, or take nothing” — the choice. Truncate(t, _) fixes the horizon at t. Get forces the decision to happen at that horizon. That is exactly what a European option is, and it took no new machinery at all.

That pattern recurs constantly, so it gets a name:

def perhaps(t: Date, u: Contract): Contract = Truncate(t, u or Zero)
def european(t: Date, u: Contract): Contract = Get(perhaps(t, u))

🔗The contract from the paper’s first page

The paper opens with a deliberately gnarly example: the right to choose, on 30 June 2000, between (a) a pair of cashflows, and (b) a further option, exercisable in December, between two other pairs of cashflows. An option over an option over cashflows.

The point of showing it on page one is that no catalogue contains it. Here it is:

val d11 = zcb(Date("29 Jan 2001"), 100, USD)
val d12 = zcb(Date("1 Feb 2002"), 105, USD).give
val d1  = d11 and d12

val d21 = zcb(Date("29 Jan 2001"), 100, USD) and
          zcb(Date("1 Feb 2002"), 106, USD).give
val d22 = zcb(Date("29 Jan 2001"), 100, USD) and
          zcb(Date("1 Feb 2003"), 112, USD).give
val d2  = european(Date("15 Dec 2000"), d21 or d22)

val c = european(Date("30 Jun 2000"), d1 or d2)

Nested options, composed without ceremony, using nothing that wasn’t in the ten primitives. The paper mentions in passing that their Haskell prototype could already value contracts — options over options specifically — that the production C++ system it was compared against could not, not because C++ can’t do it but because the production system handled contracts case by case and the hard cases were daunting to add.

That’s the payoff of compositionality stated as a business outcome, which is not something you often get to point at.

🔗What’s missing

Two things I’ve been quietly deferring.

konst. I’ve used it in every scale without saying what it is. Observables — objective, time-varying quantities like an interest rate or a temperature — are a whole second type with their own combinators, and they’re what makes contracts interesting rather than just schedules of fixed payments. Part 2.

Choice. Or lets you choose which contract; Anytime lets you choose when. The paper describes separating those two forms of choice, and encapsulating each in one combinator and nothing else, as a breakthrough. Part 2 shows what that buys, by deriving American options — and getting it wrong twice on the way.

Next: Part 2 — Observables, and the two kinds of choice.


🔗Sources

  • Peyton Jones, Simon, Jean-Marc Eber & Julian Seward. Composing Contracts: An Adventure in Financial Engineering. ICFP 2000. The combinator set, the acquisition-date/horizon distinction, the horizon function, the decomposition of zcb into four primitives, the european/perhaps derivation, and the opening nested-option example are all theirs. The paper is short and very readable — go and read it.
  • Jean-Marc Eber went on to found LexiFi, which commercialised this work.
  • Maguire, Sandy. Algebra-Driven Design. 2020 — for the design method this paper independently anticipates, covered in an earlier series.

The Scala 3 translation, the opaque-type treatment of Date/Days, and the note on then being a Scala keyword are mine.