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.
- 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.
- 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.
- 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.
- 4Eta expansionWrapping the call in an explicit function supplies the second argument when the wrapper runs. The demonstrated program then compiles.
- 5Parentheses matterExplicitly parenthesizing the returned function type without local_ expresses a different mode, which the implementation cannot satisfy.
À retenir
- A local argument to a multi-argument function can make its partially applied closure local.
- A closure that captures a local value must not escape into a longer-lived context.
- Integers can cross modes, so an integer taken from a local list can be placed in a global list in the example.
- Eta expansion can avoid the problematic partial application by supplying the remaining argument explicitly.
- In this local-mode extension, parentheses around a returned function type can change its mode and meaning.
Lire le texte remanié, chapitre par chapitre
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.

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.
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.
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.

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.

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.
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.

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
1:34:36EN
15:48EN
1:20:57EN
1:12:58EN
58:47EN