Skip to content

Instantly share code, notes, and snippets.

@matthewhammer
Last active October 19, 2023 13:01
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save matthewhammer/a092b9e4ae23d7f478d16a06605c755b to your computer and use it in GitHub Desktop.
Save matthewhammer/a092b9e4ae23d7f478d16a06605c755b to your computer and use it in GitHub Desktop.
CBPV: Different Types for Partial/Full Application

CBPV: Different Types for Partial/Full Application

Instead of using call-by-value (CBV), I often use Call-by-push-value (CBPV) in my core calculi, and many people ask me "why?".

Most reasons relate to the "fine-grained" typed structure that it introduces, and the ability of this structure to describe "low-level" concerns, like the difference between partial and full function application (of curried, higher-order functions).

An example of partial application

For example, consider this use of function f:

let g = f 1
let x = g 3
let y = g 4
(x, y)

Without effects the above use is the same as, but when effectful, is different from the following:

let x = f 1 3
let y = f 1 4
(x, y)

In the first version, f is partially applied, then applied twice. By contrast, the second version uses two separate (full) applications.

In CBV and CBN, both versions of function f have the same type (e.g., int -> int -> int). In fact, in CBV and call-by-name (CBN), curried functions used for partial application always have the same type as ones whose applications are all full. However, in the presence of effects (or any low-level reasoning), we may want those functions and uses to have different types.

For instance, suppose we were writing a compiler. We'd want to treat (generalized versions of) these situations differently: as compiler designers, we may care about the difference between applying f to one argument, then getting back a function (as a value), versus the second case, of applying both arguments, and never needing to produce an intermediate closure. Likewise, systems programmers care about this distinction too. Beyond caring about the operational cost of creating closures, these audiences may also want to reason precisely about effects, which are also different in the two cases above.

CBPV and partial application

Notably, in call-by-push-value (CBPV), those two versions of function f (for partial vs full application) have different types. CPBV distinguishes value types from computation types in a way that permits us to describe the difference between applying f to one argument, then getting back a function (as a value), vs in the second case, applying both arguments, and never needing to produce a closure value.

Consider these two distinct (value) types for f in CBPV:

 f: U(int -> F(U(int -> F(int))))    // type for partial application
 f: U(int -> int -> F(int))          // type for full application (only)

CBPV background

To adopt a CPBV typing mindset, we start by assuming that expressions for value terms and computation terms have distinct syntax, with distinct type syntax representing distinct "spaces" of types.

Value types:  A, B ::= 1 | 0 | A x B | A + B | U C
Comp types:   C, D ::= A -> C | F A | C x D

While these value vs computation type spaces are distinct, they are closely related: the U connective injects a computation type into the space of value types. The F connective injects a value type into the space of computation types. Intuitively, read U C as "a thunk value whose suspended computation has type C", and read F A as "a computation that will produce a value of type A".

Returning to our example function f, and its two CBPV types: The arrow (A -> C) and int types are otherwise standard, though the former is a computation type (parameterized by a value type argument A, and a "result" computation type C) and the latter is a simple base value type.

As the reader may notice, in CBPV, the version for partial application has one additional occurrence of U and of F (each), to describe the production of the partially-applied function -- a thunk value holding the rest of the function. This type reflects the "fine-grained structure" hidden by CBV: The intermediate closure introduced by partial application.

Furthermore, the full application type in CBPV has no analog in CBV: CBV lacks the necessary distinctions to be sensitive to full versus partial applications in the type structure, and to universally enforce the former. Meanwhile, CBPV has exactly that "extra" type structure.

Some further discussion

Q: What's the relationship between CBPV and systems languages?

A: As a C/Rust programmer, I think CBPV gives a particularly nice core calculus for describing the typical programming mindset of a systems programmer: aware of where closures are created and used. Systems programmers think about resources at the level of closures; CBPV can be viewed as adding this "extra" distinction to CBV, though it also adds a bit more too (e.g., co-products).

Q: Aren't curried functions in CBPV the same as uncurried functions, whose arguments consist of a single tuple?

A: In many cases, but not generally. CBPV has more computation type connectives than merely arrow. E.g., CBPV has co-products (CBV lacks them), and the mixture of arrow and co-products permits some computation type patterns involving currying that do not exist directly in CBV, and cannot (easily?) be expressed by uncurried applications.

Q: Where can I read more about CBPV?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment