OCaml Local Function Types: Why Partial Application Can Escape

Richard Eisenberg uses a list-append example to show how a local argument changes the mode of a partially applied function in Jane Street’s OCaml compiler branch.

RERichard EisenbergIntervenant · Jane Street
Chaîne
Jane Street
Publié
29 mai 2024
Vidéo
9:14
Lecture
4 min
Diapositives
4
Original
anglais
Ouvrir sur YouTube
En faisant défiler, la vidéo se réduit en bas à droite et continue. Les sous-titres suivent votre langue de lecture.
Langue de lectureDisponible en 2 langues
Cet article n’est pas encore disponible en français ; il s’affiche dans l’original en anglais.
RÉSUMÉ

En 30 secondes

In this OCaml Unboxed episode, Richard Eisenberg demonstrates a subtle interaction between local mode, currying and partial application. A function that accepts a local argument can produce a local closure when partially applied, even where its written type does not visibly say so. He explains why passing that closure to List.map fails, shows how eta expansion can make the example compile, and demonstrates why parentheses in a function type change the meaning in this extension.

  1. 1Why the integer example worksThe first list is copied into a global result, but integer elements are considered global through mode crossing. Replacing them with unconstrained polymorphic values causes an escape error.
  2. 2A partial application captures its inputList.map expects a global function in this example. Partially applying append_ints to its local first argument creates a closure that may retain that argument.
  3. 3The hidden local modeA local argument on a multi-argument function implies a local mode for the partial application, even if that mode is not written explicitly.
  4. 4Eta expansionWrapping the call in an explicit function supplies the second argument when the wrapper runs. The demonstrated program then compiles.
  5. 5Parentheses matterExplicitly parenthesizing the returned function type without local_ expresses a different mode, which the implementation cannot satisfy.

À retenir

  1. A local argument to a multi-argument function can make its partially applied closure local.
  2. A closure that captures a local value must not escape into a longer-lived context.
  3. Integers can cross modes, so an integer taken from a local list can be placed in a global list in the example.
  4. Eta expansion can avoid the problematic partial application by supplying the remaining argument explicitly.
  5. In this local-mode extension, parentheses around a returned function type can change its mode and meaning.
TEXTE INTÉGRAL

Lire le texte remanié, chapitre par chapitre

Transcription seule
01

A local list argument

Eisenberg builds a recursive integer-list append function with a local first argument. Integer mode crossing explains why its global result is safe.

In this episode of OCaml Unboxed, I want to continue exploring local mode in Jane Street’s OCaml compiler branch, focusing on function types. A function that takes a local value can have behavior that is not immediately apparent from the type as written.

Consider a recursive function, append_ints, that appends one integer list to another. If the first list is empty, it returns the second; otherwise it puts the first element in front of the recursive result. Tail recursion could improve the implementation, but it is not the issue in this example.

A local integer-list append
DIAPOSITIVE 1 / 4A local integer-list appendThe function type marks the first list local while the result remains global.

I give the first argument the type local_ int list, while the second argument and result are int list. The compiler accepts this. The first list is copied into the result and the second is not, so it is worth asking why elements from a local list can go into a global result.

The answer is that the elements are integers. An int crosses modes and is considered global, so it can be placed in the global list. If I instead make the element type polymorphic, the compiler reports that a value escapes its region: an arbitrary local element cannot safely be put into the global result.

02

The partial application escape

Passing a partially applied append function to List.map triggers an escape error. The closure must retain its first argument, but List.map expects a global function.

Now use List.map to append [1;2;3] to each list in a list of lists. Passing append_ints [1;2;3] as the function produces a region-escape error. The compiler explains that this is a partial application and suggests adding another argument.

A partial application constructs a function object that has to remember the first list until the second argument arrives. That object might live for a long time. Although we know List.map does not save its function, its standard-library type has no local annotation that promises this, so it requires a global function argument.

At first the remaining function type looks global: there is no visible local annotation beside it. But the closure must retain the local first argument. If a heap-allocated closure could point to a value on the local stack, that pointer might outlive the region, which is precisely what the mode restriction prevents.

03

Hidden modes and eta expansion

The inferred mode of a partial application is local when it retains a local argument. Eta expansion supplies the missing argument explicitly in the demonstrated call.

The key is an implicit local mode on the intermediate function. When a multi-argument function takes a local argument, its partial application is local because it stores that argument in a closure. Writing the local annotation explicitly at that point does not change the meaning; the compiler already inferred it.

The partial application escape error
DIAPOSITIVE 2 / 4The partial application escape errorThe compiler reports that the partially applied append function escapes its region and suggests adding the remaining argument.

There is a way to make this particular call work: eta-expand it. Instead of passing append_ints [1;2;3] directly, pass a function that takes x and calls append_ints [1;2;3] x. The example compiles because the call is fully applied inside the wrapper. The compiler can treat the list as global in this context and use it where a local argument is accepted.

Eta-expanded call and explicit local mode
DIAPOSITIVE 3 / 4Eta-expanded call and explicit local modeThe wrapper applies append_ints to both arguments, while the type explicitly shows the local intermediate function.

The distinction becomes visible when I add parentheses to the function type. Ordinarily we learn to read nested arrows as curried functions, but with locals the explicitly parenthesized form is not equivalent to the unparenthesized one.

04

Parentheses change the function type

An explicitly parenthesized returned function implies a different mode from the unparenthesized form. Eisenberg explains the resulting error and the compiler hint.

Parentheses and an escaping function
DIAPOSITIVE 4 / 4Parentheses and an escaping functionParenthesizing the returned function type changes its mode and produces a different compiler error.

Without a local annotation on the parenthesized returned function, the type says that applying the first argument yields a global function. My implementation would need an intermediate heap closure holding a local argument, so the compiler correctly reports that the function or one of its parameters escapes when partially applied.

The parentheses effectively express a global mode there, even though there is no global keyword written in that position. In the original unparenthesized type, the intermediate local mode is implicit. This explains why the original partial application failed and why supplying the remaining argument in the wrapper works.

The compiler has a specific hint for this partial-application case because it has confused many users. It is an unusual corner of local mode and one reason we have considered syntax for function definitions and arrows that do not allow partial application.

Sur la même chaîne

1:34:29EN
JSJane Street· 24 juin 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.Infrastructure et déploiementSoftware engineeringDecision Making+1
1:34:36EN
JSJane Street· 1 juin 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.Infrastructure et déploiementProgramming LanguagesSoftware engineering+2
5 diapositives15: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.Infrastructure et déploiementAI cost & performance
7 diapositives1:20:57EN
JSJane Street· 23 déc. 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 juil. 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.Infrastructure et déploiementEnergyObservabilité et fiabilité+1