Keywords

figure a
figure b

1 Introduction

An invertible computation can be run in two ways: forward in the conventional way, or backward to recover an input given the output. Such processes appear frequently and prominently in a variety of contexts, enabling the shape of information to be adapted to different purposes, while preserving the essential content. For instance, (lossless) compression shrinks the size of a piece of information to facilitate efficient storage, encryption transforms it to be inaccessible to third parties, and serialization reshapes it to enable storage or transmission. The property of invertibility is crucial, as it guarantees that the data can always be refit to its original purpose.

For example, consider the function \( autokey \) below, which computes a variant of the Autokey cipher (see e.g., [50]). The cipher takes a primer character \( k \), and interprets it as an integer (e.g., \(\texttt {'A'} \mapsto 0, \texttt {'B'} \mapsto 1, \ldots , \texttt {'Z'} \mapsto 25\)) determining a shift to apply to the first element of the input. Each consecutive character in the input is similarly shifted by the amount given by its predecessor. For instance, \( autokey \;\texttt {'F'} \;\texttt {"HELLO"} = \texttt {"CXHAD"}\), as ’F’ represents a (cyclic left) shift of 5 characters, mapping ’H’ to ’C’, and ’H’ a shift of 7 characters, mapping ’E’ to ’X’, and so on.

$$\begin{aligned}{}\begin{array}{@{}l@{}} autokey :\,\!: \textsf{Char} \rightarrow [\textsf{Char}] \rightarrow [\textsf{Char}]\\ \begin{array}{lllll} autokey &{}k &{} [\,]&{}=&{} [\,]\\ autokey &{}k &{} (h : t) &{}=&{} \end{array}\\ \quad shift \;( chrToInt \;k) \;h : autokey \;h \;t \end{array}&\quad & \begin{array}{@{}l@{}} autokey ^\prime :\,\!: \textsf{Char} \rightarrow [\textsf{Char}] \rightarrow [\textsf{Char}]\\ \begin{array}{lllll} autokey ^\prime &{}k &{} [\,]&{}=&{} [\,]\\ autokey ^\prime &{}k &{} (h' : t') &{}=&{} \end{array}\\ \quad \begin{array}{ll} \textbf{let} &{}h = shift \;(-( chrToInt \;k)) \;h'\\ \textbf{in} &{}h : autokey ^\prime \;h \;t' \end{array} \end{array} \end{aligned}$$

The corresponding decryption function \( autokey ^\prime \) is given to the right, and shifts backward to restore the original input. We assume \( shift : \textsf{Int} \rightarrow \textsf{Char} \rightarrow \textsf{Char}\) performing the cyclic shift is previously defined. This is a simple example, but it serves as a toy model of more advanced encryption schemes and has a few interesting features which we highlight momentarily.

In traditional unidirectional languages, each direction of an invertible algorithm has to be specified separately in this way, and there is no easy way of ensuring that the two programs really constitute each other’s inverses. Furthermore, there is a maintenance concern—when one direction is updated, the other has to be updated accordingly. An alternative, more scalable approach is to let a single program denote both directions at the same time—intuitively, the inverse is derived by “reading the original code right-to-left”. Invertible programming languages implement this approach, letting each program be executed in either of two directions, which are guaranteed to form a pair of inverse functions. Some examples of invertible languages include Janus [35, 53], R [17], Inv [43], \(\varPi \) [10, 26], RFun [54], Theseus [27], \(\textsf{CoreFun}\) [25] and Sparcl [39, 40].

These languages traditionally require each individual step of computation to be invertible, which can be ensured, e.g., by providing a set of invertible combinators as basic building blocks, or by imposing various syntactic restrictions. This form of local invertibility has several benefits, in addition to being a simple foundation for building programming languages. For example, it was observed early on that discarding information fundamentally results in heat dissipation, meaning that a machine executing only invertible instructions could in principle operate at lower energy levels than a conventional computer [32]. Moreover, locally invertible languages serve as a foundation when considering other domains with similar requirements, such as quantum computing, where computations are composed of individually invertible quantum gates along with irreversible measurements [22, 48]. Despite these benefits, the local flavor of invertibility severely limits the flexibility of the programmer. In particular, our example function \( autokey \) is not actually invertible up front! The case \(~ autokey \;k \;[\,]= [\,]~\) discards the value of \( k \), which means we cannot simply read the definition right-to-left. Of course, the primer \( k \) is not intended to be treated as part of the invertible input to \( autokey \), but rather as a parameter determining the bijection between input and output strings. However, this cannot be naturally expressed in a language adhering strictly to the (locally) invertible paradigm, where the parameter would need to be preserved in the result.

The property of becoming invertible when some parameters are fixed is known as partial invertibility [39, 40, 44, 47], and many previous languages offer some form of support for partially invertible definitions. However, the level of support varies from more limited (e.g., [25, 27, 35]) to more complete (e.g., [39, 40]), and the previous work largely lacks a systematic treatment. The case of \( autokey \) is especially tricky, since its invertible input \( h \) flows to the unidirectional parameter \( k \) in the recursive call. To our knowledge, only Sparcl [39, 40] handles cases like this in a systematic way, but it does so through an advanced language foundation quite different from that of traditional invertible languages, and its connection to the locally invertible paradigm is not well-understood. Thus, it is an open question whether it is possible to support fully expressive partial invertibility while maintaining a compositional locally invertible interpretation.

It is theoretically known that any (partially) invertible computation can be simulated in a locally invertible system [8]; however, this simulation gives poor control over the invertible behavior and is inefficient in both time and space. There has been research on inversion of arbitrary programs (e.g., [41, 44, 49]), and on logic languages with no fixed direction of execution, like Prolog and Curry, which use (lazy) generate-and-test to find inputs corresponding to a given output [4]. Yet, these approaches lack the guarantee of invertibility, which is the main motivation of an invertible language.

1.1 Contributions and Organization

In this paper, we identify a core set of constructs for partially invertible programming, and explain them in terms of a locally invertible semantics. These constructs are sufficient to allow expressive partially-invertible and higher-order computation, thus solving an open problem in the invertible programming literature. The constructs include (1) partially invertible branching, (2) pinning invertible inputs, (3) partially invertible composition, and (4) abstraction and application of invertible computations.

We demonstrate the above findings by designing and formalizing two systems based on these constructs, KalpisFootnote 1 and \(\textsc {rrArr}\). Kalpis is a typed functional programming language accommodating expressive partially-invertible and higher-order computation, and \(\textsc {rrArr}\) is an arrow combinator language intended to capture the essence of partially invertible programs. Kalpis is given semantics via \(\textsc {rrArr}\), which captures partial invertibility as an effect on top of ‘pure’ invertible computations, intuitively adjoining a parameter to an invertible function, analogously to the reader monad in unidirectional computation. By interpreting terms of Kalpis as parameterized bijections, we are able to give a translation into \(\textsc {rrArr}\) combinators, giving a compositional embedding into a locally invertible setting. Thus, we present a simple and rigorous take on partial invertibility which bridges the gap between previous work in the field.

The core constructs for partial invertibility that we present are not new per se, and the features of Kalpis largely coincide with those of Sparcl [39, 40]. However, the goal of this paper is not to present Kalpis as such, but rather to describe partial invertibility from first principles and give a simpler semantics which is compatible with local invertibility. There are key technical differences between the two languages, and the fact that they are still similar should be taken as a sign that we have achieved our goal without a significant loss of expressiveness.

In summary, our main contributions are:

  • We identify a core set of partially invertible programming constructs (Section 2), which we demonstrate to be sufficient to achieve a level of expressiveness similar to the state-of-the-art.

  • We showcase the constructs through the design of the invertible functional language Kalpis, including a formal type system and operational semantics (Section 3).

  • We present \(\textsc {rrArr}\), an extension of the irreversibility effect [26] and the reversible reader [23] (Section 4) as a core calculus for partially invertible computation with a locally invertible interpretation.

  • We give a compositional translation from Kalpis into \(\textsc {rrArr}\) (Section 5).

  • We prove type safety and invertibility properties (Section 3), and prove the correctness of the arrow translation (Sections 4 and 5).

  • Our developments come with a formalization in Agda including proofs of all theorems,Footnote 2 and a prototype implementation of Kalpis.Footnote 3

Section 6 discusses the results in relation to previous work, and Section 7 concludes.

2 Constructs for Partially Invertible Programming

In this section, we introduce a set of core constructs for partially invertible programming and explain their intuitive idea using programming examples in our partially invertible language Kalpis, which we introduce formally in Section 3. The constructs include (1) partially invertible branching, (2) pinning invertible inputs, (3) partially invertible composition, and (4) abstraction and application of invertible computations. We explain them each in turn, and show how they can be understood as operations on parameterized bijections, which we exploit in later sections to embed them into a locally invertible setting.

These constructs act as a form of glue, allowing invertible and unidirectional computations to be run in tandem. Thus, we also assume some traditional invertible constructs taken from the existing literature, like invertible pattern matching, which we briefly explain where necessary.

2.1 Partially Invertible Branching

