Keywords

1 Introduction

Programming languages based on the -calculus have different semantics depending on the reduction strategy employed. Three common variants are call-by-value, call-by-name and call-by-need (with the third sometimes also referred to as “lazy evaluation” when data constructors defer evaluation of arguments until the data structure is traversed). Reasoning about such programs and their equivalence under varying reduction strategies can be difficult as we have to reason about meta-level reduction strategies and not merely at the object level.

Levy [17] introduced call-by-push-value (CBPV) to improve the situation. CBPV is a calculus with separated notions of value and computation. A characteristic feature is that each CBPV program encodes its own evaluation order. It is best seen as an intermediate language into which lambda-calculus-based source-language programs can be translated. Moreover, CBPV is powerful enough that programs employing call-by-value or call-by-name (or even a mixture) can be simply translated into it, giving an object-calculus way to reason about the meta-level concept of reduction order.

However, CBPV does not enable us to reason about call-by-need evaluation. An intuitive reason is that call-by-need has “action at a distance” in that reduction of one subterm causes reduction of all other subterms that originated as copies during variable substitution. Indeed call-by-need is often framed using mutable stores (graph reduction [32], or reducing a thunk which is accessed by multiple pointers [16]). CBPV does not allow these to be encoded.

This work presents extended call-by-push-value (ECBPV), a calculus similar to CBPV, but which can capture call-by-need reduction in addition to call-by-value and call-by-name. Specifically, ECBPV adds an extra primitive which runs , with being evaluated the first time is used. On subsequent uses of , the result of the first run is returned immediately. The term is evaluated at most once. We give the syntax and type system of ECBPV, together with an equational theory that expresses when terms are considered equal.

A key justification for an intermediate language that can express several evaluation orders is that it enables equivalences between the evaluation orders to be proved. If there are no (side-)effects at all in the source language, then call-by-need, call-by-value and call-by-name should be semantically equivalent. If the only effect is nondeterminism, then need and value (but not name) are equivalent. If the only effect is nontermination then need and name (but not value) are equivalent. We show that ECBPV can be used to prove such equivalences by proving the latter using an argument based on Kripke logical relations of varying arity [12].

These equivalences rely on the language being restricted to particular effects. However, one may wish to switch evaluation order for subprograms restricted to particular effects, even if the language itself does not have such a restriction. To allow reasoning to be applied to these cases, we add an effect system [20] to ECBPV, which allows the side-effects of subprograms to be statically estimated. This allows us to determine which parts of a program are invariant under changes in evaluation order. As we will see, support for call-by-need (and action at a distance more generally) makes describing an effect system significantly more difficult than for call-by-value.

Contributions. We make the following contributions:

  • We describe extended call-by-push-value, a version of CBPV containing an extra construct that adds support for call-by-need. We give its syntax, type system, and equational theory (Sect. 2).

  • We describe two translations from a lambda-calculus source language into ECBPV: one for call-by-name and one for call-by-need (the first such translation) (Sect. 3). We then show that, if the source language has nontermination as the only effect, call-by-name and call-by-need are equivalent.

  • We refine the type system of ECBPV so that its types also carry effect information (Sect. 4). This allows equivalences between evaluation orders to be exploited, both at ECBPV and source level, when subprograms are statically limited to particular effects.

2 Extended Call-by-Push-Value

We describe an extension to call-by-push-value with support for call-by-need. The primary difference between ordinary CBPV and ECBPV is the addition of a primitive that allows computations to be added to the environment, so that they are evaluated only the first time they are used. Before describing this change, we take a closer look at CBPV and how it supports call-by-value and call-by-name.

CBPV stratifies terms into values, which do not have side-effects, and computations, which might. Evaluation order is irrelevant for values, so we are only concerned with how computations are sequenced. There is exactly one primitive that causes the evaluation of more than one computation, which is the computation . This means run the computation M, bind the result to x, and then run the computation N. (It is similar to in Haskell.) The evaluation order is fixed: M is always eagerly evaluated. This construct can be used to implement call-by-value: to apply a function, eagerly evaluate the argument and then evaluate the body of the function. No other constructs cause the evaluation of more than one computation.

To allow more control over evaluation order, CBPV allows computations to be thunked. The term is a value that contains the thunk of the computation . Thunks can be duplicated (to allow a single computation to be evaluated more than once), and can be converted back into computations with . This allows call-by-name to be implemented: arguments to functions are thunked computations. Arguments are used by forcing them, so that the computation is evaluated every time the argument is used. Effectively, there is a construct , which evaluates each time the variable is used by , rather than eagerly evaluating. (The variable is underlined here to indicate that it refers to a computation rather than a value: uses of it may have side-effects.)

To support call-by-need, extended call-by-push-value adds another construct . This term runs the computation , with the computation is used. On subsequent uses of , the result of the first run is returned immediately. The computation is evaluated at most once. This new construct adds the “action at a distance” missing from ordinary CBPV.