As a first example, we define partially invertible addition. In particular, the function \(x \mapsto x + n\) has inverse \(x \mapsto x - n\) for any \(n \in \mathbb {N}\). Kalpis supports recursive type definitions, and we can define the naturals as follows.

$$\begin{aligned} \textbf{data} ~\textsf{Nat} = \textsf{Z} \mid \textsf{S}\;\textsf{Nat} \end{aligned}$$

Now, addition is implemented naturally by the following function \( add \), taking an \( n \) to produce the corresponding bijection.

$$\begin{aligned} \quad & \begin{array}{l}\begin{array}{ll}\textbf{sig} &{} add : \textsf{Nat} \rightarrow \textsf{Nat} \leftrightarrow \textsf{Nat}\\ \textbf{def} ^\bullet &{} add \;n \;x =\\ \end{array}\\ \quad \begin{array}{l} \textbf{case} ~n~\textbf{of} \\ \quad \begin{array}{lcl} \textsf{Z} &{}\rightarrow &{} x \\ \textsf{S} \;n &{} \rightarrow &{} \textsf{S} \;( add \;n \diamond x) \end{array}\end{array}\end{array} & \end{aligned}$$

The language uses a functional syntax, and features elements typical to invertible programming: a bijection type \(A \leftrightarrow B\), bijection definition \(\textbf{def} ^\bullet \), and bijection application \(f \diamond x\). The functional types associate to the right, so the type of

$$\begin{aligned} add : \textsf{Nat} \rightarrow \textsf{Nat} \leftrightarrow \textsf{Nat} \end{aligned}$$

indicates a partially invertible function taking a \(\textsf{Nat}\) to produce a bijection \(\textsf{Nat} \leftrightarrow \textsf{Nat}\). The \(\textbf{case}\) form showcases our first core construct, partially invertible branching. If \( n \) is zero, \( x \) is returned unchanged, and otherwise \(\textsf{S}\) is applied to the result of a recursive computation. The resulting function appends \( n \) copies of \(\textsf{S}\) to \( x \) in the forward direction, or peels them off in the backward direction.

What is interesting is that \(\textbf{case}\) results in a loss of information: without prior knowledge of \( n \), it is impossible to determine which branch to choose when executing backwards. This corresponds to the fact that one cannot uniquely determine \( n \) and \( x \) given \( y = n + x\). However, when \( n \) is fixed beforehand, we can refer to its value regardless of executing forwards or backwards, which is what motivates the \(\textbf{case}\) construct. For example, we get the following results when applying \( add \) to some example inputs, where the primitive operator \((\cdot )^\dagger : (A \leftrightarrow B) \rightarrow (B \leftrightarrow A)\) lets us compute the inverse.

$$\begin{aligned} \quad & \begin{array}{l} \texttt {-{}-}~\text {1 + 2 = 3}\\ \texttt {>}~ add \;(\textsf{S}\;(\textsf{S}\;\textsf{Z})) \diamond \textsf{S} \;\textsf{Z} \\ \textsf{S}\;(\textsf{S}\;(\textsf{S}\;\textsf{Z})) \end{array}&\quad &\begin{array}{l} \texttt {-{}-}~\text {3 - 2 = 1}\\ \texttt {>}~ ( add \;(\textsf{S}\;(\textsf{S}\;\textsf{Z})))^\dagger \diamond \textsf{S}\;(\textsf{S}\;(\textsf{S}\;\textsf{Z}))\\ \textsf{S} \;\textsf{Z} \end{array}& \end{aligned}$$

As the type \(\textsf{Nat} \leftrightarrow \textsf{Nat}\) requires, the argument \( x \) in the definition of \( add \) must be treated linearly, i.e., must be used exactly once in any successful evaluation (see e.g., [51]) in order to ensure invertibility. For instance, changing the first case above to \(\textsf{Z} \rightarrow \textsf{Z}\) gives an error, as x is unused in the case body. Indeed, if \(x\) is never used, there is no way to recover its value in the backward direction. While allowing more than one use does not directly prevent invertibility, it requires implicit copying of values, which may induce unintended runtime failures in the backward execution. Similarly, we cannot branch on \( x \) using \(\textbf{case}\) for the reasons mentioned above; instead, an invertible \(\textbf{case} ^\bullet \) form is available, explained later.

Note that \( add \) is not a total function: e.g., the application \(( add \;(\textsf{S} \;\textsf{Z}))^\dagger \diamond \textsf{Z}\) will try to peel an \(\textsf{S}\) when there is none, resulting in a runtime error.Footnote 4 The guarantee given by Kalpis is that whenever evaluating a bijection f on argument v gives \(v^\prime \) in the forward direction, then evaluating f on \(v^\prime \) gives v in the backward direction, and vice versa (this is made formal in Section 3).

Mathematically, \( add \) represents a parameterized bijection, a family of (partial) one-to-one mappings \(f_n : \mathbb {N} \rightarrow \mathbb {N}\) (such that \(f_n(x) = x + n\)). This view will underpin our explanation of partially invertible computations in later sections, and each of the core constructs in this section can also be understood from this viewpoint. Seen from this perspective, the \(\textbf{case}\) construct allows definitions of the form

$$\begin{aligned} f_n(x) = \left\{ \begin{array}{ll} g_n(x) \quad &{}\text {if}~n=0 \\ h_n(x) &{} \text {otherwise}\end{array}\right. , \end{aligned}$$

where \( g \) and \( h \) are also parameterized bijections.

2.2 Pinning Invertible Inputs

As a second example, we consider a program \( fib \) computing pairs of Fibonacci numbers (defined by the equations \(F_0 = F_1 = 1\) and \(F_{n+1} = F_{n} + F_{n-1}\) for \(n > 0\)), a classic in the invertible programming literature (e.g., [18, 53]). We can compute \( fib \;n\) by case distinction on \( n \); if \( n = 0\), we return \((F_0, F_1)\), and otherwise we recursively obtain \( fib \;(n - 1) = (F_{n-1}, F_n)\), with which we compute the next pair \((F_n, F_n + F_{n - 1})\).

However, if we try to implement this algorithm invertibly using the function \( add \) above, we encounter an issue: we cannot make the call \(~ add \;F_n \diamond F_{n - 1},~\) as \( add \) does not treat its first argument invertibly. Since \(F_n\) comes from the invertible input \( n \), we need an operation that is properly invertible in both inputs. To this end, we can define an invertible addition \( add' \) such that \(~ add' \diamond (x, y) = (x, x + y).~\) By preserving a copy of \( x \) in the output, the same \( x \) can be used to recover \( y \) by subtraction in the inverse direction. Indeed, \( add' \diamond (F_{n}, F_{n-1})\) gives just the result we need. In Kalpis, \( add' \) can be derived from \( add \) automatically using our second core construct, \( pin \).

$$\begin{aligned} \quad & \begin{array}{ll} \textbf{sig} &{} add' : (\textsf{Nat}, \textsf{Nat}) \leftrightarrow (\textsf{Nat}, \textsf{Nat})\\ \textbf{def} ^\bullet &{} add' \;(x, y) = pin \; add \diamond (x,y) \end{array} & \end{aligned}$$

Here, the operator \(~ pin : (c \rightarrow a \leftrightarrow b) \rightarrow (c, a) \leftrightarrow (c, b)~\) lifts a partially invertible function to operate on invertible data; we refer to this as pinning the invertible input \( x \), allowing it to be used in a unidirectional position. This construct (inherited from Sparcl [39, 40]) is crucial in practical programming, as it lets unidirectional computations depend on invertible data in a controlled manner. With \( add' \) defined, \( fib \) can be written as follows.

figure c

This example is defined by invertible pattern matching (\(\textbf{case} ^\bullet \)), a construct inherited from previous languages like Janus [35, 53] and \(\varPsi \)-Lisp [7]. When branching on the input to a bijection (as opposed to a fixed parameter), postconditions marked by the keyword \(\textbf{with}\) ensure that the execution can determine which branch to take in the backward direction. Each postcondition is a boolean function that must return \(\textsf{True}\) for any result of its branch and \(\textsf{False}\) for any result of the branches below it (this is checked at runtime following the symmetric first-match policy [54]). The backward evaluation tests each condition in turn, selecting the first branch whose condition is true. Here, \( is11 \) is used to distinguish the base case where the output is \((\textsf{S}\;\textsf{Z},\textsf{S}\;\textsf{Z})\).

The inverse behavior of \( fib \) computes \( n \) given a pair \((F_n, F_{n+1})\). Specifically, by computing \(F_{n+1} - F_n\), we obtain \(F_{n-1}\), and repeating the process until we reach the start of the sequence lets us deduce the index of the initial pair. Kalpis runs \( fib \) as below.

figure d

Again, \( fib \) is non-total: running it backwards on a pair not constituting two consecutive Fibonacci numbers will cause the computation to fail.

Viewed as an operation on parameterized bijections, \( pin \) lets part of an invertible input be shifted to the parameter position if a copy is returned in the end. Formally, we have \( pin (f)_n(x,y) = (x, f_{(n,x)}(y))\); in our example, \(f_{(n,x)}\) corresponds to addition by \( x \), ignoring a trivial \( n \) representing variables captured in the \( pin \) form.

2.3 Partially Invertible Composition

We now return to the example of the introduction, \( autokey \). It can be defined in Kalpis as follows:

figure e

The structure is very similar to the unidirectional version in Section 1, but uses the invertible branching and pinning constructs explained previously. We assume primitives \( chrToInt : \textsf{Char} \rightarrow \textsf{Int}\) and \( shift : \textsf{Int} \rightarrow \textsf{Char} \leftrightarrow \textsf{Char}\) for computing and performing the cyclical shifts, respectively. We omit the \(\textbf{with}\)-conditions of the invertible match by convention, as the syntactically distinct branch bodies can act as patterns to guide backward branching.

This example features our third core construct, partially invertible composition. This simply refers to the fact that we can modify the parameter of a bijection unidirectionally, as in \( shift \;( chrToInt \;k)\diamond h\). In this case, the (irreversible) function \( chrToInt \) is applied to \( k \) inside the (invertible) call to \( shift \). In other words, the parameter part of an invertible computation is allowed to depend freely on unidirectional computations, greatly enhancing the flexibility when programming. The reason we call it composition is because from the perspective of parameterized bijections, this corresponds to the composition of a parameterized bijection \( f \) with an (arbitrary) function \( g \) on the parameter part, i.e., \((f \circ g)_n(x) = f_{g(n)}(x)\). In our example, we have \( f \) corresponding to \( shift \) and \( g \) corresponding to \( chrToInt \).

The example also further highlights the utility of \( pin \). As noted in the introduction, \( autokey \) is tricky to express since each character in the invertible output depends unidirectionally on the preceding character in the corresponding input. Similar patterns also appear in more advanced examples; for instance, consider an adaptive compression method where each character in the input must be treated invertibly, and yet also be used as part of the (unidirectional) compression table. \( pin \) enables this sort of dependency in a safe way, letting us use \( h \) in the recursive call to \( autokey \) and returning a copy to use in the output.

Again, Kalpis lets us execute \( autokey \) in either direction, and guarantees that the two are inverses.

$$\begin{aligned} \quad & \begin{array}{l} \texttt {>}~ autokey \;\texttt {'F'} \diamond \texttt {"HELLO"}\\ {}\texttt {"CXHAD"} \end{array}&\quad & \begin{array}{l} \texttt {>}~ ( autokey \;\texttt {'F'})^\dagger \diamond \texttt {"CXHAD"}\\ {}\texttt {"HELLO"} \end{array}& \end{aligned}$$

2.4 Abstraction and Application of Invertible Computations

Our final core construct of partially invertible programming is the ability to abstract and apply invertible computations. Although the examples we have seen so far have defined (partially) invertible computations using the \(\textbf{def} ^\bullet \) keyword in a style close to traditional invertible languages, Kalpis actually features bijections as first-class values and supports proper higher-order programming. Bijections can be constructed with an invertible \(\lambda \)-form \(\lambda ^\bullet x. e\) analogous to that typical for ordinary functions, and the form \(\textbf{def} ^\bullet ~f\;x_1\;x_2~\ldots ~x_n = e\) is simply syntactic sugar for \(f = \lambda x_1.\lambda x_2 \ldots \lambda ^\bullet x_n.~e\). To our knowledge, only Sparcl [39, 40] shares this feature, with most invertible languages being limited to first-order computation.

For example, we are able to define multiple variants of the typical \( map \) function for lists in Kalpis.

$$\begin{aligned} \quad &\begin{array}{l} \begin{array}{ll} \textbf{sig} &{} map : (a \rightarrow b) \rightarrow [a] \rightarrow [b]\\ \textbf{def} &{} map \;f \;l = \end{array}\\ \quad \begin{array}{l} \textbf{case} ~xs~\textbf{of} \\ \quad \begin{array}{lcl} [\,]&{}\rightarrow &{} [\,]\\ h : t &{}\rightarrow &{} f \;h : map \;f \;t \end{array} \end{array} \end{array}&~& \begin{array}{l} \begin{array}{ll} \textbf{sig} &{} mapBij : (a \leftrightarrow b) \rightarrow [a] \leftrightarrow [b]\\ \textbf{def} ^\bullet &{} mapBij \;f \;l = \end{array}\\ \quad \begin{array}{l} \textbf{case} ^\bullet ~xs~\textbf{of} \\ \quad \begin{array}{lcl} [\,]&{}\rightarrow &{} [\,]\\ h : t &{}\rightarrow &{} (f \diamond h) : ( mapBij \;f \diamond t) \end{array} \end{array} \end{array}& \end{aligned}$$

Here, \( map \) is defined as usual, and maps a function over each element of a list, while \( mapBij \) makes use of the language’s invertible constructs, taking a bijection argument to produce a bijection on lists. For example, using \( mapBij \), the Caesar cipher (which shifts each character in the input a fixed number of steps) can be defined with a one-liner, as below to the left.

$$\begin{aligned} \quad & \begin{array}{ll} \textbf{sig} &{} caesar : \textsf{Char} \rightarrow [\textsf{Char}] \leftrightarrow [\textsf{Char}]\\ \textbf{def} &{} caesar \;k = mapBij \;( shift \;k) \end{array} &~& \begin{array}{ll} \textbf{sig} &{} vig : [\textsf{Char}] \rightarrow [\textsf{Char}] \leftrightarrow [\textsf{Char}]\\ \textbf{def} &{} vig \; ks = apBij \;( map \; shift \; ks ) \end{array}& \end{aligned}$$

The function on the right, \( vig \) (from Vigenère), takes a list of keys, shifting each character in the input using the corresponding key—the definition relies on \( apBij : [a \leftrightarrow b] \rightarrow [a] \leftrightarrow [b]\) to apply a list of bijections pointwise to a list of inputs (assuming the two have equal lengths). The latter example demonstrates that bijections can even occur inside data structures such as lists.

Some restrictions must be observed when dealing with higher-order computation in Kalpis. The language distinguishes between unidirectional and invertible terms, and carefully controls the interaction between the two. The restrictions mean that the invertible fragment of the language is essentially first-order; a formal account is given in Section 3.

Viewed from the perspective of parameterized bijections, abstraction corresponds to forming the function \(n \mapsto f_n\), witnessing that each choice of parameter \(n\) induces a bijection \(f_n\) which can be treated as a standalone value. On the other hand, application of a bijection \(\alpha \) corresponds to forming the parameterized bijection \( app _\alpha (x) = \alpha (x)\), where the parameter determining the bijection is \(\alpha \) itself.

This concludes Section 2; for more programming examples in Kalpis, we refer to the prototype implementation,Footnote 5 which contains a number of nontrivial programs, including implementations of Huffman coding and sliding-window compression.

3 The Kalpis Core System

In this section, we formally define the Kalpis core system and state the essential metatheoretic properties. A salient feature of the system is the clear separation between unidirectional and invertible terms: we have two main syntactic categories, two typing relations, and three evaluation relations (one for unidirectional terms, and one in each direction for invertible terms). The unidirectional terms are a conservative extension of a standard simply-typed call-by-value \(\lambda \)-calculus, and the invertible terms add support for (partially) invertible computation.

After introducing the syntax and reviewing some examples, Sections 3.4 and 3.5 give a formal semantics which suggests an interpretation of Kalpis terms as parameterized bijections. This view is made precise in Sections 4 and 5, which define a translation from Kalpis into the arrow language \(\textsc {rrArr}\), enabling a locally invertible interpretation.

3.1 Syntax

The syntax of Kalpis core is given below, where \( u \) denotes unidirectional terms, \( r \) denotes invertible terms, and \( p \) denotes patterns. The vector notation \(\overline{t}\) denotes an ordered sequence of elements \(t_i\), whose length we will refer to by \(|\overline{t}|\).

$$ \begin{array}{rcl} u&{}:\,\!:=&{} x \mid \lambda x. u \mid u_1 \;u_2 \mid \lambda ^\bullet x. r \mid u_1 \diamond u_2 \mid \textsf{C} \;\overline{u} \mid \textbf{case} ~u_0~\textbf{of} ~\{\overline{p \rightarrow u}\} \\ r&{}:\,\!:=&{} x \mid u \diamond r \mid u^\dagger \diamond r \mid pin \;u \diamond r \\ &{}\mid &{} \textsf{C} \;\overline{r} \mid \textbf{case} ~u~\textbf{of} ~\{\overline{p \rightarrow r}\} \mid \textbf{case} ^\bullet ~r_0~\textbf{of} ~\{\overline{p \rightarrow r~\textbf{with} ~u }\}\\ p&{}:\,\!:=&{} \textsf{C} \;\overline{x} \\ \end{array} $$

The syntax of unidirectional terms include the standard cases for variables, abstraction and application, along with data constructors and pattern matching. In addition, there is the invertible abstraction \(\lambda ^\bullet x. r\) and application \(u_1 \diamond u_2\) explained in the previous section. Note that while the body \(r\) is an invertible term, the abstraction itself is unidirectional.

The syntax of invertible terms resembles a first-order functional language, but with a couple of key additions. We have bijection application \(u \diamond r\), where the bijection is unidirectional whereas the argument is invertible. We also have fully applied versions of the \((\cdot )^\dagger \) and \( pin \) operators explained in the previous section (this is without loss of generality, as e.g., the higher-order version of \( pin \) can be recovered as \(\lambda f.\lambda ^\bullet x.~ pin \;f \diamond x\)). Partially invertible branching is represented by the \(\textbf{case} \) form, whose scrutinee \( u \) is unidirectional. The \(\textbf{case} ^\bullet \) form deconstructs an invertible term, and has a \(\textbf{with}\)-condition for invertible branching, following Janus [35, 53] and \(\varPsi \)-Lisp [7]. The core constructs of the previous section are all featured explicitly in the syntax, except for partially invertible composition, which is implicitly performed whenever a unidirectional term \( u \) occurs in an invertible context.

3.2 Types

Next, we define the types of Kalpis core.

$$ \begin{array}{rrl} A,B&:\,\!:=& \textsf{T} \;\overline{B} \mid A \rightarrow B \mid A \leftrightarrow B \mid X \end{array} $$

The types include constructors \(\textsf{T} \;\overline{B}\), functions \(A \rightarrow B\), bijections \(A \leftrightarrow B\) and type variables X. The types are conventional with the exception of invertible computations \(A \leftrightarrow B\); this simplicity is a design feature of Kalpis. With each type constructor \(\textsf{T}\) we associate an arity k and a set of constructors \(\textsf{C}\) with signatures \( \textsf{C} : A_1 \rightarrow A_2 \rightarrow \cdots \rightarrow A_n \rightarrow \textsf{T} \;\overline{B}, \) where \(|\overline{B}| = k\). We will assume the type constructors include at least the unit \(\textsf{1}\), products \(\otimes \), and sums \(\oplus \) with constructors

$$ \mathsf {()} : \textsf{1} \qquad (-,-) : A \rightarrow B \rightarrow A \otimes B \qquad \textsf{InL} : A \rightarrow A \oplus B \qquad \textsf{InR} : B \rightarrow A \oplus B $$

for any AB. We use \(\textsf{Bool}\) as a shorthand for \(\textsf{1} \oplus \textsf{1}\), and \(\textsf{True}\), \(\textsf{False}\) as shorthands for \(\textsf{InL}\;\mathsf {()}, \textsf{InR}\;\mathsf {()}\), respectively.

Types can be (mutually) recursive via constructors; for example, the type \(\textsf{Nat}\) has constructors \(\textsf{Z} : \textsf{Nat}\) and \(\textsf{S} : \textsf{Nat} \rightarrow \textsf{Nat}.\) In general, for any fixed A, the recursive type \(\mu X.A\) can be represented with a nullary type constructor \(\textsf{Rec}_A\), with constructor

$$\begin{aligned} \textsf{Roll} : A[\textsf{Rec}_A/X] \rightarrow \textsf{Rec}_A. \end{aligned}$$

For instance, \(\textsf{Rec}_{\textsf{1} \oplus X}\) has constructor \(\textsf{Roll} : \textsf{1} \oplus \textsf{Rec}_{\textsf{1} \oplus X} \rightarrow \textsf{Rec}_{\textsf{1} \oplus X}\), making it isomorphic to \(\textsf{Nat}\). Technically, we consider a variable X implicitly bound in the annotation to \(\textsf{Rec}\), and assume all other types are closed.

3.3 Correspondence to the Surface Language

The correspondence between the core syntax and the examples of Section 2 should be clear. For instance, the examples of addition and Fibonacci number calculation can be written as follows:

figure f

Here, \( add \) is a unidirectional term defined using a fixpoint operator \( fix \), and the structure is similar to the version presented in Section 2.1. The function \( fib \) is similarly defined, but uses the fixpoint operator \( fixBij \) instead of \( fix \), which works for bijections instead of functions. We omit the definition of \( is11 : \textsf{Nat} \otimes \textsf{Nat} \rightarrow \textsf{Bool}\) in the interest of space. The term \( fixBij \) (and analogously \( fix \)) is defined as below, making use of the language’s recursive types.

$$\begin{array}{l} fixBij \triangleq \lambda f.~ (\lambda g.~ g \;(\textsf{Roll} \;g)) \;(\lambda x. \lambda ^\bullet a.~ f \;((\textbf{case} ~x~\textbf{of} ~\textsf{Roll}\;y \rightarrow y) \;x) \diamond a) \end{array} $$

The type system we define in the next section will assign these terms the following types as expected.

$$ \begin{array}{llll} add &{}: \textsf{Nat} \rightarrow \textsf{Nat} \leftrightarrow \textsf{Nat} &{} fix &{}: ((A \rightarrow B) \rightarrow A \rightarrow B) \rightarrow A \rightarrow B \\ fib &{}: \textsf{Nat} \leftrightarrow \textsf{Nat} \otimes \textsf{Nat} &{} fixBij &{}: ((A \leftrightarrow B) \rightarrow A \leftrightarrow B) \rightarrow A \leftrightarrow B \end{array} $$

3.4 Type System

Figure 1 shows the typing rules for unidirectional (\(\varGamma \vdash u : A\)) and invertible (\(\varGamma ;\varTheta \vdash r : A\)) terms. The latter relation uses two contexts \(\varGamma \) and \(\varTheta \); intuitively, \(\varGamma \) contains variables for unidirectional data, which may be discarded or duplicated freely, whereas \(\varTheta \) contains variables for data that must be treated in an invertible way. This use of a dual context system [13] is inspired by previous work such as \(\textsf{CoreFun}\) [25] and Sparcl [39, 40]. Formally, we define the typing contexts as \( \varGamma , \varTheta :\,\!:= \varepsilon \mid \varGamma , x : A, \) and assume names \(x\) are unique within a context. We let \(\varGamma _1 , \varGamma _2\) denote the concatenation of two contexts.

The rules for \(\varGamma \vdash u : A\) are mostly straightforward. \(\textsc {T}\hbox {-}\textsc {Abs}^\bullet \) pushes the parameter x of \(\lambda ^\bullet x.r\) into \(\varTheta \) instead of \(\varGamma \) to ensure that the variable is used in an invertible way in r, and \(\textsc {T}\hbox {-}\textsc {Run}\) gives a rule for bijection application analogous to \(\textsc {T}\hbox {-}\textsc {App}\). In the Case rules, we implicitly require that patterns are disjoint and exhaustive.

In the rules for \(\varGamma ; \varTheta \vdash r : A\), the variables in the \(\varTheta \) environments must be used exactly once to ensure invertibility. Hence, we need to separate \(\varTheta \) into, e.g., \(\varTheta = \varTheta _1 \uplus \varTheta _2\) for typing subterms, where \(\uplus \) is used analogously to a linear type system (see, e.g., [9]). The rules follow the intuition that r denotes a bijection between \(\varTheta \) and A parameterized by \(\varGamma \). This highlights the difference between the pattern matching rules, T-UCase and T-RCase: the bound variables \(\varGamma _i\) in the former are parameters for the bijection that \(r_i\) defines, while in the latter, the variables \(\varTheta _i\) are part of the inputs of \(r_i\), so that \(\textbf{case} ^\bullet \) performs a composition of two invertible computations.

As stated in Section 2.4, there are some restrictions on how unidirectional and invertible terms can interact. Note that the unidirectional subterms occurring in the invertible typing rules are only typed using \(\varGamma \), and not \(\varTheta \). For instance, since the left-hand side in rule T-RApp is unidirectional, it cannot depend directly on invertible variables, ruling out terms like \(\lambda ^\bullet x. (x \diamond \textsf{True})\). This is a natural restriction, as we cannot generally deduce which function was used to produce some given result. Conversely, there is no rule for directly accessing \(\varGamma \) from the invertible typing relation; instead, unidirectional data can only affect the computation through rules like T-UCase and T-RApp. Both \(\lambda \)-forms are unidirectional, meaning they can neither capture invertible variables nor be returned from an invertible computation. In this sense, the invertible fragment of the language is first-order.

We note that there are no particular restrictions on unidirectional terms, and the approach presented could be used to augment any standard functional language with invertible computations \(\lambda ^\bullet x. r\) and \(u_1 \diamond u_2\). The prototype implementation further adds \(\textbf{let}\)-polymorphism as an orthogonal extension.

Fig. 1.
figure 1

The type system of Kalpis core: \(\smash {\overline{A}} \rightarrow B\) means \(A_1 \rightarrow \cdots \rightarrow A_{|\overline{A}|} \rightarrow B\).

3.5 Operational Semantics

We first define the set of values as below.

$$ \begin{array}{rrl} v&:\,\!:=& \textsf{C} \;\overline{v} \mid \langle \lambda x. u, \gamma \rangle \mid \langle \lambda ^\bullet x. r, \gamma \rangle \end{array} $$

Here, \(\gamma \) is a value environment, i.e., a mapping from variables to their values. Formally, we define \(\gamma , \theta :\,\!:= \emptyset \mid \gamma , x \mapsto v\), with \(\gamma \) and \(\theta \) corresponding to \(\varGamma \) and \(\varTheta \). We use the disjoint union \(\theta _1 \uplus \theta _2\) to concatenate two environments \(\theta _1\) and \(\theta _2\), which is defined only when \(\textsf{dom}(\theta _1)\) and \(\textsf{dom}(\theta _2)\) are disjoint. The values include constructors and two closure forms \(\langle \lambda x. u, \gamma \rangle \) and \(\langle \lambda ^\bullet x. r, \gamma \rangle \), corresponding to unidirectional and invertible computations. We type the values in analogy with the terms, with the rules for closures as follows:

figure g

Here, we write \(\gamma : \varGamma \) to mean that \(\textsf{dom}(\gamma ) = \textsf{dom}(\varGamma )\) and \(\gamma (x) : \varGamma (x)\) for all \(x \in \textsf{dom}(\varGamma )\). For p a pattern, we write \(p \gamma \) to denote the value obtained by applying the substitution \(\gamma \) to p’s variables. In addition, we use the shorthand \(\widehat{i = j} \triangleq \left\{ \begin{array}{ll} \textsf{True}\quad &{}\text {if}~i = j\\ \textsf{False}&{} \text {otherwise} \end{array}\right. \).

We now present in Figure 2 the operational semantics of Kalpis core, which consists of three evaluation relations: unidirectional, forward, and backward. The unidirectional evaluation relation \(\gamma \vdash u \Downarrow v\) reads that under \(\gamma \) term u evaluates to value v, as usual. In contrast, the forward and backward evaluation relations define a bijection. The former relation \(\gamma ; \theta \vdash r \Rightarrow v\) reads that under \(\gamma \) the forward evaluation of r maps \(\theta \) to v, and the latter relation \(\gamma ; v \vdash r \Leftarrow \theta \) reads that under \(\gamma \) the backward evaluation of r maps v to \(\theta \). As one can see, \(\gamma \) serves as parameter for this bijection that defines a one-to-one correspondence between \(\theta \) and v. Due to the space limitations, we omitted the rules for backward evaluation, as they are completely symmetric to forward evaluation. That is, for each rule of the forward evaluation, the corresponding backward rule is obtained by swapping each occurrence \(\gamma ; \theta \vdash r \Rightarrow v\) with \(\gamma ; v \vdash r \Leftarrow \theta \), and vice versa. Crucially, the evaluation relations are mutually dependent, and when a unidirectional term is embedded in an invertible computation, the unidirectional evaluation will be invoked to evaluate the term in the same way regardless of whether executing forwards or backwards.

We encourage the reader to study the rules for partially invertible \(\textbf{case}\) and invertible \(\textbf{case} ^\bullet \) especially. The former branches based on a unidirectional term, which is evaluated first regardless of the direction of execution. The latter branches based on an invertible term, which is evaluated first in the forward direction but last in the backward direction. In the backward direction, the \(\textbf{with}\)-conditions \(\overline{u}\) are instead evaluated first; the condition \(\widehat{i=j}\) for \(j \le i\) encodes the branch selection and the runtime check of postconditions mentioned previously.

There is a subtlety in the backward evaluation rule for constructors \(\textsf{C}\;\overline{r}\), where the same \(\textsf{C}\) occurs both in the term \(\textsf{C} \;\overline{r}\) and the input \(\textsf{C} \;\overline{v}\), meaning that evaluation fails if the value does not match the constructor \(\textsf{C}\). This corresponds to, e.g., the term \((\lambda ^\bullet x.~\textsf{S} \;x)^\dagger \diamond \textsf{Z}\) failing as it tries to subtract one from zero.

Fig. 2.
figure 2

The operational semantics of Kalpis core. Rules for the backward evaluation are omitted in the interest of space, but can be derived as explained in the text.

3.6 Metatheory

In this section, we briefly state the essential properties of the core system. The propositions in this section have been formalized mechanically, by implementing and reasoning about a definitional interpreter [46] in Agda. The implementation follows the presentation of the paper closely, but uses intrinsically-typed terms and nameless variables, and relies on the sized delay monad [1, 11].

Theorem 1 (Subject reduction)

[Subject reduction]

  • If \(\varGamma \vdash u : A\), \(\gamma : \varGamma \) and \(\gamma \vdash u \Downarrow v\), then v : A.

  • If \(\varGamma ; \varTheta \vdash r : A\), \(\gamma : \varGamma \), \(\theta : \varTheta \) and \(\gamma ; \theta \vdash r \Rightarrow v\), then v : A.

  • If \(\varGamma ; \varTheta \vdash r : A\), \(\gamma : \varGamma \), v : A and \(\gamma ; v \vdash r \Leftarrow \theta \), then \(\theta : \varTheta \).

Proof

Directly from the existence and type of the definitional interpreter in Agda.

Theorem 2 (Invertibility)

[Invertibility] If \(\varGamma ; \varTheta \vdash r : A\), \(\gamma : \varGamma \), \(\theta : \varTheta \) and v : A, then

$$ \gamma ; \theta \vdash r \Rightarrow v \quad \text {if and only if}\quad \gamma ; v \vdash r \Leftarrow \theta \text {.} $$

Proof

By simultaneous induction on the term r and the step count of evaluation; simple induction on the term r is not enough as the language has general recursion. The proof is otherwise straightforward, since the evaluation relations are completely symmetric.

Remark on Progress. We have chosen to give the semantics in a big-step style in this paper. This choice was made both because the invertibility property is more natural to state about a big-step semantics, which relates input to output directly, and to make the step to a denotational semantics smaller—as mentioned, the evaluation relations suggest an interpretation of invertible terms as parameterized bijections.

Thus, the progress property typically proven for a small-step semantics, meaning that evaluation never gets “stuck” given a valid input (see, e.g., [45]), is not direct to state in our case. However, we get a similar guarantee from the implementation in Agda, whose type checker asserts that no uncontrolled run-time errors are possible. Indeed, the only errors that can occur during evaluation are those caused by imprecise \(\textbf{with}\)-conditions or mismatched constructors.

4 Arrows for Partial and Local Invertibility

While the core system of Kalpis presented in the previous section is simple and illuminating, it only offers an operational understanding of the language. Furthermore, it depends on a unidirectional evaluation, which does not fit in a locally invertible setting. We want to get at the essence of partially invertible programming, and show that partial and local invertibility can be reconciled, which is the focus of this section.

In what follows, we define \(\textsc {rrArr}\), a low-level language based on arrow combinators, intended to capture the essence of partially invertible computation. The operations of \(\textsc {rrArr}\) directly correspond to the core constructs of Section 2, and have an immediate interpretation in terms of abstract functions and parameterized bijections. What is more, we show that they have an alternative, compositional and locally invertible interpretation using an idea similar to the reader monad in unidirectional computation (based on the irreversibility effect [26] and the reversible reader [23]). This property is not obvious for Kalpis, not to mention earlier work such as Sparcl [39, 40].

We begin by explaining the syntax and semantics of a first-order fragment of \(\textsc {rrArr}\), before proceeding to give its locally invertible interpretation. We then extend this fragment to match the full expressiveness of Kalpis in Section 4.5 with operations for higher-order computation. In Section 5, we top it all off by giving a formal translation from Kalpis core to \(\textsc {rrArr}\).

Fig. 3.
figure 3

The syntax and types of \(\textsc {rrArr}\): A and B denote base types, \(\tau \) denotes combinator types, c denotes bijections, \(\mu \) denotes unidirectional arrow combinators and \(\alpha \) denotes invertible arrow combinators.

4.1 Syntax and Type System of \(\textsc {rrArr}\)

Figure 3 shows the syntax and type system of \(\textsc {rrArr}\) (where base bijections c of type \(A \leftrightharpoons B\) are kept abstract). The language involves unidirectional (\(\mu \)) and invertible (\(\alpha \)) terms, similarly to Kalpis. Both kinds of terms form arrows over bijections, through the combinators \( arr \), \(\ggg \), and \( first \).

The former arrow, denoted by \(\mu : A \rightsquigarrow B\), intuitively represents an ordinary function; \( arr _\textrm{u} \;c\) extracts the forward semantics of a bijection c, \(\mu _1 \ggg _\textrm{u} \mu _2\) composes two functions \(\mu _1\) and \(\mu _2\), and \( first _\textrm{u} \;\mu \) simply applies \(\mu \) to the first component of the input. The unidirectional arrows also feature \( left _\textrm{u}\), the sum counterpart of \( first \), and allow copying data through \( clone \).

The latter arrow, denoted by , represents bijections between A and B parameterized by C; \( arr _\textrm{r} \;c\) constructs a parameterized bijection that behaves as the bijection c ignoring any parameter, \(\alpha _1 \ggg _\textrm{r} \alpha _2\) composes the two bijections obtained by passing the parameter to both \(\alpha _1\) and \(\alpha _2\), and \( first _\textrm{r} \;\alpha \) applies the bijection determined by \(\alpha \) to the first component of the input. These arrows also support \( left _\textrm{r}\), and form an inverse arrow [23] through a dagger operator \(\alpha ^\dagger \), that undoes \(\alpha \) and its effect.

What is special in \(\textsc {rrArr}\) is the communication between the two arrows through \( case! \), \( pin \), \(\mathbin {\gg !}\), and \( run \), where the former three directly correspond to the core constructs of Section 2. The term \( case! ~\alpha _1~\alpha _2\) performs partially invertible branching, running \(\alpha _1\) or \(\alpha _2\) depending on the value of its parameter. The term \( pin ~\alpha \) corresponds to the pinning construct; in \(\textsc {rrArr}\), this operation moves part of the input (D) into the parameter (\(C \otimes D\)) of \(\alpha \). The term \(\mu \mathbin {\gg !}\alpha \) represents partially invertible composition of the function \(\mu \) with the parameterized bijection \(\alpha \). Finally, the operator \( run \) allows converting a parameterized bijection to a function \(C\otimes A \rightsquigarrow B\) by extracting its forward semantics. This can be seen as a special case of applying invertible computations (in a unidirectional context); the treatment of abstraction and application supporting higher-order computation is left for Section 4.5, as it requires a slight extension.

It is worth noting that invertible arrows are inherently allowed to ignore their parameter (through \( arr _\textrm{r}\)), a fact that can be used to derive the crucial erasure operation in unidirectional arrows. In particular, supposing \( id : A \leftrightharpoons A\), we get the term \( run ~( arr _\textrm{r}~ id ) : C \otimes \textsf{1} \rightsquigarrow \textsf{1}\), which ignores any input C to return \(\mathsf {()}\).

4.2 Semantics of \(\textsc {rrArr}\)

We now formalize the intuitive interpretation through the semantics presented in Figure 4. We define a base set of values containing unit, pairs, and tagged values, which we type in the conventional way. Recursively typed values \(\textbf{roll} ~w\) are only manipulated by the base invertible combinators c.

$$\begin{aligned} w :\,\!:= () \mid (w_1, w_2) \mid \textbf{inl} ~w \mid \textbf{inr} ~w \mid \textbf{roll} ~w \end{aligned}$$

The semantics of \(\textsc {rrArr}\) again takes the form of three relations: one for unidirectional arrows and two for invertible arrows. The first (\(\mu ~w_1 \mapsto w_2\)) reads that \(\mu \) maps \(w_1\) to \(w_2\), confirming the intuition that unidirectional arrows represent functions. The second (\(\alpha ~w ; w_1 \mapsto w_2\)) and third () read that given parameter w, \(\alpha \) maps \(w_1\) to \(w_2\) under the forward (resp. backward) evaluation, confirming the intuition that our invertible arrows correspond to parameterized bijections. The rules closely follow the informal descriptions presented in the previous section. We assume a base invertible semantics for combinators c of the form \(c~w_1\mapsto w_2\), invoked by the rules concerning \( arr \) for each arrow.

The semantics satisfies the desired properties of subject reduction and invertibility, although we refer to our mechanized formalization for the details.Footnote 6

Fig. 4.
figure 4

The semantics of \(\textsc {rrArr}\). As before, the backward evaluation rules are symmetrically obtained from the forward rules.

4.3 Locally Invertible Interpretation

Recall that our goal is to define a locally invertible interpretation, whereas the straightforward semantics of Section 4.2 depended on a unidirectional evaluation. In this section, we give an alternative interpretation of \(\textsc {rrArr}\), utilizing the reversible reader (\(\textsf{RReader}\)) [23] to interpret the invertible arrow combinators.

figure k

Here, \(\textsf{RReader} \;C \;A \;B\) consists of the bijections of type \(C \otimes A \leftrightharpoons C \otimes B\) that keep the C part unchanged. This arrow was originally introduced with the intention of modelling a bijection with some “static” input C [23]. Regarding \(\rightsquigarrow \), we use the irreversibility effect [26] that leverages the fact that every unidirectional computation can be simulated by a locally invertible computation yielding “garbage” [8], as:

$$ \llbracket A \rightsquigarrow B \rrbracket = \exists G.\, A \leftrightharpoons G \otimes B $$

Combining these two effects is a novel point of \(\textsc {rrArr}\); in particular, we contribute the core constructs of \( case! \), \(\mathbin {\gg !}\), \( pin \) and \( run \), which enable communication between the two. Locally invertible interpretations of the primitives in each system have been given in the existing results. Here, we extend the results with the operations novel to \(\textsc {rrArr}\), to show that the two systems together give a locally invertible model of partially invertible computations.

As our target invertible language, we use \(\varPi ^o\) [26], whose combinators c constitute a minimal set of (non-total) invertible operations. The combinators support sequential composition (), parallel composition (\(c_1 \otimes c_2\) and \(c_1 \oplus c_2\)), and importantly, a local inversion operator (\(c^\dagger \)) such that . Figure 5 shows a summary of the primitives; their behavior should be obvious from the types (see the Agda formalization for details).

Fig. 5.
figure 5

The invertible primitives of \(\varPi ^o\) [26]. Note that we replace the looping construct \( trace \) with the derived \( inl \) for simplicity (Section 4.5 recovers the expressiveness of this combinator).

We now proceed to give another interpretation of the core constructs of \(\textsc {rrArr}\).

  • Partially invertible branching. Given \(\alpha _1\) and \(\alpha _2\) with \(\llbracket \alpha _1 \rrbracket : C \otimes A \leftrightharpoons C \otimes B\) and \(\llbracket \alpha _2 \rrbracket : D \otimes A \leftrightharpoons D \otimes B\), we must construct

    $$ \llbracket case! ~\alpha _1~\alpha _2 \rrbracket : (C \oplus D) \otimes A \leftrightharpoons (C \oplus D) \otimes B. $$

    Using \( distr \), we can convert \((C \oplus D) \otimes A\) to \(C \otimes A \oplus D \otimes A\), after which \(\llbracket \alpha _1 \rrbracket \) and \(\llbracket \alpha _2 \rrbracket \) can be run in parallel. Factoring out the B, we get the required transformation.

    figure n
  • Pinning. Given \(\alpha \) with \(\llbracket \alpha \rrbracket : (C \otimes D) \otimes A \leftrightharpoons (C \otimes D) \otimes B\) , we must produce

    $$ \llbracket pin \;\alpha \rrbracket : C \otimes (D \otimes A) \leftrightharpoons C \otimes (D \otimes B). $$

    As the reversible reader arrow \(\llbracket \alpha \rrbracket \) already returns the context C unchanged, we only need to shuffle the inputs and outputs appropriately.

    figure o
  • Partially invertible composition. Given \(\mu \) and \(\alpha \) with \(\llbracket \mu \rrbracket : C \leftrightharpoons G \otimes D\) and \(\llbracket \alpha \rrbracket : D \otimes A \leftrightharpoons D \otimes B\), we must construct

    $$ \llbracket \mu \mathbin {\gg !}~ \alpha \rrbracket : C \otimes A \leftrightharpoons C \otimes B. $$

    The basic idea is to run \(\llbracket \mu \rrbracket \) to produce a D-typed value to run \(\llbracket \alpha \rrbracket \) on, however, this brings with it unwanted garbage. Fortunately, since \(\llbracket \alpha \rrbracket \) is a reversible reader arrow, it is guaranteed to preserve the D-component, meaning that after running it we have the same D and G-values available to us as before. These can be turned back into the original C value by running \(\llbracket \mu \rrbracket \) backwards, giving the transformation required.

    figure p

    Note that this is precisely the construction underlying the reversible updates [5] of imperative reversible languages, and that \(\llbracket \alpha \rrbracket \) preserving the context is crucial for the construction to succeed.

  • Running invertible computations. Given \(\alpha \) with \(\llbracket \alpha \rrbracket : C \otimes A \leftrightharpoons C \otimes B\), we must produce

    $$ \llbracket run ~\alpha \rrbracket : C \otimes A \leftrightharpoons G \otimes B, $$

    for some G. Clearly it suffices to take \(\llbracket \alpha \rrbracket \) with \(G = C\), and we are done.

4.4 Correctness

We now state the desired correctness properties of our locally invertible interpretation, which show that it is equivalent to the direct semantics of Figure 4 and that \(\llbracket \alpha \rrbracket \) is indeed a reversible reader arrow (i.e., it preserves the context C).

Theorem 3

(rrArr\(\dashrightarrow \varPi ^o\) Soundness).

  • \(\mu ~w_1 \mapsto w_2\) implies \(\llbracket \mu \rrbracket ~w_1 \mapsto (g, w_2)\) for some g.

  • \(\alpha ~w; w_1 \mapsto w_2\) implies \(\llbracket \alpha \rrbracket ~(w, w_1) \mapsto (w, w_2)\).    \(\square \)

Theorem 4

(rrArr\(\dashrightarrow \varPi ^o\) Completeness).

  • \(\llbracket \mu \rrbracket ~w_1 \mapsto (g, w_2)\) implies \(\mu ~w_1 \mapsto w_2\).

  • \(\llbracket \alpha \rrbracket ~(w, w_1) \mapsto (w', w_2)\) implies \(w = w'\) and \(\alpha ~w;w_1 \mapsto w_2\).    \(\square \)

The theorems do not refer to the backward evaluation directly, utilizing the invertibility of both \(\textsc {rrArr}\) and \(\varPi ^o\).

4.5 Higher-order Computation

The previous sections laid out the fundamental ideas for representing partial invertibility in a locally invertible setting. However, with \(\textsc {rrArr}\) being first-order, it is not sufficient to be able to interpret Kalpis in a simple way. In this section, we extend the language with four new combinators enabling proper higher-order computation, shown in Figure 6.

Fig. 6.
figure 6

Combinators for higher-order computation in \(\textsc {rrArr}\).

The combinators \( curry \) and \( app \) are the standard currying and evaluation maps, creating and applying functions \(A \rightarrow B\). Their invertible counterparts \( curry ^\bullet \) and \( app ^\bullet \) provide the final core construct from Section 2: abstraction and application of invertible computations. They operate over parameterized bijections, abstracting the parameter to get a bijection value \(A \leftrightarrow B\). The values are extended accordingly with two new closure forms \(\langle \mu , w \rangle : A \rightarrow B\) and \(\langle \alpha , w \rangle : A \leftrightarrow B\), where \(\mu : C \otimes A \rightsquigarrow B\), , and w : C, representing staged unidirectional and invertible computations, respectively.

Having higher-order computation in the invertible setting has been challenging [2, 12, 39, 40]. Borrowing the idea from [39, 40], we address the issue by leveraging the fact that the function and bijection values are only part of invertible computations as parameters of parameterized bijections; hence, we only need a limited form of higher-orderness. We extend \(\varPi ^o\) with two additional primitive operations:

figure r

The former takes a combinator with an auxiliary piece of “state” C, and abstracts it into a bijection given a value of C. The latter applies a bijection, and saves it to enable reversing the operation later. To represent the values of type \(A \leftrightarrow B\) in \(\varPi ^o\), we introduce a third form of closure \(\langle f, w \rangle \), where we have \(f : C \otimes A \leftrightharpoons C \otimes B\) and w : C. Then, the semantics of \( app_\leftrightharpoons \) and \( curry_\leftrightharpoons \) are as follows:

figure s

As before, the inverse semantics is symmetric; e.g., \(( curry_\leftrightharpoons ~f)^\dagger ~(w, clos ) \mapsto w \; \text {if} \; clos = \langle f, w \rangle \). The (non-total) invertibility of \( curry_\leftrightharpoons \) is trivial, as its inverse fails unless its input matches the corresponding output; it is essentially a unidirectional function embedded in the invertible world. Since observational equality of closure values is undecidable, the equality check must rely on some other, intensional (e.g., syntactic) equality. Practically, this means that the combinator can only be used to create a closure and then subsequently undo the very same closure. However, this does not pose an issue for the translation from \(\textsc {rrArr}\), where closures will only result from uses of \( curry \) and \( curry ^\bullet \), both of which are unidirectional arrows (\(\rightsquigarrow \)). These unidirectional arrows will only be executed backwards as part of partially invertible compositions (\(\mathbin {\gg !}\)), which ensures that the input is the same as the corresponding output.

Now, we can interpret \(\llbracket app \rrbracket = app_\leftrightharpoons \), \(\llbracket app ^\bullet \rrbracket = app_\leftrightharpoons \), and

figure t

The former construction curries \(\llbracket \mu \rrbracket : C \otimes A \leftrightharpoons G \otimes B\) given w : C by creating a one-shot closure \(\langle f , \textbf{inl} ~w \rangle \) which turns into \(\langle f , \textbf{inr} ~g \rangle \) for g : G when first applied, and fails on a second application.

The theorems of Section 4.4 extend without difficulty to the higher-order combinators, although the statement is somewhat more intricate due to the differing set of closure values between \(\textsc {rrArr}\) and \(\varPi ^o\). We refer to the mechanized formalization in Agda for details.

5 Interpreting Kalpis with Arrows

Theorem 1 (Section 3.6) suggests that a unidirectional term-in-context \(\varGamma \vdash u : A\) can be seen as a function from \(\varGamma \) to A, and that an invertible term-in-context \(\varGamma ; \varTheta \vdash r : A\) can be seen as a bijection between \(\varTheta \) and A parameterized by \(\varGamma \). Then, it is natural that they be related with the two arrows \(({-} \rightsquigarrow {-})\) and of \(\textsc {rrArr}\), respectively. In this section, we give a formal account of this relation by translating terms of Kalpis into \(\textsc {rrArr}\), giving by extension a compositional locally invertible interpretation of Kalpis.

We first define some operations on typing contexts. We define \(\varGamma ^\times \) as

$$ (x_1 : A_1 , \ldots , x_n : A_n)^\times = (((\textsf{1} \otimes A_1) \otimes A_2) \otimes \cdots ) \otimes A_n. $$

It is straightforward to define an operator \( lookup _x : \varGamma ^\times \rightsquigarrow A\) provided that \(\varGamma (x) = A\). We also use a combinator \( split _{\varTheta _1,\varTheta _2} : (\varTheta _1 \uplus \varTheta _2)^\times \leftrightharpoons \varTheta _1^\times \otimes \varTheta _2^\times \) for splitting the linear environments. Then, we give two type-directed transformations: \(\varGamma \vdash u : A \dashrightarrow \mu \) that transforms u to \(\mu \) of type \(\varGamma ^\times \rightsquigarrow A\), and \(\varGamma ; \varTheta \vdash r : A \dashrightarrow \alpha \) that transforms r to \(\alpha \) of type . For the purposes of the translation, we consider a fixed set of type constructors \(\textsf{T}~\overline{B} :\,\!:= \textsf{1} \mid A \otimes B \mid A \oplus B \mid \textsf{Rec}_A\), identifying \(\mu X. A\) with \(\textsf{Rec}_A\).

Without loss of generality, we drop unnecessary \(\textbf{with}\)-conditions, so that a \(\textbf{case} ^\bullet \)-expression with one branch needs no \(\textbf{with}\)-clause, and one with two branches needs only one clause. Due to the space limitations, we present only the most representative cases here, and point the interested reader to the mechanized formalization in Agda.Footnote 7

  • Case T-UCase (\(A \oplus B\)).

    figure w

    We can duplicate \(\varGamma ^\times \) using \( clone \) and use one copy to construct \(A \oplus B\) with \(\mu \). Using \( distl : A \otimes (B \oplus C) \leftrightharpoons A \otimes B \oplus A \otimes C\), which is easily derived, we distribute the second copy of \(\varGamma \) over the sum. Then, the required combinator can be constructed through a combination of partially invertible composition (\(\mathbin {\gg !}\)) and branching (\( case! \)), where we have .

  • Case T-RCase (\(A \oplus B\)).

    figure y

    The idea is similar to T-UCase, but we now operate in the invertible world, so we split \((\varTheta _1 \uplus \varTheta _2)^\times \) instead of duplicating \(\varGamma \), and compose using \(\ggg _\textrm{r}\) instead of \(\gg !\). The combinator \( case ~\alpha _1~\alpha _2~\alpha _3 \triangleq left _\textrm{r}~\alpha _1 \ggg _\textrm{r} right _\textrm{r}~\alpha _2 \ggg _\textrm{r} \alpha _3^\dagger \) with type

    figure z

    provides an invertible branching operator analogous to \( case! \), with a postcondition for merging the branches. We convert \(\mu : \varGamma ^\times \rightsquigarrow (C \rightarrow \textsf{Bool})\) to an arrow through the \( mkCond \) operator, which can be defined using \( pin \), \( case! \) and \( app \) in tandem.

  • Cases \(\textsc {T-Abs}^\bullet \), T-RApp.

    figure ab

    For \(\textsc {T-Abs}^\bullet \), we get , which we \( curry ^\bullet \) after handling the unit. For T-RApp, \(\alpha \) transforms \(\varTheta ^\times \) to A, letting \(\mu \) be applied through a partially invertible composition (\(\mathbin {\gg !}\)) with \( app ^\bullet \).

  • Case T-Pin.

    figure ad

    We have \(\alpha \) producing \(C \otimes A\), and with parameter \(\varGamma ^\times \otimes C\), we can apply \(\mu \) to produce B. Thus, we must shift C from the output into the parameter, and \( pin \) achieves just that.

Correctness. Finally, we show the correctness of the translation with respect to the semantics of Sections 3.5 and 4.2. Before we state correctness, we must first define a translation of the values, since they differ between Kalpis and \(\textsc {rrArr}\).

$$\begin{aligned}\begin{gathered} \llbracket () \rrbracket = (), \quad \llbracket (v_1, v_2) \rrbracket = (\llbracket v_1 \rrbracket , \llbracket v_2 \rrbracket ), \\ \llbracket \textsf{InL}\;v \rrbracket = \textbf{inl} ~\llbracket v \rrbracket , \quad \llbracket \textsf{InR}\;v \rrbracket = \textbf{inr} ~\llbracket v \rrbracket , \quad \llbracket \textsf{Roll}\;v \rrbracket = \textbf{roll} ~\llbracket v \rrbracket , \\ \llbracket \langle \lambda x. u , \gamma \rangle \rrbracket = \langle \llbracket u \rrbracket , \llbracket \gamma \rrbracket \rangle ,\quad \llbracket \langle \lambda ^\bullet x. r , \gamma \rangle \rrbracket = \langle arr ~ unitel^\dagger _{\times } \ggg _\textrm{r} \llbracket r \rrbracket , \llbracket \gamma \rrbracket \rangle \end{gathered}\end{aligned}$$

The base values are translated trivially, whereas the closures are translated according to the type-directed translation given above (cf. Case \(\textsc {T-Abs}^\bullet \)). We also define a translation of value environments \(\gamma \) in the obvious way.

Then, we can state the correctness of the translation as below.

Theorem 5

(Kalpis\(\dashrightarrow \textsc {rrArr} \) Soundness).

  • \(\varGamma \vdash u : A \dashrightarrow \mu \) and \(\gamma \vdash u \Downarrow v\) implies \(\mu ~\llbracket \gamma \rrbracket \mapsto \llbracket v \rrbracket \)

  • \(\varGamma ; \varTheta \vdash r : A \dashrightarrow \alpha \) and \(\gamma ; \theta \vdash r \Rightarrow v\) implies \(\alpha ~\llbracket \gamma \rrbracket ; \llbracket \theta \rrbracket \mapsto \llbracket v \rrbracket \).    \(\square \)

This theorem does not refer to the backward evaluation directly, utilizing the invertibility of both Kalpis and \(\textsc {rrArr}\). The completeness part, on the other hand, does need a separate statement for the backward direction, since there is no a priori guarantee that the output w is of the form \(\llbracket \theta \rrbracket \).

Theorem 6

(Kalpis\(\dashrightarrow \textsc {rrArr} \) Completeness).

  • \(\varGamma \vdash u : A \dashrightarrow \mu \) and \(\mu ~\llbracket \gamma \rrbracket \mapsto w\) implies \(\gamma \vdash u \Downarrow v\) for v with \(\llbracket v \rrbracket = w\).

  • \(\varGamma ; \varTheta \vdash r : A \dashrightarrow \alpha \) and \(\alpha ~\llbracket \gamma \rrbracket ;\llbracket \theta \rrbracket \mapsto w\) implies \(\gamma ; \theta \vdash r \Rightarrow v\) for v with \(\llbracket v \rrbracket = w\).

  • \(\varGamma ; \varTheta \vdash r : A \dashrightarrow \alpha \) and implies \(\gamma ; v \vdash r \Leftarrow \theta \) for \(\theta \) with \(\llbracket \theta \rrbracket = w\).    \(\square \)

We refer to the Agda code in the supplementary material for the proofs.

6 Related Work

Kalpis and \(\textsc {rrArr}\) are not the first to support partial invertibility. In the imperative setting, languages such as Janus [35, 53], Frank’s R [17], and R-While [19] support a limited form of partial invertibility via reversible update operators [6]. An example of a reversible update statement is \(x \mathbin {\texttt {+=}} e\), whose effect can be reverted by the corresponding inverse statement \(x \mathbin {\texttt {-=}} e\). Both statements use the same e, which need not be invertible (e.g., \(x \mathbin {\texttt {+=}} yz\) is reverted by \(x \mathbin {\texttt {-=}} yz\), and vice versa). In the functional setting, Theseus [27] allows a bijection to take additional parameters, but only provided that they are available at compile time. RFun version 2,Footnote 8 an extension to the original RFun [54], and CoreFun [25] allow more flexibility via so-called ancilla parameters, which are translated to auxiliary inputs and outputs of the invertible computation. Their approach is similar to Kalpis ’s but more restrictive since they lack support for the \( pin \) operator and higher-order computation. Jeopardy [31] is a recent invertible language where even irreversible functions can be inverted in certain contexts depending on implicitly available information. However, this is still work in progress, and seems to lean closer to program inversion methods than the lightweight type-based approach we employ.

Sparcl [39, 40] is the most flexible system that supports partial invertibility to our knowledge, which is realized through a more advanced language foundation. Instead of bijections \(A \leftrightarrow B\), Sparcl features invertible data marked by the type \(A^\bullet \), which implicitly corresponds to some bijection \(S \leftrightarrow A\). This idea of invertible data is inherited from the HOBiT language [38], which represents lens combinators [15, 16] as higher-order functions to achieve applicative-style higher-order bidirectional programming [36, 37]. The type system of Sparcl ensures that a closed linear function between invertible data \(!(A^\bullet \multimap B^\bullet )\) is isomorphic to a (non-total) bijection between A and B, so that partial invertibility can be represented as a function that takes both unidirectional and invertible data \(C \rightarrow A^\bullet \multimap B^\bullet \). This representation affords more flexibility than Kalpis does: invertible data is allowed to be captured in abstractions, and can even appear in subcomponents of datatypes (e.g., \(\textsf{Int} \otimes (\textsf{Int}^\bullet )\) or \(\textsf{Int} \oplus (\textsf{Int}^\bullet )\) are both valid types). However, this flexibility comes at the cost of complexity, requiring a semantics that interleaves partial evaluation and invertible computation, making a locally invertible interpretation difficult. We remark that the holed residuals \(\langle x. E \rangle \) featured in Sparcl’s core system bear a strong resemblance to bijections \(\lambda ^\bullet x. r\) in Kalpis.

Our combinator language \(\textsc {rrArr}\) can be seen as an extension of \(\textsf {ML}_{\varPi }\), an arrow metalanguage on top of the invertible language \(\varPi \) treating information creation and loss (non-totality and irreversibility) as an effect [26]. By combining their work with the reversible reader arrow [23], we are able to give erasing (weakening) as a derived operation defined via the operator \( run \) (as demonstrated in Section 4). Further research on the nontrivial interaction between the arrows, such as an equational characterization and a denotational model, is left for future work. While the previous work is able to treat non-totality as part of an effect, we assume some non-total operations in the underlying invertible system due to the inclusion of recursive and functional types.

The design of Kalpis is inspired by the arrow calculus of Lindley, Wadler, and Yallop [33], which is a metalanguage for the conventional representation of arrows [24], analogous to the monad metalanguage [42]. In a sense, Kalpis can be seen as a counterpart to the arrow calculus for \(\textsc {rrArr}\). For example, the treatment of \(\lambda ^\bullet x. r\) is actually inherited from the arrow calculus, where arrows cannot be nested in general [34], unless the underlying arrow supports application to form a monad [24]. To the best of our knowledge, a monad-based programming system for invertible/reversible computation does not exist, though there are some closely related results, including monads for nondeterministic computation (such as [14]) and a monadic programming framework for bidirectional transformations [20, 52]. However, these existing approaches lack the guarantee of bijectivity—a motivation to use invertible languages.

The importance of partial invertibility has been recognized in the neighboring literature on program inversion—program transformations that derive a program of \(f^{-1}\) for a given program of f. Partial inversion [44, 47] essentially applies a binding-time analysis [21, 28] to an input program, where the static data can be treated as unidirectional inputs. The technique is further extended to treat results of inverses as unidirectional [3, 29, 30]. This treatment is similar to the role of \( pin \) in Kalpis and Sparcl [39, 40] in that it converts invertible data into “static” parameters. Some approaches to program inversion are more liberal: semi inversion [41] essentially converts a program into a logic program, where there is no clear boundary between unidirectional and invertible data, and the PINS system [49], in addition to an original program, can take a control structure of an inverse program to effectively synthesize inverses that may not mirror the control structures of the original. The main limitation of program inversion is that as a program transformation it may fail, often for reasons that are not obvious to programmers.

7 Conclusion

We have presented a set of four core constructs for partially invertible programming, demonstrated their expressiveness through examples, and shown that they can be given a locally invertible interpretation, thus solving an open problem in the field. The four constructs are (1) partially invertible branching, (2) pinning invertible inputs, (3) partially invertible composition, and (4) abstraction and application of invertible computations. We designed the partially invertible language Kalpis on top of these constructs and formalized its syntax, type system and operational semantics. We then presented \(\textsc {rrArr}\), a low-level arrow language with primitives directly corresponding to the constructs, and gave it a locally invertible interpretation based on two effects—the irreversibility effect [26] and the reversible reader [23]. Finally, we presented a type-directed translation from Kalpis to \(\textsc {rrArr}\), showing how to support expressive partial invertibility on top of a locally invertible foundation. Proofs of all theorems stated in the paper are formalized by the accompanying Agda code.Footnote 9