We briefly mention that adding general mutable references to call-by-push-value would allow call-by-need to be encoded. However, reasoning about evaluation order would be difficult, and so we do not take this option.

2.1 Syntax

The syntax of extended call-by-push-value is given in Fig. 1. The parts are new here. The rest of the syntax is similar to CBPV.Footnote 1

Fig. 1.
figure 1

Syntax of ECBPV

We assume two sets of variables: value variables and computation variables . While ordinary CBPV does not include computation variables, they do not of themselves add any expressive power to the calculus. The ability to use call-by-need in ECBPV comes from the construct used to bind the variable.Footnote 2

There are two kinds of terms, value terms VW which do not have side-effects (in particular, are strongly normalizing), and computation terms MN which might have side-effects. Value terms include constants , and specifically the constant . There are no constant computation terms; value constants suffice (see Sect. 3 for an example). The value term suspends the computation ; the computation term runs the suspended computation . Computation terms also include -ary tuples (where ranges over finite sets); the th projection of a tuple is . Functions send values to computations, and are computations themselves. Application is written , where is the argument and is the function to apply. The term is a computation that just returns the value , without causing any side-effects. Eager sequencing of computations is given by , which evaluates until it returns a value, then places the result in and evaluates . For example, in , the term is evaluated once, and the result is duplicated. In , the term is still evaluated once, but its result is never used. Syntactically, both and (explained below) are right-associative (so means ).

The primary new construct is . This term evaluates . The first time is evaluated (due to a use of inside ) it behaves the same as the computation . If returns a value , then subsequent uses of behave the same as . Hence only the first use of will evaluate . If is not used then is not evaluated at all. The computation variable bound inside the term is primarily used by eagerly sequencing it with other computations. For example,

uses twice: once where the result is bound to , and once where the result is bound to . Only the first of these uses will evaluate , so this term has the same semantics as . The term does not evaluate at all, and has the same semantics as .

With the addition of it is not in general possible to determine the order in which computations are executed statically. Uses of computation variables are given statically, but not all of these actually evaluate the corresponding computation dynamically. In general, the set of uses of computation variables that actually cause effects depends on run-time behaviour. This will be important when describing the effect system in Sect. 4.

The standard capture-avoiding substitution of value variables in value terms is denoted \({V}[{x} \mapsto {W}]\). We similarly have substitutions of value variables in computation terms, computation variables in value terms, and computation variables in computation terms. Finally, we define the call-by-name construct mentioned above as syntactic sugar for other CBPV primitives:

where is not free in .

Types are stratified into value types and computation types . Value types include the unit type, products and sum types. (It is easy to add further base types; we omit Levy’s empty types for simplicity.) Value types also include thunk types , which are introduced by and eliminated by . Computation types include -ary product types for finite I, function types , and returner types . The latter are introduced by , and are the only types of computation that can appear on the left of either or (which are the eliminators of returner types). The type constructors and form an adjunction in categorical models. Finally, contexts map value variables to value types, and computation variables to computation types of the form . This restriction is due to the fact that the only construct that binds computation variables is , which only sequences computations of returner type. Allowing computation variables to be associated with other forms of computation type in typing contexts is therefore unnecessary. Typing contexts are ordered lists.

The syntax is parameterized by a signature, containing the constants c.

Definition 1

(Signature). A signature consists of a set of constants of type A for each value type A. All signatures contain .

2.2 Type System

The type system of extended call-by-push-value is a minor extension of the type system of ordinary call-by-push-value. Assume a fixed signature . There are two typing judgements, one for value types and one for computation types. The rules for the value typing judgement and the computation typing judgement are given in Fig. 2. Rules that add a new variable to the typing context implicitly require that the variable does not already appear in the context. The type system admits the usual weakening and substitution properties for both value and computation variables.

Fig. 2.
figure 2

Typing rules for ECBPV

It should be clear that ECBPV is actually an extension of call-by-push-value. CBPV terms embed as terms that never use the highlighted forms. We translate call-by-need by encoding call-by-need functions as terms of the form

where \(x'\) is not free in M. This is a call-by-push-value function that accepts a thunk as an argument. The thunk is added to the context, and the body of the function is executed. The first time the argument is used (via ), the computation inside the thunk is evaluated. Subsequent uses do not run the computation again. A translation based on this idea from a call-by-need source language is given in detail in Sect. 3.2.

2.3 Equational Theory

In this section, we present the equational theory of extended call-by-push-value. This is an extension of the equational theory for CBPV given by Levy [17] to support our new constructs. It consists of two judgement forms, one for values and one for computations:

These mean both terms are well typed, and are considered equal by the equational theory. We frequently omit the context and type when they are obvious or unimportant.

The definition is given by the axioms in Fig. 3. Note that these axioms only hold when the terms they mention have suitable types, and when suitable constraints on free variables are satisfied. For example, the second sequencing axiom holds only if is not free in N. These conditions are left implicit in the figure. The judgements are additionally reflexive (assuming the typing holds), symmetric and transitive. They are also closed under all possible congruence rules. There are no restrictions on congruence related to evaluation order. None are necessary because ECBPV terms make the evaluation order explicit: all sequencing of computations uses and . Finally, note that enriching the signature with additional constants will in general require additional axioms capturing their behaviour; Sect. 3 exemplifies this for constants representing nontermination.

Fig. 3.
figure 3

Equational theory of ECBPV

For the equational theory to capture call-by-need, we might expect computation terms that are not of the form to never be duplicated, since they should not be evaluated more than once. There are two exceptions to this. Such terms can be duplicated in the axioms that duplicate value terms (such as the laws for sum types). In this case, the syntax ensures such terms are thunked. This is correct because we should allow these terms to be executed once in each separate execution of a computation (and separate executions arise from duplication of thunks). We are only concerned with duplication within a single computation. Computation terms can also be duplicated across multiple elements of a tuple of computation terms. This is also correct, because only one component of a tuple can be used within a single computation (without thunking), so the effects still will not happen twice. (There is a similar consideration for functions, which can only be applied once.) The remainder of the axioms never duplicate -bound terms that might have effects.

The majority of the axioms of the equational theory are standard. Only the axioms involving are new; these are highlighted. The first new sequencing axiom (in Fig. 3c) is the crucial one. It states that if a computation will next evaluate , where is a computation variable bound to M, then this is the same as evaluating M, and then using the result for subsequent uses of . In particular, this axiom (together with the law for ) implies that .

The second sequencing axiom does garbage collection [22]: if a computation bound by is not used (because the variable does not appear), then the binding can be dropped. This equation implies, for example, that

The next four sequencing axioms (two from CBPV and two new) state that binding a computation with or commutes with the remaining forms of computation terms. These allow and to be moved to the outside of other constructs except thunks. The final four axioms (one from CBPV and three new) capture associativity and commutativity involving and ; again these parallel the existing simple associativity axiom for .

Note that associativity between different evaluation orders is not necessarily valid. In particular, we do not have

(The first term might not evaluate \(M_1\), the second always does.) This is usually the case when evaluation orders are mixed [26].

These final two groups allow computation terms to be placed in normal forms where bindings of computations are on the outside. (Compare this with the translation of source-language answers given in Sect. 3.2.) Finally, the law for (in Fig. 3a) parallels the usual \(\beta \) law for : it gives the behaviour of computation terms that return values without having any effects.

The above equational theory induces a notion of contextual equivalence between ECBPV terms. Two terms are contextually equivalent when they have no observable differences in behaviour. When we discuss equivalences between evaluation orders in Sect. 3, is the notion of equivalence between terms that we consider.

Contextual equivalence is defined as follows. The ground types G are the value types that do not contain thunks:

A value-term context is a computation term with a single hole (written \(-\)), which occurs in a position where a value term is expected. We write for the computation term that results from replacing the hole with V. Similarly, computation-term contexts are computation terms with a single hole where a computation term is expected, and is the term in which the hole is replaced by M. Contextual equivalence says that the terms cannot be distinguished by closed computations that return ground types. (Recall that \(\diamond \) is the empty typing context.)

Definition 2

(Contextual equivalence). There are two judgement forms of contextual equivalence.

  1. 1.

    Between value terms: if , , and for all ground types G and value-term contexts such that and we have

  2. 2.

    Between computation terms: if , , and for all ground types G and computation-term contexts such that and we have

3 Call-by-Name and Call-by-Need

Extended call-by-push-value can be used to prove equivalences between evaluation orders. In this section we prove a classic example: if the only effect in the source language is nontermination, then call-by-name is equivalent to call-by-need. We do this in two stages.

First, we show that call-by-name is equivalent to call-by-need within ECBPV (Sect. 3.1). Specifically, we show that

(Recall that is syntactic sugar for .)

Second, an important corollary is that the meta-level reduction strategies are equivalent (Sect. 3.2). We show this by describing a lambda-calculus-based source language together with a call-by-name and a call-by-need operational semantics and giving sound (see Theorem 2) call-by-name and call-by-need translations into ECBPV. The former is based on the translation into the monadic metalanguage given by Moggi [25] (we expect Levy’s translation [17] to work equally well). The call-by-need translation is new here, and its existence shows that ECBPV does indeed subsume call-by-need. We then show that given any source-language expression, the two translations give contextually equivalent ECBPV terms.

To model non-termination being our sole source-language effect, we use the ECBPV signature which contains a constant for each value type A, representing a thunked diverging computation. It is likely that our proofs still work if we have general fixed-point operators as constants, but for simplicity we do not consider this here. The constants \(\bot _A\) enable us to define a diverging computation for each computation type :

We characterise nontermination by augmenting the equational theory of Sect. 2.3 with the axiom

(Omega)

for each context , value type A and computation type . In other words, diverging as part of a larger computation causes the entire computation to diverge. This is the only change to the equational theory we need to represent nontermination. In particular, we do not add additional axioms involving .

3.1 The Equivalence at the Object (Internal) Level

In this section, we show our primary result that

As is usually the case for proofs of contextual equivalence, we use logical relations to get a strong enough inductive hypothesis for the proof to go through. However, unlike the usual case, it does not suffice to relate closed terms. To see why, consider a closed term M of the form

If we relate only closed terms, then we do not learn anything about itself (since may be free in it). We could attempt to proceed by considering the closed term . For example, if this returns a value V then cannot have been evaluated and M should have the same behaviour as . However, we get stuck when proving the last step. This is only a problem because is a nonterminating computation: every terminating computation of returner type has the form (up to ), and when these are bound using we can eliminate the binding using the equation

The solution is to relate terms that may have free computation variables (we do not need to consider free value variables). The free computation variables should be thought of as referring to nonterminating computations (because we can remove the bindings of variables that refer to terminating computations). We relate open terms using Kripke logical relations of varying arity, which were introduced by Jung and Tiuryn [12] to study lambda definability.

We need a number of definitions first. A context \({\varGamma }'\) weakens another context \({\varGamma }\), written , whenever \({\varGamma }\) is a sublist of \({\varGamma }'\). For example, . We define as the set of equivalence classes (up to the equational theory ) of terms of value type A in context \({\varGamma }\), and similarly define for computation types:

Since weakening is admissible for both typing judgements, implies that and (note the contravariance).

A computation context, ranged over by \({\varDelta }\), is a typing context that maps variables to computation types (i.e. has the form ). Variables in computation contexts refer to nonterminating computations for the proof of contextual equivalence. A Kripke relation is a family of binary relations indexed by computation contexts that respects weakening of terms:

Definition 3

(Kripke relation). A Kripke relation R over a value type A (respectively a computation type ) is a family of relations (respectively ) indexed by computation contexts \({\varDelta }\) such that whenever we have .

Note that we consider binary relations on equivalence classes of terms because we want to relate pairs of terms up to (to prove contextual equivalence). The relations we define are partial equivalence relations (i.e. symmetric and transitive), though we do not explicitly use this fact.

We need the Kripke relations we define over computation terms to be closed under sequencing with nonterminating computations. (For the rest of this section, we omit the square brackets around equivalence classes.)

Definition 4

A Kripke relation R over a computation type is closed under sequencing if each of the following holds:

  1. 1.

    If and then .

  2. 2.

    The pair is in \(R^{\varDelta }\).

  3. 3.

    For all and , all four of the following pairs are in \(R^{\varDelta }\):

    $$\begin{aligned} (N \mathbin {\mathsf {need}}\underline{y}.\,M,~N \mathbin {\mathsf {need}}\underline{y}.\,M') \quad&\quad ({M}[{\underline{y}} \mapsto {N}],~{M'}[{\underline{y}} \mapsto {N}]) \\ ({M}[{\underline{y}} \mapsto {N}],~N \mathbin {\mathsf {need}}\underline{y}.\,M') \quad&\quad (N \mathbin {\mathsf {need}}\underline{y}.\,M,~{M'}[{\underline{y}} \mapsto {N}]) \end{aligned}$$

For the first case of the definition, recall that the computation variables in \({\varDelta }\) refer to nonterminating computations. Hence the behaviour of M and \(M'\) are irrelevant (they are never evaluated), and we do not need to assume they are related.Footnote 3 The second case implies (using axiom Omega) that

mirroring the first case. The third case is the most important. It is similar to the first (it is there to ensure that the relation is closed under the primitives used to combine computations). However, since we are showing that is contextually equivalent to substitution, we also want these to be related. We have to consider computation variables in the definition (as possible terms N) only because of our use of Kripke logical relations. For ordinary logical relations, there would be no free variables to consider.

The key part of the proof of contextual equivalence is the definition of the Kripke logical relation, which is a family of relations indexed by value and computation types. It is defined in Fig. 4 by induction on the structure of the types. In the figure, we again omit square brackets around equivalence classes.

Fig. 4.
figure 4

Definition of the logical relation

The definition of the logical relation on ground types (\(\mathbf {unit}\), sum types and product types) is standard. Since the only way to use a thunk is to force it, the definition on thunk types just requires the two forced computations to be related.

For returner types, we want any pair of computations that return related values to be related. We also want the relation to be closed under sequencing, in order to show the fundamental lemma (below) for and . We therefore define as the smallest such Kripke relation. For products of computation types the definition is similar to products of value types: we require that each of the projections are related. For function types, we require as usual that related arguments are sent to related results. For this to define a Kripke relation, we have to quantify over all computation contexts \({\varDelta }'\) that weaken \({\varDelta }\), because of the contravariance of the argument.

The relations we define are Kripke relations. Using the sequencing axioms of the equational theory, and the \(\beta \) and \(\eta \) laws for computation types, we can show that \(R_{\underline{C}}\) is closed under sequencing for each computation type \(\underline{C}\). These facts are important for the proof of the fundamental lemma.

Substitutions are given by the following grammar:

We have a typing judgement \({\varDelta } \vdash \sigma : {\varGamma }\) for substitutions, meaning in the context \({\varDelta }\) the terms in \(\sigma \) have the types given in \({\varGamma }\). This is defined as follows:

figure b

We write \(V[\sigma ]\) and \(M[\sigma ]\) for the applications of the substitution \(\sigma \) to value terms V and computation terms M. These are defined by induction on the structure of the terms. The key property of the substitution typing judgement is that if \({\varDelta } \vdash \sigma : {\varGamma }\), then implies and implies . The equational theory gives us an obvious pointwise equivalence relation on well-typed substitutions. We define sets of equivalence classes of substitutions, and extend the logical relation by defining :

As usual, the logical relations satisfy a fundamental lemma.

Lemma 1

(Fundamental)

  1. 1.

    For all value terms \({\varGamma } \vdash _\mathrm {v} V : A\),

  2. 2.

    For all computation terms ,

The proof is by induction on the structure of the terms. We use the fact that each is closed under sequencing for the and cases. For the latter, we also use the fact that the relations respect weakening of terms.

We also have the following two facts about the logical relation. The first roughly is that \(\mathsf {name}\) is related to by the logical relation, and is true because of the additional pairs that are related in the definition of closed-under-sequencing (Definition 4).

Lemma 2

For all computation terms and we have

The second fact is that related terms are contextually equivalent.

Lemma 3

 

  1. 1.

    For all value terms \({\varGamma } \vdash _\mathrm {v} V : A\) and \({\varGamma } \vdash _\mathrm {v} V' : A\), if for all then

  2. 2.

    For all computation terms and , if for all then

This gives us enough to achieve the goal of this section.

Theorem 1

For all computation terms and , we have

3.2 The Meta-level Equivalence

In this section, we show that the equivalence between call-by-name and call-by-need also holds on the meta-level; this is a consequence of the object-level theorem, rather than something that is proved from scratch as it would be in a term rewriting system.

To do this, we describe a simple lambda-calculus-based source language with divergence as the only side-effect and give it a call-by-name and a call-by-need operational semantics. We then describe two translations from the source language into ECBPV. The first is a call-by-name translation based on the embedding of call-by-name in Moggi’s [25] monadic metalanguage. The second is a call-by-need translation that uses our new constructs. The latter witnesses the fact that ECBPV does actually support call-by-need. Finally, we show that the two translations give contextually equivalent ECBPV terms.

The syntax, type system and operational semantics of the source language are given in Fig. 5. Most of this is standard. We include only booleans and function types for simplicity. In expressions, we include a constant \({\mathsf {diverge}_{A}}\) for each type A, representing a diverging computation. (As before, it should not be difficult to replace these with general fixed-point operators.) In typing contexts, we assume that all variables are distinct, and omit the required side-condition from the figure. There is a single set of variables \(x, y, \dots \); we implicitly map these to ECBPV value or computation variables as required.

The call-by-name operational semantics is straightforward; its small-step reductions are written .

Fig. 5.
figure 5

The source language

The call-by-need operational semantics is based on Ariola and Felleisen [2]. The only differences between the source language and Ariola and Felleisen’s calculus are the addition of booleans, \({\mathsf {diverge}_{A}}\), and a type system. It is likely that we can translate other call-by-need calculi, such as those of Launchbury [16] and Maraist et al. [22]. Call-by-need small-step reductions are written \(e \overset{\mathrm {need}}{\mathbin {\rightsquigarrow }}e'\).

The call-by-need semantics needs some auxiliary definitions. An evaluation context \(E[-]\) is a source-language expression with a single hole, picked from the grammar given in the figure. The hole in an evaluation context indicates where reduction is currently taking place: it says which part of the expression is currently needed. We write E[e] for the expression in which the hole is replaced with e. A (source-language) value is the result of a computation (the word value should not be confused with the value terms of extended call-by-push-value). An answer is a value in some environment, which maps variables to expressions. These can be thought of as closures. The environment is encoded in an answer using application and lambda abstraction: the answer \((\lambda x.\,a)\,e\) means the answer a where the environment maps x to e. Encoding environments in this way makes the translation slightly simpler than if we had used a Launchbury-style [16] call-by-need language with explicit environments. In the latter case, the translation would need to encode the environments. Here they are already encoded inside expressions. Answers are terminal computations: they do not reduce.

The first two reduction axioms (on the left) of the call-by-need semantics (Fig. 5d) are obvious. The third axiom is the most important: it states that if the subexpression currently being evaluated is a variable x, and the environment maps x to a source-language value v, then that use of x can be replaced with v. Note that E[v] may contain other uses of x; the replacement only occurs when the value is actually needed. This axiom roughly corresponds to the first sequencing axiom of the equational theory of ECBPV (in Fig. 3c). The fourth and fifth axioms of the call-by-need operational semantics rearrange the environment into a standard form. Both use a syntactic restriction to answers so that each expression has at most one reduct (this restriction is not needed to ensure that captures call-by-need). The rule on the right of the Fig. 5d states that the reduction relation is a congruence (a needed subexpression can be reduced).

Fig. 6.
figure 6

Translation from the source language to ECBPV

The two translations from the source language to ECBPV are given in Fig. 6. The translation of types (Fig. 6a) is shared between call-by-name and call-by-need. The two translations differ only for contexts and expressions. Types A are translated into value types . The type becomes the two-element sum type \(\mathbf {unit}+ \mathbf {unit}\). The translation of a function type \(A \rightarrow B\) is a thunked CBPV function type. The argument is a thunk of a computation that returns an , and the result is a computation that returns a .

For call-by-name (Fig. 6b), contexts \({\varGamma }\) are translated into contexts that contain thunks of computations. We could also have used contexts containing computation variables (omitting the thunks), but choose to use thunks to keep the translation as close as possible to previous translations into call-by-push-value. A well-typed expression is translated into a ECBPV computation term that returns , in context . The translation of variables just forces the relevant variable in the context. The diverging computations \({\mathsf {diverge}_{A}}\) just use the diverging constants from our ECBPV signature. The translations of \(\mathsf {true}\) and \(\mathsf {false}\) are simple: they are computations that immediately return one of the elements of the sum type \(\mathbf {unit}+ \mathbf {unit}\). The translation of first evaluates , then uses the result to choose between and . Lambdas are translated into computations that just return a thunked computation. Finally, application first evaluates the computation that returns a thunk of a function, and then forces this function, passing it a thunk of the argument.

For call-by-need (Fig. 6c), contexts \({\varGamma }\) are translated into contexts , containing computations that return values. The computations in the context are all bound using . An expression is translated to a computation that returns in the context . The typing is therefore similar to call-by-name. The key case is the translation of lambdas. These become computations that immediately return a thunk of a function. The function places the computation given as an argument onto the context using , so that it is evaluated at most once, before executing the body. The remainder of the cases are similar to call-by-name.

Under the call-by-need translation, the expression is translated into a term that executes the computation , and executes only when needed. This is the case because, by the \(\beta \) rules for thunks, functions, and returner types:

As a consequence, translations of answers are particularly simple: they have the following form (up to ):

which intuitively means the value V in the environment mapping each to \(M_i\).

It is easy to see that both translations produce terms with the correct types. We prove that both translations are sound: if \(e \overset{\mathrm {name}}{\mathbin {\rightsquigarrow }}e'\) then , and if \(e \overset{\mathrm {need}}{\mathbin {\rightsquigarrow }}e'\) then . To do this for call-by-need, we first look at translations of evaluation contexts. The following lemma says the translation captures the idea that the hole in an evaluation context corresponds to the term being evaluated.

Lemma 4

Define, for each evaluation context \(E[-]\), the term by:

For each expression e we have:

This lemma omits the typing of expressions for presentational purposes. It is easy to add suitable constraints on typing. Soundness is now easy to show:

Theorem 2 (Soundness)

For any two well-typed source-language expressions :

  1. 1.

    If then .

  2. 2.

    If \(e \overset{\mathrm {need}}{\mathbin {\rightsquigarrow }}e'\) then .

Now that we have sound call-by-name and call-by-need translations, we can state the meta-level equivalence formally. Suppose we are given a possibly open source-language expression . Recall that the call-by-need translation uses a context containing computation variables (i.e. ) and the call-by-name translation uses a context containing value variables, which map to thunks of computations. We have two ECBPV computation terms of type in context : one is just , the other is with all of its variables substituted with thunked computations. The theorem then states that these are contextually equivalent.

Theorem 3 (Equivalence between call-by-name and call-by-need)

For all source-language expressions e satisfying

Proof

The proof of this theorem is by induction on the typing derivation of e. The interesting case is lambda abstraction, where we use the internal equivalence between call-by-name and call-by-need (Theorem 1).

4 An Effect System for Extended Call-by-Push-Value

The equivalence between call-by-name and call-by-need in the previous section is predicated on the only effect in the language being nontermination. However, suppose the primitives of language have various effects (which means that in general the equivalence fails) but a given subprogram may be statically shown to have at most nontermination effects. In this case, we should be allowed to exploit the equivalence on the subprogram, interchanging call-by-need and call-by-name locally, even if the rest of the program uses other effects. In this section, we describe an effect system [20] for ECBPV, which statically estimates the side-effects of expressions, allowing us to exploit equivalences which hold only within subprograms. Effect systems can also be used for other purposes, such as proving the correctness of effect-dependent program transformations [7, 29]. The ECBPV effect system also allows these.

Call-by-need makes statically estimating effects difficult. Computation variables bound using might have effects on their first use, but on subsequent uses do not. Hence to precisely determine the effects of a term, we must track which variables have been used. McDermott and Mycroft [23] show how to achieve this for a call-by-need effect system; their technique can be adapted to ECBPV. Here we take a simpler approach. By slightly restricting the effect algebras we consider, we remove the need to track variable usage information, while still ensuring the effect information is not an underestimate (an underestimate would enable incorrect transformations). This can reduce the precision of the effect information obtained, but for our use case (determining equivalences between evaluation orders) this is not an issue, since we primarily care about which effects are used (rather than e.g. how many times they are used).

4.1 Effects

The effect system is parameterized by an effect algebra, which specifies the information that is tracked. Different effect algebras can be chosen for different applications. There are various forms of effect algebra. We follow Katsumata [15] and use preordered monoids, which are the most general.

Definition 5

(Preordered monoid). A preordered monoid consists of a monoid and a preorder on , such that the binary operation is monotone in each argument separately.

Since we do not track variable usage information, we might misestimate the effect of a call-by-need computation variable evaluated for a second time (whose true effect is ). To ensure this misestimate is an overestimate, we assume that the effect algebra is pointed (which is the case for most applications).

Definition 6

(Pointed preordered monoid). A preordered monoid is pointed if for all we have .

The elements of the set are called effects. Each effect abstractly represents some potential side-effecting behaviours. The order provides approximation of effects. When this means behaviours represented by are included in those represented by \(f'\). The binary operation represents sequencing of effects, and \(1\) is the effect of a side-effect-free expression.

Traditional (Gifford-style) effect systems have some set \({\varSigma }\) of operations (for example, ), and use the preordered monoid . In these cases, an effect \(f\) is just a set of operations. If a computation has effect then contains all of the operations the computation may perform. They can therefore be used to enforce that computations do not use particular operations. Another example is the preordered monoid , which can be used to count the number of possible results a nondeterministic computation can return (or to count the number of times an operation is used).

In our example, where we wish to establish whether the effects of an expression are restricted to nontermination for our main example, we use the two-element preorder with join for sequencing and . The effect \(\mathsf {diveff}\) means side-effects restricted to (at most) nontermination, and \(\top \) means unrestricted side-effects. Thus we would enable the equivalence between call-by-name and call-by-need when the effect is \(\mathsf {diveff}\), and not when it is . All of these examples are pointed. Others can be found in the literature.

4.2 Effect System and Signature

The effect system includes effects within types. Specifically, each computation of returner type will have some side-effects when it is run, and hence each returner type is annotated with an element f of \(\mathcal {F}\). We write the annotated type as . Formally we replace the grammar of ECBPV computation types (and similarly, the grammar of typing contexts) with

(The highlighted parts indicate the differences.) The grammar used for value types is unchanged, except that it uses the new syntax of computation types.

Fig. 7.
figure 7

Subtyping in the ECBPV effect system

The definition of ECBPV signature is similarly extended to contain the effect algebra as well as the set of constants:

Definition 7

(Signature). A signature consists of a pointed preordered monoid of effects and, for each value type A, a set of constants of type A, including .

We assume a fixed effect system signature for the remainder of this section.

Since types contain effects, which have a notion of subeffecting, there is a natural notion of subtyping. We define (in Fig. 7) two subtyping relations: \({A} <:_\mathrm {v} {B}\) for value types and for computation types.

We treat the type constructor as an operation on computation types by defining computation types .

This is an action of the preordered monoid on computation types. Its purpose is to give the typing rule for sequencing of computations. The sequencing of a computation with effect \(f\) with a computation of type \(\underline{C}\) has type .

The typing judgements have exactly the same form as before (except for the new syntax of types). The majority of the typing rules, including all of the rules for value terms, are also unchanged. The only rules we change are those for computation variables, \(\mathop {\mathsf {return}} \), and , which are replaced with the first four rules in Fig. 8. We also add two subtyping rules, one for values and one for computations. These are the last two rules of Fig. 8.

Fig. 8.
figure 8

Effect system modifications to ECBPV

The equational theory does not need to be changed to use it with the new effect system (except that the types appearing in each axiom now include effect information). For each axiom of the equational theory, the two terms still have the same type in the effect system. In particular, for the axiom

if and then the left-hand side has type . For the right-hand-side, we have , because of the assumption that the preordered monoid is pointed (which implies \(\mathop {\mathsf {return}} y\) can have any effect by subtyping, not just the unit effect \(1\)). Hence the right-hand-side also has type . This axiom is the reason for our pointedness requirement. In particular, if we drop from the language, the pointedness requirement is not required. Thus the rules we give also describe a fully general effect system for CBPV in which the effect algebra can be any preordered monoid.

4.3 Exploiting Effect-Dependent Equivalences

Our primary goal in adding an effect system to ECBPV is to exploit (local, effect-justified) equivalences between evaluation orders even without a whole-language restriction on effects. We sketch how to do this for our example.

When proving the equivalence between call-by-name and call-by-need in Sect. 3 we assumed that the only constants in the language were \({()}\) and . To relax this restriction, we use the effect algebra with preorder described above, and change the type of to . We can include other effectful constants, and give them the effect (e.g. ).

The statement of the internal (object-level) equivalence becomes:

The premise restricts the effect of M to \(\mathsf {diveff}\) so that nontermination is its only possible side-effect. To prove this equivalence, we need a logical relation for the effect system, which means we have to define a Kripke relation for each effect \(f\). For we use the same definition as before (the definition of ). The definition of depends on the specific other effects included.

To state and prove a meta-level equivalence for a source language that includes other side-effects, we need to define an effect system for the source language. This would use the same effect algebra as the ECBPV effect system, and be such that the translation of source language expressions preserves effects. To do this for the source language of Sect. 3, we replace the syntax of function types with , where \(f\) is the effect of the argument (required due to lazy evaluation), and \(f'\) is the latent effect of the function (the effect it has after application). The translation is then

Just as for the object-level equivalence, the statement of the meta-level equivalence similarly requires the source-language expression to have the effect \(\mathsf {diveff}\). We omit the details here.

5 Related Work

Metalanguages for Evaluation Order. Call-by-push-value is similar to Moggi’s monadic metalanguage [25], except for the distinction between computations and values. Both support several evaluation orders, but neither supports call-by-need. Polarized type theories [34] also take the approach of stratifying types into several kinds to capture multiple evaluation orders. Downen and Ariola [10] recently described how to capture call-by-need using polarity. They take a different approach to ours, by splitting up terms according to their evaluation order, rather than whether they might have effects. This means they have three kinds of type, resulting in a more complex language than ours. They also do not apply their language to reasoning about the differences between evaluation orders, which was the primary motivation for ECBPV. It is not clear whether their language can also be used for this purpose.

Multiple evaluation orders can also be captured in a Moggi-style language by using joinads instead of monads [28]. It is possible that there is some joinad structure implicit in extended call-by-push-value.

Reasoning About Call-by-Need. The majority of work on reasoning about call-by-need source languages has concentrated on operational semantics based on environments [16], graphs [30, 32], and answers [2, 3, 9, 22]. However, these do not compare call-by-need with other evaluation orders. The only type-based analysis of a lazy source language we know of apart from McDermott and Mycroft’s effect system [23] is [31, 33].

Logical Relations. Kripke logical relations have previously been applied to the problems of lambda definability [12] and normalization [1, 11]. Previous proofs of contextual equivalence relate only closed terms. We were forced to relate open terms because of the construct.

Reasoning about effects using logical relations often runs into a difficulty in ensuring the relations are closed under sequencing of computations. We are able to work around this due to our specific choice of effects. It is possible that considering other effects would require a technique such as Lindley and Stark’s leapfrog method [18, 19].

Effect Systems. Effect systems have a long history, starting with Gifford-style effect systems [20]. We use preordered monoids as effect algebras following Katsumata [15]. Almost all of the previous work on effect systems has concentrated on call-by-value only. Kammar and Plotkin [13, 14] describe a Gifford-style call-by-push-value effect system, though their formulation does not generalise to other effect algebras. Our effect system is the first general effect system for a CBPV-like language. The only previous work on call-by-need effects is [23].

There has also been much work on reasoning about program transformations using effect systems, e.g. [4,5,6,7,8, 29]. We expect it to be possible to recast much of this in terms of extended call-by-push-value, and therefore apply these transformations for various evaluation orders.

6 Conclusions and Future Work

We have described extended call-by-push-value, a calculus that can be used for reasoning about several evaluation orders. In particular, ECBPV supports call-by-need via the addition of the construct . This allows us to prove that call-by-name and call-by-need reduction are equivalent if nontermination is the only effect in the source language, both inside the language itself, and on the meta-level. We proved the latter by giving two translations of a source language into ECBPV: one that captures call-by-name reduction, and one that captures call-by-need reduction. We also defined an effect system for ECBPV. The effect system statically bounds the side-effects of terms, allowing equivalences between evaluation orders to be used without restricting the entire language to particular effects. We close with a description of possible future work.

Other Equivalences Between Evaluation Orders. We have proved one example of an equivalence between evaluation orders using ECBPV, but there are others that we might also expect to hold. For example, we would expect call-by-need and call-by-value to be equivalent if the effects are restricted to nondeterminism, allocating state, and reading from state (but not writing). It should be possible to use ECBPV to prove these by defining suitable logical relations. More generally, it might be possible to characterize when particular equivalences hold in terms of the algebraic properties of the effects we restrict to.

Denotational Semantics. Using logical relations to prove contextual equivalence between terms directly is difficult. Adequate denotational semantics would allow us to reduce proofs of contextual equivalence to proofs of equalities in the model. Composing the denotational semantics with the call-by-need translation would also result in a call-by-need denotational semantics for the source language. Some potential approaches to describing the denotational semantics of ECBPV are Maraist et al.’s [21] translation into an affine calculus, combined with a semantics of linear logic [24], and also continuation-passing-style translations [27]. None of these consider side-effects however.