1 Introduction
The trade-off between “high-level” and “low-level” styles of programming is almost as old as the field of computer science itself. In a high-level style of programming, we lean on abstractions to make our programs easier to read and write and less error prone. We pay for this comfort by giving up precise control over the underlying machinery; we forego optimization opportunities or have to trust a (usually opaque) compiler to perform low-level optimizations for us. For performance-sensitive applications, compiler optimizations are not reliable enough; instead we often resort to lower-level programming techniques ourselves. Although these lower-level programming techniques allow a fine-grained control over program execution and the implementation of optimization techniques, they tend to be harder to write and not compose very well. This is an important trade-off to take into account when choosing an appropriate programming language for implementing an application.
Maybe surprisingly, as they are rarely described in this way, there is a similar pattern for side effects within programming languages: some effects can be described as “lower-level” than others. Informally, we say that an effect is lower-level than another effect when the lower-level effect can simulate the higher-level effect. In other words, it is possible to write a program using lower-level effects that has identical semantics to the same program with higher-level effects. Yet, due to the lack of abstraction of low-level effects, writing a faithful simulation requires careful discipline and is quite error prone.
This article investigates how we can construct programs that are most naturally expressed with a high-level effect, but where we still want access to the optimization opportunities of a lower-level effect. In particular, inspired by Prolog and Constraint Programming systems, we investigate programs that rely on high-level interaction between the nondeterminism and state effects which we call local state. Following low-level implementation techniques for these systems, like the Warren Abstract Machine (WAM) (Warren, Reference Warren1983; Aït-Kaci, 1991), we show how these high-level effects can be simulated in terms of the low-level global state interaction of state and nondeterminism, and finally by state alone. This allows us to incorporate typical optimizations like exploiting mutable state for efficient backtracking based on trailing as opposed to copying or recomputing the state from scratch (Schulte, Reference Schulte1999).
Our approach is based on algebraic effects and handlers (Plotkin and Power, Reference Plotkin and Power2003; Plotkin and Pretnar, Reference Plotkin and Pretnar2009, Reference Plotkin and Pretnar2013) to cleanly separate the syntax and semantics of effects. For programs written with high-level effects and interpreted by their handlers, we can define a general translation handler to transform these high-level effects to low-level effects and then interpret the translated programs with the handlers of low-level effects. Though we do not give a formal definition of a simulation from high-level effects to low-level effects, we expect it to be a handler that interprets the operations of the high-level effects in terms of the operations of the low-level effects. This handler is essentially a monomorphism from the syntax tree of high-level effects to that of low-level effects, similar to Felleisen (Reference Felleisen1991)’s notion of macro expansion but at the continuation-passing level (as the syntax trees of free monads provide access to continuations).
Of particular interest is the way we reason about the correctness of our approach. There has been much debate in the literature on different equational reasoning approaches for effectful computations. Hutton and Fulger (Reference Hutton and Fulger2008) break the abstraction boundaries and use the actual implementation in their equational reasoning approach. Gibbons and Hinze (Reference Gibbons and Hinze2011) promote an alternative, law-based approach to preserve abstraction boundaries and combine axiomatic with equational reasoning. In an earlier version of this work (Pauwels et al., Reference Pauwels, Schrijvers and Mu2019), we have followed the latter, law-based approach for reasoning about the correctness of simulating local state with global state. However, we have found that approach to be unsatisfactory because it incorporates elements that are usually found in the syntactic approach for reasoning about programming languages (Wright and Felleisen, Reference Wright and Felleisen1994), leading to more boilerplate and complication in the proofs: notions of contextual equivalence and explicit manipulation of program contexts. Hence, for that reason we return to the implementation-based reasoning approach, which we believe works well with algebraic effects and handlers. Indeed, we prove all of our simulations correct using equational reasoning techniques, exploiting in particular the fusion property of handlers (Wu and Schrijvers, Reference Wu and Schrijvers2015; Gibbons, Reference Gibbons2000).
After introducing the reader to the appropriate background material and motivating the problem (Sections 2 and 3), this paper makes the following contributions:
• We distinguish between local-state and global-state semantics and simulate the former in terms of the latter (Section 4).
• We simulate nondeterminism using a state that consists of a choicepoint stack (Section 5).
• We combine the previous two simulations and merge the two states into a single state effect (Section 6).
• By only allowing incremental, reversible updates to the state we can avoid holding on to multiple copies of the state (Section 7).
• By storing the incremental updates in a trail stack state, we can restore them in batch when backtracking (Section 8).
• We prove all simulations correct using equational reasoning techniques and the fusion law for handlers in particular (Appendices 2, 3, 4, 5, 6, and 7).
Finally, we discuss related work (Section 9) and conclude (Section 10). Table 1 gives an overview of the simulations of high-level effects with low-level effects, we implemented and proved in the paper. Throughout the paper, we use Haskell as a means to illustrate our findings with code. In particular, we restrict ourselves to a well-behaved and well-founded fragment of Haskell that avoids non-termination and other forms of bottom and readily admits equational reasoning with structural induction. Moreover, we focus on formalising and proving the correctness of the simulations rather than empirical evidence of performance improvements, as those have already been demonstrated by real-world systems like Prolog. In fact, the Haskell implementations themselves do not exhibit performance improvements due to aspects like laziness, immutable state, and the overhead of algebraic effects and handlers.
2 Background and motivation
This section summarizes the main prerequisites for equational reasoning with effects and motivates our translations from high-level effects to low-level effects. We discuss the two central effects of this paper: state and nondeterminism.
2.1 Functors and monads
Functors. In Haskell, a functor ${{f}\mathbin{::}\mathbin{\ast}\to \mathbin{\ast}}$ instantiates the functor type class, which has a single functor mapping operation.
Furthermore, a functor should satisfy the following two functor laws:
We sometimes use the operator ${\mathbin{\langle{{{\$}}}\rangle}}$ as an alias for fmap.
Monads. Monadic side effects (Moggi, Reference Moggi1991), the main focus of this paper, are those that can dynamically determine what happens next. A monad ${{m}\mathbin{::}\mathbin{\ast}\to \mathbin{\ast}}$ is a functor which instantiates the monad type class, which has two operations return (η) and bind $({>\!\!>\!\!=})$ .
Furthermore, a monad should satisfy the following three monad laws:
Haskell supports ${\mathbf{do}}$ blocks as syntactic sugar for monadic computations. For example, ${\mathbf{do}\;{x}\leftarrow {m};{f}\;{x}}$ is translated to ${{m}>\!\!>\!\!={f}}$ . Two convenient derived operators are ${>\!\!>}$ and ${\mathbin{\langle{{{\ast}}}\rangle}}$ .Footnote 1
2.2 Nondeterminism and state
Following both the approaches of Hutton and Fulger (Reference Hutton and Fulger2008) and of Gibbons and Hinze (Reference Gibbons and Hinze2011), we introduce effects as subclasses of the Monad type class.
Nondeterminism. The first monadic effect we introduce is nondeterminism. We define a subclass MNondet of Monad to capture the nondeterministic interfaces as follows:
Here, ∅ denotes failures and $(\left[\!\right])$ denotes nondeterministic choices. Instances of the MNondet interface should satisfy the following four laws:Footnote 2
The first two laws state that ${(\left[\!\right])}$ and should form a monoid, i.e., ${(\left[\!\right])}$ should be associative with as its neutral element. The last two laws show that ${(>\!\!>\!\!=)}$ is right-distributive over ${(\left[\!\right])}$ and that ${{\varnothing}}$ cancels bind on the left.
The approach of Gibbons and Hinze (Reference Gibbons and Hinze2011) is to reason about effectful programs using an axiomatic characterization given by these laws. It does not rely on the specific implementation of any particular instance of MNondet. In contrast, Hutton and Fulger (Reference Hutton and Fulger2008) reason directly in terms of a particular instance. In the case of MNondet, the quintessential instance is the list monad, which extends the conventional Monad instance for lists.
State. The signature for the state effect has two operations: a get operation that reads and returns the state and a put operation that modifies the state, overwriting it with the given value, and returns nothing. Again, we define a subclass MState of Monad to capture its interfaces.
These operations are regulated by the following four laws:
The standard instance of MState is the state monad State s.
2.3 The N-queens puzzle
The n-queens problem used here is an adapted and simplified version from that of Gibbons and Hinze (Reference Gibbons and Hinze2011). The aim of the puzzle is to place n queens on a $n \times n$ chess board such that no two queens can attack each other. This means that no two queens should be placed on the same row, the same column or the same diagonal of the chess board.
Given n, we number the rows and columns by ${[\mathrm{1}\mathinner{\ldotp\ldotp}{n}]}$ . Since all queens should be placed on distinct rows and distinct columns, a potential solution can be represented by a permutation xs of the list ${[\mathrm{1}\mathinner{\ldotp\ldotp}{n}]}$ , such that ${{xs}\mathbin{!!}{i}\mathrel{=}{j}}$ denotes that the queen on the ith column is placed on the jth row. Using this representation, queens cannot be put on the same row or column.
A naive algorithm. We have the following naive nondeterministic algorithm for n-queens.
The program ${{queens}_{{naive}}\;\mathrm{4}\mathbin{::}[ [ {Int}]]}$ gives as result ${[ [ \mathrm{2},\mathrm{4},\mathrm{1},\mathrm{3}],[ \mathrm{3},\mathrm{1},\mathrm{4},\mathrm{2}]]}$ . The program uses a generate-and-test strategy: it generates all permutations of queens as candidate solutions, and then tests which ones are valid.
The function ${{permutations}\mathbin{::}[ {a}]\to [ [ {a}]]}$ from Data.List computes all the permutations of its input. The function choose implemented as follows nondeterministically picks an element from a list.
The function filtr p x returns x if p x holds and fails otherwise.
The pure function ${{valid}\mathbin{::}[ {Int}]\to {Bool}}$ determines whether the input is a valid solution.
A solution is valid when each queen is safe with respect to the subsequent queens:
The call safe q n qs checks whether the current queen q is on a different ascending and descending diagonal than the other queens qs, where n is the number of columns that q is apart from the first queen ${{q}}_{{1}}$ in qs.
Although this generate-and-test approach works and is quite intuitive, it is not very efficient. For example, all solutions of the form ${(\mathrm{1}\mathbin{:}\mathrm{2}\mathbin{:}{qs})}$ are invalid because the first two queens are on the same diagonal. However, the algorithm still needs to generate and test all $(n-2)!$ candidate solutions of this form.
A backtracking algorithm. We can fuse the two phases of the naive algorithm to obtain a more efficient algorithm, where both generating candidates and checking for validity happens in a single pass. The idea is to move to a state-based backtracking implementation that allows early pruning of branches that are invalid. In particular, when placing the new queen in the next column, we make sure that it is only placed in positions that are valid with respect to the previously placed queens.
We use a state (Int, [ Int]) to contain the current column and the previously placed queens. The backtracking algorithm of n-queens is implemented as follows.
The function guard fails when the input is false.
The function ${{s}\mathbin{\oplus}{r}}$ updates the state with a new queen placed on row r in the next column.
The above monadic version of queens essentially assume that each searching branch has its own state; we do not need to explicitly restore the state when backtracking. Though it is a convenient high-level programming assumption for programmers, it causes obstacles to low-level implementations and optimizations. In the following sections, we investigate how low-level implementation and optimization techniques, such as those found in Prolog’s Warren Abstract Machine and Constraint Programming systems, can be incorporated and proved correct.
3 Algebraic effects and handlers
This section introduces algebraic effects and handlers, the approach we use to define syntax, semantics, and simulations for effects. Comparing to giving concrete monad implementations for effects, algebraic effects and handlers allow us to easily provide different interpretations for the same effects due to the clear separation of syntax and semantics. As a result, we can smoothly specify translations from high-level effects to low-level effects as handlers of these high-level effects and then compose them with the handlers of low-level effects to interpret high-level programs. Algebraic effects and handlers also provide us with a modular way to combine our translations with other effects, and a useful tool, the fusion property, to prove the correctness of translations.
3.1 Free monads and their folds
We implement algebraic effects and handlers as free monads and their folds.
Free monads. Free monads are gaining popularity for their use in algebraic effects (Plotkin and Power, Reference Plotkin and Power2002, Reference Plotkin and Power2003) and handlers (Plotkin and Pretnar, Reference Plotkin and Pretnar2009, Reference Plotkin and Pretnar2013), which elegantly separate syntax and semantics of effectful operations. A free monad, the syntax of an effectful program, can be captured generically in Haskell.
This data type is a form of abstract syntax tree (AST) consisting of leaves (Var a) and internal nodes ( ${{Op}\;({f}\;({Free}\;{f}\;{a}))}$ ), whose branching structure is determined by the functor f. This functor is also known as the signature of operations.
A fold recursion scheme. Free monads come equipped with a fold recursion scheme.
This fold interprets an AST structure of type ${{Free}\;{f}\;{a}}$ into some semantic domain b. It does so compositionally using a generator ${{gen}\mathbin{::}{a}\to {b}}$ for the leaves and an algebra ${{alg}\mathbin{::}{f}\;{b}\to {b}}$ for the internal nodes; together these are also known as a handler.
The monad instance of Free is straightforwardly implemented with fold.
Under certain conditions folds can be fused with functions that are composed with them (Wu and Schrijvers, Reference Wu and Schrijvers2015; Gibbons, Reference Gibbons2000). This gives rise to the following laws:
These three fusion laws turn out to be essential in the further proofs of this paper.
Nondeterminism. Instead of using a concrete monad like List, we use the free monad Free Nondet F over the signature Nondet F following algebraic effects.
This signatures gives rise to a trivial MNondet instance:
With this representation the right-distributivity law and the left-identity law follow trivially from the definition of ${(>\!\!>\!\!=)}$ for the free monad.
In contrast, the identity and associativity laws are not satisfied on the nose. Indeed, ${{Op}\;({Or}\;{Fail}\;{p})}$ is for instance a different abstract syntax tree than p. Yet, these syntactic differences do not matter as long as their interpretation is the same. This is where the handlers come in; the meaning they assign to effectful programs should respect the laws. We have the following ${{h}}_{{ND}}$ handler which interprets the free monad in terms of lists.
With this handler, the identity and associativity laws are satisfied up to handling as follows:
In fact, two stronger contextual equalities hold:
These equations state that the interpretations of the left- and right-hand sides are indistinguishable even when put in a larger program context ${>\!\!>\!\!={k}}$ . They follow from the definitions of ${{h}}_{{ND}}$ and ${(>\!\!>\!\!=)}$ , as well as the associativity and identity properties of ${(+\!\!+)}$ .
We obtain the two non-contextual equations as a corollary by choosing ${{k}\mathrel{=}{\eta}}$ .
State. Again, instead of using the concrete State monad in Section 2.2, we model states via the free monad ${{Free}\;({State}}_{{F}\;{s})}$ over the state signature.
This state signature gives the following MState s instance:
The following handler ${{h}^\prime_{{State}}}$ maps this free monad to the ${{State}\;{s}}$ monad.
It is easy to verify that the four state laws hold contextually up to interpretation with ${{h}^\prime_{{State}}}$ .
3.2 Modularly combining effects
Combining multiple effects is relatively easy in the axiomatic approach based on type classes. By imposing multiple constraints on the monad m, e.g. ${({MState}\;{s}\;{m},{MNondet}\;{m})}$ , we can express that m should support both state and nondeterminism and respect their associated laws. In practice, this is often insufficient: we usually require additional laws that govern the interactions between the combined effects. We discuss possible interaction laws between state and nondeterminism in details in Section 4.
The coproduct operator for combining effects. To combine the syntax of effects given by free monads, we need to define a right-associative coproduct operator ${\mathrel{{:}{+}{:}}}$ for signatures.
Note that given two functors f and g, it is obvious that ${{f}\mathrel{{:}{+}{:}}{g}}$ is again a functor. This coproduct operator allows a modular definition of the signatures of combined effects. For instance, we can encode programs with both state and nondeterminism as effects using the data type ${Free}\;({State}_{F}\mathrel{:}{+}{:}{Nondet}_{F})\;{a}$ . The coproduct also has a neutral element ${{Nil}_{{F}}}$ , representing the empty effect set.
We define the following two instances, which allow us to compose state effects with any other effect functor f, and nondeterminism effects with any other effect functors f and g, respectively. As a result, it is easy to see that ${Free}\;({State}_{F}\;{s}\mathrel{{:}{+}{:}}{Nondet}_{F}\mathrel{{:}{+}{:}}{f})$ supports both state and nondeterminism for any functor f.
Modularly combining effect handlers. In order to interpret composite signatures, we use the forwarding approach of Schrijvers et al. (Reference Schrijvers, Piróg, Wu and Jaskelioff2019). This way the handlers can be modularly composed: they only need to know about the part of the syntax their effect is handling and forward the rest of the syntax to other handlers.
A mediator ${(\mathbin{\#})}$ is used to separate the algebra alg for the handled effects and the forwarding algebra fwd for the unhandled effects.
The handlers for state and nondeterminism we have given earlier require a bit of adjustment to be used in the composite setting since they only consider the signature of their own effects. We need to interpret the free monads into composite domains, ${{StateT}\;({Free}\;{f})\;{a}}$ and ${{Free}\;{f}\;[ {a}}$ , respectively. Here, StateT is the state transformer from the Monad Transformer Library (Jones, Reference Jones1995).
The new handlers, into these composite domains, are defined as follows:
Also, the empty signature ${{Nil}}_{{{F}}}$ has a trivial associated handler.
3.3 Proof device
The algebraic effects and handlers implementation we introduce in this paper is, much like the core calculus of a programming language, a proof device and not an ergonomic library. The results we obtain for this representation can be transferred to other representations.
Explicit isomorphisms. For instance, notice for instance that ${{h}}_{{{State}}}$ and ${{h}}_{{{ND+f}}}$ both require the signature they handle to be on the left in the co-product. It is possible to relax this requirement by means of advanced type-level programming, e.g., using type class overloading (Swierstra, Reference Swierstra2008). That makes using handlers more ergonomic at the cost of obscuring formal reasoning about them. Because the latter is the focus of this paper, we do not introduce the additional flexibility. Instead, we appeal to explicit isomorphisms, to reorder the signatures in a co-product. For example, the ${{(\Leftrightarrow)}}$ isomorphism that we will use in Section 4.2 swaps the order of two functors in the co-product signature of the free monad.
Transfer to other representations. Our use of type class constraints allows us to reduce other monadic representations to the core algebraic effects and handlers representation by an appeal to parametricity (Voigtländer, Reference Voigtländer2009). For instance, for a program ${{p}\mathbin{::}\forall {m}\forall {\circ }{MNondet}\;{m}\Rightarrow {m}\;{Int}}$ we have that:
This is true because ${h}_{ND}$ is the structure-preserving map from the ${{Free}\;{Nondet}}_{{{F}}}$ instance of ${{MNondet}}$ to the [] instance. That is to say, ${h}_{ND}$ satisfies the following four equations:
Since the free-monad representation is initial, the structure-preserving map ${{h}}_{{ND}}$ is guaranteed to exist and be unique. Now, if we want to prove a property about ${{p}\mathbin{::}[ {Int}]}$ , the parametricity equation allows us to prove it instead about ${{h}_{{ND}}\;({p}\mathbin{::}{Free}\;{Nondet}_{{F}}\;{Int})}$ . A similar observation can be made for other constraints, like MState or the combination of MState and MNondet.
In the rest of this paper, we focus on results for the core representation of algebraic effects and handlers. By means of the above approach, these results can be generalized to other representations.
4 Modelling local state with global state
This section studies two flavours of effect interaction between state and nondeterminism: local-state and global-state semantics. Local state is a higher-level effect than global state. In a program with local state, each nondeterministic branch has its own local copy of the state. This is a convenient programming abstraction provided by many systems that solve search problems, e.g., Prolog. In contrast, global state linearly threads a single state through the nondeterministic branches. This can be interesting for performance reasons: we can limit memory use by avoiding multiple copies, and perform in-place updates to reduce allocation and garbage collection, and to improve locality.
In this section, we first formally characterize local-state and global-state semantics, and then define a translation from the former to the latter which uses the mechanism of nondeterminism to store previous states and insert backtracking branches.
4.1 Local-state semantics
When a branch of a nondeterministic computation runs into a dead end and the continuation is picked up at the most recent branching point, any alterations made to the state by the terminated branch are invisible to the continuation. We refer to this semantics as local-state semantics. Gibbons and Hinze (Reference Gibbons and Hinze2011) also call it backtrackable state.
The local-state laws. The following two laws characterize the local-state semantics for a monad with state and nondeterminism:
The equation (4.1) expresses that is the right identity of put; it annihilates state updates. The other law expresses that put distributes from the left in ${(\left[\!\right])}$ .
These two laws only focus on the interaction between put and nondeterminism. The following laws for get can be derived from other laws. The proof can be found in Appendix 1.
If we take these four equations together with the left-identity and right-distributivity laws of nondeterminism, we can say that nondeterminism and state “commute”; if a get or put precedes a or ${\mathbin{\left[\!\right]}}$ , we can exchange their order (and vice versa).
Implementation. Implementation-wise, the laws imply that each nondeterministic branch has its own copy of the state. For instance, Equation (4.2) gives us
The state we get in the second branch is still 42, despite the ${{put}\;\mathrm{21}}$ in the first branch.
One implementation satisfying the laws is
where m is a nondeterministic monad, the simplest structure of which is a list. This implementation is exactly that of StateT s m a in the Monad Transformer Library (Jones, Reference Jones1995) which we have introduced in Section 3.2.
With effect handling (Kiselyov and Ishii, Reference Kiselyov and Ishii2015; Wu et al., Reference Wu, Schrijvers and Hinze2014), we get the local state semantics when we run the state handler before the nondeterminism handler:
In the case where the remaining signature is empty ( ${{f}\mathrel{=}{Nil}_{{F}}}$ ), we get:
Here, the result type ${({s}\to [ {a}])}$ differs from ${{s}\to [ ({a},{s})]}$ in that it produces only a list of results ( ${[ {a}]}$ ) and not pairs of results and their final state $({[ ({a},{s})]})$ . The latter is needed for ${{Local}\;{s}\;{m}}$ to have the structure of a monad, in particular to support the modular composition of computations with ${(>\!\!>\!\!=)}$ . Such is not needed for the carriers of handlers because the composition of computations is taken care of by the ${(>\!\!>\!\!=)}$ operator of the free monad.
4.2 Global-state semantics
Alternatively, one can choose a semantics where state reigns over nondeterminism. In this case of non-backtrackable state, alterations to the state persist over backtracks. Because only a single state is shared over all branches of nondeterministic computation, we call this state the global-state semantics.
The global-state law. The global-state semantics sets apart non-backtrackable state from backtrackable state. In addition to the general laws for nondeterminism ((2.6) – (2.9)) and state ((2.10) – (2.13)), we provide a global-state law to govern the interaction between nondeterminism and state.
This law allows lifting a put operation from the left branch of a nondeterministic choice. For instance, if ${{m}\mathrel{=}{\varnothing}}$ in the left-hand side of the equation, then under local-state semantics (laws (2.6) and (4.1)) the left-hand side becomes equal to n, whereas under global-state semantics (laws (2.6) and (4.5)) the equation simplifies to ${{put}\;{s}>\!\!>{n}}$ .
Implementation. Figuring out a correct implementation for the global-state monad is tricky. One might believe that ${{Global}\;{s}\;{m}\;{a}\mathrel{=}{s}\to ({m}\;{a},{s})}$ is a natural implementation of such a monad. However, the usual naive implementation of ${(>\!\!>\!\!=)}$ for it does not satisfy right-distributivity (2.8) and is therefore not even a monad. The type ${{ListT}\;({State}\;{s})}$ from the Monad Transformer Library (Jones, Reference Jones1995) expands to essentially the same implementation with monad m instantiated by the list monad. This implementation has the same flaws. More careful implementations of ListT (often referred to as “ListT done right”) satisfying right-distributivity (2.8) and other monad laws have been proposed by Volkov (Reference Volkov2014); Gale (Reference Gale2007). The following implementation is essentially that of Gale.
The Maybe in this type indicates that a computation may fail to produce a result. However, since the s is outside of the Maybe, a modified state is returned even if the computation fails. This ${{Global}\;{s}\;{a}}$ type is an instance of the MState and MNondet type classes.
Failure, of course, returns an empty continuation and an unmodified state. Branching first exhausts the left branch before switching to the right branch.
Effect handlers (Kiselyov and Ishii, Reference Kiselyov and Ishii2015; Wu et al., Reference Wu, Schrijvers and Hinze2014) also provide implementations that match our intuition of non-backtrackable computations. The global-state semantics can be implemented by simply switching the order of the two effect handlers compared to the local state handler ${{h}_{{Local}}}$ .
This also runs a single state through a nondeterministic computation. Here, the ${{(\Leftrightarrow)}}$ isomorphism from Section 3.2 allows ${{h}_{{Local}}}$ and ${{h}_{{Global}}}$ have the same type signature.
In the case where the remaining signature is empty ( ${{f}\mathrel{=}{Nil}_{{F}}}$ ), we get:
Like in Section 4.1, the carrier type here is again simpler than that of the corresponding monad because it does not have to support the ${(>\!\!>\!\!=)}$ operator.
4.3 Simulating local state with global state
Both local state and global state have their own laws and semantics. Also, both interpretations of nondeterminism with state have their own advantages and disadvantages.
Local-state semantics imply that each nondeterministic branch has its own state. This may be expensive if the state is represented by data structures, e.g. arrays, that are costly to duplicate. For example, when each new state is only slightly different from the previous, we have a wasteful duplication of information.
Global-state semantics, however, threads a single state through the entire computation without making any implicit copies. Consequently, it is easier to control resource usage and apply optimization strategies in this setting. However, doing this to a program that has a backtracking structure, and would be more naturally expressed in a local-state style, comes at a great loss of clarity. Furthermore, it is significantly more challenging for programmers to reason about global-state semantics than local-state semantics.
To resolve this dilemma, we can write our programs in a local-state style and then translate them to the global-state style to enable low-level optimizations. In this subsection, we show one systematic program translation that alters a program written for local-state semantics to a program that, when interpreted under global-state semantics, behaves exactly the same as the original program interpreted under local-state semantics. This translation explicitly copies the whole state and relies on the nondeterminism mechanism to insert state-restoring branches. We will show other translations from local-state semantics to global-state semantics which avoid the copying and do not rely on nondeterminism in Sections 7 and 8.
State-restoring put. Central to the implementation of backtracking in the global state setting is the backtracking variant ${{put}_{{R}}}$ of ${{put}}$ . The idea is that ${{put}_{{R}}}$ , when run with a global state, satisfies laws (2.10) to (4.2) — the state laws and the local-state laws. Going forward, ${{put}_{{R}}}$ modifies the state as usual, but, when backtracked over, it restores the old state.
We implement ${{put}_{{R}}}$ using both state and nondeterminism as follows:
Here the side branch is executed for its side effect only; it fails before yielding a result.
Intuitively, the second branch generated by ${{put}_{{R}}}$ can be understood as a backtracking or state-restoring branch. The put_R s operation changes the state to s in the first branch put s, and then restores it to the original state s’ in the second branch after we finish all computations in the first branch. Then, the second branch immediately fails so that we can keep going to other branches with the original state s’. For example, assuming an arbitrary computation comp is placed after a state-restoring put, we have the following calculation.
This program saves the current state s’, computes comp using state s, and then restores the saved state s’. Figure 1 shows how the state-passing works and the flow of execution for a computation after a state-restoring put.
Another example of ${{put}_{{R}}}$ is shown in Table 2, where three programs are run with initial state ${{s}}_{{0}}$ . Note the difference between the final state and the program result for the state-restoring put.
Translation with state-restoring put. We do not expect the programmer to program against the global-state semantics directly and use the state-restoring ${{put}_{{R}}}$ as they see fit, as this can be quite confusing and error-prone. Instead, we provide an automatic translation: The programmer writes their program against the local-state semantics and uses the regular put. We then translate the local-state semantics program to a corresponding global-state semantics program using the effect handler local2global:
This handler maps the put with local-state semantics onto the state-restoring ${{put}_{{R}}}$ with global-state semantics. All other local-state operations are mapped onto their global-state counterpart.
For example, recall the backtracking algorithm queens for the n-queens example in Section 2.3. It is initially designed to run in the local-state semantics because every branch maintains its own copy of the state and has no influence on other branches. We can handle it with ${{h}_{{Local}}}$ as follows.
With the simulation local2global, we can also translate queens to an equivalent program in global-state semantics and handle it with ${{h}_{{Global}}}$ .
The following theorem guarantees that the translation local2global preserves the meaning when switching from local-state to global-state semantics:
Theorem 1.
Proof Both the left-hand side and the right-hand side of the equation consist of function compositions involving one or more folds. We apply fold fusion separately on both sides to contract each into a single fold:
We approach this calculationally. That is to say, we do not first postulate definitions of the unknowns above ( ${{alg}}_{{{LHS}}^{{S}}}$ and so on) and then verify whether the fusion conditions are satisfied. Instead, we discover the definitions of the unknowns. We start from the known side of each fusion condition and perform case analysis on the possible shapes of input. By simplifying the resulting case-specific expression, and pushing the handler applications inwards, we end up at a point where we can read off the definition of the unknown that makes the fusion condition hold for that case.
Finally, we show that both folds are equal by showing that their corresponding parameters are equal:
A noteworthy observation is that, for fusing the left-hand side of the equation, we do not use the standard fusion rule fusion-post (3.2):
where ${{local2global}\mathrel{=}{fold}\;{Var}\;{alg}}$ . The problem is that we will not find an appropriate ${{alg'}}$ such that ${{alg'}\;({fmap}\;{h}_{{Global}}\;{t})}$ restores the state for any t of type $({State}_{F}\;{s}\mathrel{:}{+}{:}{Nondet}_{F}\mathrel{:}{+}{:}{f})\;({Free}\;({State}_{F}\;{s}\mathrel{:}{+}{:}{Nondet}_{F}\mathrel{:}{+}{:}{f})\;{a})$ .
Fortunately, we do not need such an ${{alg'}}$ . We can assume that the subterms of t have already been transformed by local2global, and thus all occurrences of Put appear in the ${{put}_{{R}}}$ constellation.
We can incorporate this assumption by using the alternative fusion rule fusion-post’ (3.3):
The additional ${{fmap}\;{local2global}}$ in the condition captures the property that all the subterms have been transformed by local2global.
In order to not clutter the proofs, we abstract everywhere over this additional ${{fmap}\;{local2global}}$ application, except for the key lemma which expresses that the syntactic transformation local2global makes sure that, despite any temporary changes, the computation t restores the state back to its initial value.
We elaborate each of these steps in Appendix 2.
Note on global replacement. To preserve the behaviour when going from local-state to global-state semantics, care should be taken to replace all occurrences of put. Particularly, placing a program in a larger context, where put has not been replaced, can change the meaning of its subprograms. An example of such a problematic context is ${(>\!\!>{put}\;{t})}$ , where the ${{get}}-{{put}}$ law (2.12) breaks and programs ${{get}>\!\!>{put}_{{R}}}$ and ${{\eta}\;()}$ can be differentiated:
Those two programs do not behave in the same way when ${{s}\not\equiv {t}}$ . Hence, only provided that all occurrences of put in a program are replaced by ${{put}_{{R}}}$ can we simulate local-state semantics with global-state semantics. This has been articulated in the proof by the composition ${{h}_{{Global}}{\circ }{local2global}}$ : there is no room between the replacement by local2global and the interpretation with ${{h}_{{Global}}}$ to add plain put operations. The global replacement requirement also manifests itself in the proof, in the form of the fusion-post’ rule rather than the more widely used fusion-post rule.
5 Modelling nondeterminism with state
In the previous section, we have translated the local-state semantics, a high-level combination of the state and nondeterminism effects, to the global-state semantics, a low-level combination of the state and nondeterminism effects. In this section, we further translate the resulting nondeterminism component, which is itself a relatively high-level effect, to a lower-level implementation with the state effect. Our translation coincides with the fact that, while nondeterminism is typically modelled using the List monad, many efficient nondeterministic systems, such as Prolog, use a low-level state-based implementation to implement the nondeterminism mechanism.
5.1 Simulating nondeterminism with state
The main idea of simulating nondeterminism with state is to explicitly manage
1. a list of the results found so far, and
2. a list of yet to be explored branches, which we call a stack.
This stack corresponds to the choicepoint stack in Prolog. When entering one branch, we can push other branches to the stack. When leaving the branch, we collect its result and pop a new branch from the stack to continue.
We define a new type S a consisting of the results and stack.
The branches in the stack are represented by computations in the form of free monads over the ${{State}}_{{F}}$ signature. We do not allow branches to use other effects here to show the idea more clearly. In Section 5.2, we will consider the more general case where branches can have any effects abstracted by a functor f.
For brevity, instead of defining a new stack effect capturing the stack operations like pop and push, we implement stack operations with the state effect. We define three auxiliary functions in Figure 2 to interact with the stack in ${{S}\;{a}}$ :
• The function ${{pop_S}}$ removes and executes the top element of the stack.
• The function ${{push_S}}$ pushes a branch into the stack.
• The function ${{append_S}}$ adds a result to the current results.
Now, everything is in place to define a simulation function ${{nondet2state_S}}$ that interprets nondeterministic programs represented by the free monad ${{Free}\;{Nondet}_{{F}}\;{a}}$ as state-wrapped programs represented by the free monad ${{Free}\;({State}}_{{F}\;({S}\;{a}))\;()}$ .
The generator of this handler records a new result and then pops the next branch from the stack and proceeds with it. Likewise, for failure the handler simply pops and proceeds with the next branch. For nondeterministic choices, the handler pushes the second branch on the stack and proceeds with the first branch. The ${{nondet2state_S}}$ implements the depth-first search strategy which is consistent with the implementation of ${{h}}_{{ND}}$ Footnote 3 .
To extract the final result from the S wrapper, we define the ${{extract_S}}$ function.
Finally, we define the function ${{run}_{{ND}}}$ which wraps everything up to handle a nondeterministic computation to a list of results. The state handler ${{h}^\prime_{{State}}}$ is defined in Section 3.1.
We have the following theorem showing the correctness of the simulation via the equivalence of the ${{run}_{{ND}}}$ function and the nondeterminism handler ${{h}}_{{ND}}$ defined in Section 3.1.
Theorem 2.
The proof can be found in Appendix 3.1. The main idea is again to use fold fusion. Consider the expanded form
Both ${{nondet2state_S}}$ and ${{h}}_{{ND}}$ are written as folds. We use the law fusion-post’ (3.3) to fuse the left-hand side into a single fold. Since the right-hand side is already a fold, to prove the equation we just need to check the components of the fold ${{h}}_{{ND}}$ satisfy the conditions of the fold fusion, i.e., the following two equations: For the latter, we only need to prove the following two equations:
where gen and alg are from the definition of ${{nondet2state_S}}$ , and ${{gen}_{{ND}}}$ and ${{alg}_{{ND}}}$ are from the definition of ${{h}}_{{ND}}$ .
5.2 Combining the simulation with other effects
The ${{nondet2state_S}}$ function only considers nondeterminism as the only effect. In this section, we generalize it to work in combination with other effects. One immediate benefit is that we can use it in together with our previous simulation ${{local2global}}$ in.Section 4.3.
Firstly, we need to augment the signature in the computation type for branches with an additional functor f for other effects. The computation type is essentially changed from ${Free}\;({State}_{F}\;{s})\;{a}$ to ${{Free}\;({State}}_{{F}\;{s}\mathrel{{:}{+}{:}}{f})\;{a}}$ . We define the state type ${{SS}\;{f}\;{a}}$ as follows:
We also modify the three auxiliary functions in Figure 2 to ${{pop}_{{SS}}}$ , ${{push}_{{SS}}}$ and ${{append}_{{SS}}}$ in Figure 3. They are almost the same as the previous versions apart from being adapted to use the new state-wrapper type ${{SS}\;{f}\;{a}}$ .
The simulation function nondet2state is also very similar to ${{nondet2state_S}}$ except for requiring a forwarding algebra fwd to deal with the additional effects in f.
The function ${{run}_{{ND+f}}}$ puts everything together: it translates the nondeterminism effect into the state effect and forwards other effects using ${{nondet2state}}$ , then handles the state effect using ${{h}_{{State}}}$ , and finally extracts the results from the final state using ${{extract}_{{SS}}}$ .
We have the following theorem showing that the simulation ${{run}_{{ND+f}}}$ is equivalent to the modular nondeterminism handler ${{h}_{{ND+f}}}$ in Section 3.2.
Theorem 3.
The proof proceeds essentially in the same way as in the non-modular setting. The main difference, due to the modularity, is an additional proof case for the forwarding algebra.
The full proof can be found in Appendix 3.2.
6 All in one
This section combines the results of the previous two sections to ultimately simulate the combination of nondeterminism and state with a single state effect.
6.1 Modelling two states with one state
When we combine the two simulation steps from the two previous sections, we end up with a computation that features two state effects. The first state effect is the one present originally, and the second state effect keeps track of the results and the stack of remaining branches to simulate the nondeterminism.
For a computation of type ${Free}\;({State}_{F}\;{s}_{1}\mathrel{{:}{+}{:}}{State}_{F}\;{s}_{2}\mathrel{{:}{+}{:}}{f})\;{a}$ that features two state effects, we can go to a slightly more primitive representation ${Free}\;({State}_{F}\;({s}_{1},{s}_{2})\mathrel{{:}{+}{:}}{f})\;{a}$ featuring only a single state effect that is a pair of the previous two states.
The handler states2state implements the simulation by projecting different get and put operations to different components of the pair of states.
We have the following theorem showing the correctness of states2state:
Theorem 4.
On the left-hand side, we write ${{h}_{{States}}}$ for the composition of two consecutive state handlers:
On the right-hand side, we use the isomorphism nest to mediate between the two different carrier types. The definition of nest and its inverse flatten are defined as follows:
where the isomorphism ${\alpha^{-1}}$ and its inverse ${\alpha}$ rearrange a nested tuple
The proof of Theorem 4 can be found in Appendix 4. Theorem 4 has two function compositions on the right-hand side, which would require using fusion twice, resulting in a complicated handler. To avoid this complexity, we show the correctness of the isomorphism of nest and flatten, and prove the following equation:
The following commuting diagram summarizes the simulation.
6.2 Putting everything together
We have defined three translations for encoding high-level effects as low-level effects.
• The function local2global simulates the high-level local-state semantics with global-state semantics for the nondeterminism and state effects (Section 4).
• The function nondet2state simulates the high-level nondeterminism effect with the state effect (Section 5).
• The function states2state simulates multiple state effects with a single state effect (Section 6.1).
Combining them, we can encode the local-state semantics for nondeterminism and state with just one state effect. The ultimate simulation function simulate is defined as follows:
Similar to the ${{extract}_{{SS}}}$ function in Section 5.2, we use the extract function to get the final results from the final state.
Figure 4 illustrates each step of this simulation.
In the simulate function, we first use our three simulations local2global, nondet2state and states2state to interpret the local-state semantics for state and nondeterminism in terms of only one state effect. Then, we use the handler ${{h}_{{State}}}$ to interpret the state effect into a state monad transformer. Finally, we use the function extract to get the final results.
We have the following theorem showing that the simulate function exactly behaves the same as the local-state semantics given by ${{h}_{{Local}}}$ .
Theorem 5.
The proof can be found in Appendix 5.
We provide a more compact and direct definition of simulate by fusing all the consecutive steps into a single handler:
The common carrier of the above algebras ${alg}_{1}\mathbin{\#}{alg}_{2}\mathbin{\#}{fwd}$ is ${{Comp}\;{s}\;{f}\;{a}}$ . This is a computation that takes the current results, choicepoint stack and application state, and returns the list of all results. The first two inputs are bundled in the CP type.
N-queens with only one state. With simulate, we can implement the backtracking algorithm of the n-queens problem in Section 2.3 with only one state effect as follows.
7 Modelling local state with undo
Section 4.3 uses local2global to simulate local state with global state by replacing put with the state-restoring version ${{put}_{{R}}}$ . The ${{put}_{{R}}}$ operation makes the implicit state copying of the local-state semantics explicit in the global-state semantics. This copying can be rather costly if the state is big (e.g., a long array). It is especially wasteful when the modifications made to that state are small (e.g., a single entry in the array). Fortunately, lower-level effects present more opportunities for fine-grained optimization. In particular, we can exploit the global-state semantics to avoid copying the whole state. Instead, we only keep track of the modifications made to the state, and undo them when backtracking. This section formalizes that approach in terms of an alternative translation from the local-state semantics to the global-state semantics that incrementally records reversible state updates.
7.1 Reversible state updates
Our goal is to undo a state change without holding on to the old state. Instead, we should be able to recover the old state from the new state. However, knowing only the new state is usually not enough to accomplish this. We must also know “what update was applied to the old state that led to the new state”.
We reify the information about the update in a type u, which depends on the particular application at hand. For example, in the queens program of Section 2.3 we repeatedly update the state to place an additional queen on the board. Recall that a state s of type ${({Int},[ {Int}])}$ consists of the current column c and the partial solution sol, i.e., the rows of the already placed queens. Hence, the information we need to characterize an update is the row r of the queen to place in the current column, i.e., ${{u}\mathrel{=}{Int}}$ . The update itself is performed as ${{s}\mathbin{\oplus}{r}}$ , where
Now we can clearly recover the old state from the new state and the modification as follows:
Indeed, we clearly have ${({s}\mathbin{\oplus}{r})\mathbin{\ominus}{r}\mathrel{=}{s}}$ .
In general, we define a typeclass ${{Undo}\;{s}\;{u}}$ with two operations ${(\mathbin{\oplus})}$ and ${(\mathbin{\ominus})}$ to characterize reversible state updates. Here, s is the type of states and u is the type of updates.
Instances of Undo should satisfy the following law which says that ${\mathbin{\ominus}{x}}$ is a left inverse of ${\mathbin{\oplus}{x}}$ :
7.2 Reversible state update effect
For our optimized approach to work, we have to restrict the way the state is changed in local-state programs. We no longer allow arbitrary ${{put}\;{s'}}$ calls to change the implicit state. The only supported changes are of the form ${{put}\;({s}\mathbin{\oplus}{u})}$ where ${{s}}$ is the current state.
To enforce this requirement, we replace the general ${{get}}/{{put}}$ interface provided by MState with the more restricted interface of a new type class:
This MModify class has three operations: a mget operation that reads and returns the state (similar to the get operation of MState), an update u operation that updates the state with the reversible state change u, and a ${{restore}\;{u}}$ operations that reverses the update u. Note that only the mget and update operations are expected to be used by programmers; restore operations are automatically generated by the translation to the global-state semantics.
The three operations satisfy the following laws:
The first law for ${{mget}}$ corresponds to that for get. The second and third law, respectively, capture the impact of update and restore on mget. Finally, the fourth law expresses that restore undoes the effect of update.
We can rewrite the queens program to make use of this MModify type class. Compared to the MState-based version in Section 2.3, we only need to replace get with mget and ${{put}\;({s}\mathbin{\oplus}{r})}$ with update r.
Like we did for the state effects in Section 3.1, we define a new signature Modify F representing the syntax of modification-based state effects and implement the free monad ${{Free}\;({Modify}_{{F}}\;{s}\;{r}\mathrel{{:}{+}{:}}{f})}$ as an instance of ${{MModify}\;{s}\;{r}}$ .
Like the ${{h}_{{State}}}$ handler, the following ${{h}_{{Modify}}}$ handler maps this free monad to the StateT monad transformer, but now using the operations ${(\mathbin{\oplus})}$ and ${(\mathbin{\ominus})}$ provided by ${{Undo}\;{s}\;{r}}$ .
It is easy to check that the four laws hold contextually up to interpretation with ${{h}_{{Modify}}}$ .
Note that here we still use the StateT monad transformer and immutable states for the clarity of presentation and simplicity of proofs. The ${(\mathbin{\oplus})}$ and ${(\mathbin{\ominus})}$ operations also take immutable arguments. To be more efficient, we can use mutable states to implement in-place updates or use the technique of functional but in-place update (Lorenzen et al., Reference Lorenzen, Leijen and Swierstra2023). We leave them as future work.
Similar to Sections 4.1 and 4.2, the local-state and global-state semantics of ${{Modify}_{{F}}}$ and ${{Nondet}_{{F}}}$ are given by the following functions ${{h}_{{LocalM}}}$ and ${{h}_{{GlobalM}}}$ , respectively.
For example, the locate-state interpretation of ${{queens}_{{M}}}$ is obtained through:
7.3 Simulating local state with global state and undo
We can implement the translation from local-state semantics to global-state semantics for the modification-based state effects in a similar style to the translation local2global in Section 4.3. The translation ${{local2global_M}}$ still uses the mechanism of nondeterminism to restore previous state updates for backtracking. In Section 8, we will show a lower-level simulation of local-state semantics without relying on nondeterminism.
Compared to local2global, the main difference is that we do not need to copy and store the whole state. Instead, we store the update u and reverse the state update using ${{restore}\;{u}}$ in the second branch. The following theorem shows the correctness of local2global M .
Theorem 6. Given ${{Functor}\;{f}}$ and ${{Undo}\;{s}\;{u}}$ , the equation
holds for all programs ${{p}\mathbin{::}{Free}\;({Modify}_{{F}}\;{s}\;{u}\mathrel{{:}{+}{:}}{Nondet}_{{F}}\mathrel{{:}{+}{:}}{f})\;{a}}$ that do not use the operation ${{Op}\;({Inl}\;{MRestore}\;\_\,\_ )}$ . The proof of this theorem can be found in Appendix 6.
As a consequence of the theorem, we can get the desired local-state behaviour for ${{queens}_{{M}}}$ by simulating it with global-state semantics as follows:
8 Modelling local state with trail stack
In order to restore the previous state during backtracking, the approaches of Section 4.3 and Section 7 both introduce a new failing branch at every individual modification of the state. The Warren Abstract Machine (WAM) (Aït-Kaci, 1991) does this in a more efficient and lower-level way: it stores consecutive updates in a trail stack and then batch-processes them on backtracking. This avoids introducing any additional branches. This section first incorporates that trail-stack idea in the modification-based approach of Section 7. Then, by combining it with the earlier state-based simulation of nondeterminism, we get an overall simulation of local state in terms of two stacks, the choicepoint stack and the trail stack.
8.1 Simulating local state with global trail stack
Let us work out the trail stack idea in more detail. For that, we will need a second instance of the state effect. The primary one keeps track of the state featured in the local-state semantics. The new, secondary one keeps track of the trail stack. We can easily model this stack datastructure as a Haskell lists.
We store this stack in the secondary instance of the state effect, and we add and remove elements through the pushStack and popStack functions.
We store two types of entries in the trail stack. The first types are the reversible updates u (see Section 7) that we apply to the primary state. The second types are markers that mark the end of a batch on the trail stack; we represent these with the unit type (). Hence, we use the sum type ${{Either}\;{u}\;()}$ to use both as elements of the trail stack.
When we enter a left branch, we push a ${{Right}\;()}$ marker on the trail stack. For every state update u we perform in that branch, we push the corresponding ${{Left}\;{u}}$ entry on top of the marker. When we backtrack to the right branch, we unwind the trail stack down to the marker and reverse all updates along the way. This process is known as “untrailing”.
With the above trail stack functionality in place, the following translation function local2trail simulates the local-state semantics with global-state semantics by means of the trail stack.
As already informally explained above, this translation function 1) pushes updates to the trail tack, 2) pushes a marker to the trail stack in the left branch of a choice, and 3) untrails in the right branch. All other operations remain as is.
To ensure that pushStack and popStack access the secondary, trail-stack state in the above translation, we also need to define the following instance of MState.
Now, we can combine the simulation local2trail with the global-state semantics provided by ${{h}_{{GlobalM}}}$ , and handle the trail stack at the end.
The following theorem establishes the correctness of ${{h}_{{GlobalT}}}$ with respect to the local-state semantics given by ${{h}_{{Local}}}$ defined in Section 4.1.
Theorem 7. Given ${{Functor}\;{f}}$ and ${{Undo}\;{s}\;{u}}$ , the equation
holds for all programs ${{p}\mathbin{::}{Free}\;({Modify}_{{F}}\;{s}\;{u}\mathrel{{:}{+}{:}}{Nondet}_{{F}}\mathrel{{:}{+}{:}}{f})\;{a}}$ that do not use the operation ${{Op}\;({Inl}\;({MRestore}\;\_\,\_ ))}$ .
The proof can be found in Appendix 7; it uses the same fold fusion strategy as in the proofs of other theorems.
8.2 Putting everything together, again
We can further combine the local2trail simulation with the nondet2state simulation of nondeterminism from Section 5 and the state2state simulation of multiple states from Section 6.1. The resulting simulation encodes the local-state semantics with one modification-based state and two stacks, a choicepoint stack generated by nondet2state and a trail stack generated by local2trail. This has a close relationship to the WAM of Prolog. The modification-based state models the state of the program. The choicepoint stack stores the remaining branches to implement the nondeterministic searching. The trail stack stores the previous state updates to implement the backtracking.
The combined simulation function ${{simulate_T}}$ is defined as follows:
It uses the auxiliary function extractT to get the final results and to reorder the signatures. Note that the initial state used by extractT is ${({SS}\;[ ]\;[ ],{Stack}\;[ ])}$ , which contains an empty results list, an empty choicepoint stack, and an empty trail stack.
Figure 5 illustrates each step of this simulation. The state type ${{St}\;{s}\;{u}\;{f}\;{a}}$ is defined as ${{SS}\;({Modify}_{{F}}\;{s}\;{u}\mathrel{{:}{+}{:}}{State}}_{{F}\;({Stack}\;({Either}\;{u}\;()))\mathrel{{:}{+}{:}}{f})\;{a}}$ .
In the ${{simulate_T}}$ function, we first use our three simulations local2trail, nondet2state and states2state (together with some reordering of signatures) to interpret the local-state semantics for state and nondeterminism in terms of a modification-based state and a general state containing two stacks. Then, we use the handler ${{h}_{{Modify}}}$ to interpret the modification-based state effect, and use the handler ${{h}_{{State}}}$ to interpret the two stacks. Finally, we use the function extractT to get the final results.
As in Section 6.2, we can also fuse ${{simulate_T}}$ into a single handler.
Here, the carrier type of the algebras is ${{Comp}\;{f}\;{a}\;{s}\;{u}}$ . It differs from that of ${{simulate_F}}$ in that it also takes a trail stack as an input.
N-queens with two stacks. With ${{simulate_T}}$ , we can implement the backtracking algorithm of the n-queens problem with one modification-based state and two stacks.
9 Related work
There are various related works.
9.1 Prolog
Prolog is a prominent example of a system that exposes nondeterminism with local state to the user, but is itself implemented in terms of a single, global state.
Warren abstract machine. The folklore idea of undoing modifications upon backtracking is a key feature of many Prolog implementations, in particular those based on the Warren Abstract Machine (WAM) Warren (Reference Warren1983); Aït-Kaci (1991). The WAM’s global state is the program heap and Prolog programs modify this heap during unification only in a very specific manner: following the union-find algorithm, they overwrite cells that contain self-references with pointers to other cells. Undoing these modifications only requires knowledge of the modified cell’s address, which can be written back in that cell during backtracking. The WAM has a special stack, called the trail stack, for storing these addresses, and the process of restoring those cells is called untrailing.
WAM derivation and correctness. Several authors have studied the derivation of the WAM from a specification of Prolog, and its correctness.
Börger and Rosenzweig (Reference Börger, Rosenzweig, Beierle and Plümer1995) start from an operational semantics of Prolog based on derivation trees and refine this in successive steps to the WAM. Their approach was later mechanized in Isabelle/HOL by Pusch (Reference Pusch1996). Pirog and Gibbons (Reference Pirog and Gibbons2011) sketch how the WAM can be derived from a Prolog interpreter following the functional correspondence between evaluator and abstract machine Ager et al. (Reference Ager, Danvy and Midtgaard2005).
Neither of these approaches is based on an abstraction of effects that separates them from other aspects of Prolog.
The 4-port box model. While trailing happens under the hood, there is a folklore Prolog programming pattern for observing and intervening at different point in the control flow of a procedure call, known as the 4-port box model. In this model, upon the first entrance of a Prolog procedure it is called; it may yield a result and exits; when the subsequent procedure fails and backtracks, it is asked to redo its computation, possibly yielding the next result; finally it may fail. Given a Prolog procedure p implemented in Haskell, the following program prints debugging messages when each of the four ports are used:
This technique was applied in the monadic setting by Hinze (Reference Hinze1996), and it has been our inspiration for expressing the state restoration with global state.
Functional models of prolog. Various authors have modelled (aspects of) Prolog in functional programming languages, often using monads to capture nondeterminism and state effects. Notably, Spivey and Seres (Reference Spivey and Seres1999) develop an embedding of Prolog in Haskell.
Most attention has gone towards modelling the nondeterminism or search aspect of Prolog, with various monads and monad transformers being proposed (Hinze, Reference Hinze2000; Kiselyov et al., Reference Kiselyov, Shan, Friedman and Sabry2005). Notably, Schrijvers et al. (Reference Schrijvers, Wu, Desouter and Demoen2014) shows how Prolog’s search can be exposed with a free monad and manipulated using handlers.
None of these works consider mapping high-level to low-level representations of the effects.
9.2 Reasoning about side effects
There are many works on reasoning and modelling side effects. Here, we cover those that have most directly inspired this paper.
Axiomatic reasoning. Gibbons and Hinze (Reference Gibbons and Hinze2011) proposed to reason axiomatically about programs with effects and provided an axiomatic characterization of local state semantics. Our earlier work in Pauwels et al. (Reference Pauwels, Schrijvers and Mu2019) was directly inspired by their work: we introduced an axiomatic characterization of global state and used axiomatic reasoning to prove handling local state with global state correct. We also provided models that satisfy the axioms, whereas their paper mistakenly claims that one model satisfies the local state axioms and that another model is monadic. This paper is an extension of Pauwels et al. (Reference Pauwels, Schrijvers and Mu2019), but notably, we depart from the axiomatic reasoning approach; instead we use proof techniques based on algebraic effects and handlers.
Algebraic effects. Our formulation of implementing local state with global state is directly inspired by the effect handlers approach of Plotkin and Pretnar (Reference Plotkin and Pretnar2009). By making the free monad explicit our proofs benefit directly from the induction principle that Bauer and Pretnar established for effect handler programs. While Lawvere theories were originally Plotkin’s inspiration for studying algebraic effects, the effect handlers community has for a long time paid little attention to them. Yet, Lukšič and Pretnar (Reference Lukšič and Pretnar2020) have investigated a framework for encoding axioms or effect theories in the type system: the type of an effectful function declares the operators used in the function, as well as the equalities that handlers for these operators should comply with. The type of a handler indicates which operators it handles and which equations it complies with. This allows expressing at the type level that a handler reduces a higher-level effect to a lower-level one.
Wu and Schrijvers (Reference Wu and Schrijvers2015) first presented fusion as a technique for optimizing compositions of effect handlers. They use a specific form of fusion known as fold–build fusion or short-cut fusion (Gill et al., Reference Gill, Launchbury and Peyton Jones1993). To enable this kind of fusion they transform the handler algebras to use the codensity monad as their carrier. Their approach is not directly usable because it does not fuse non-handler functions, and we derive simpler algebras (not obfuscated by the codensity monad) than those they do.
More recently, Yang and Wu (Reference Yang and Wu2021) have used the fusion approach of Wu and Schrijvers (Reference Wu and Schrijvers2015) (but with the continuation monad rather than the codensity monad) for reasoning; they remark that, although handlers are composable, the semantics of these composed handlers are not always obvious and that determining the correct order of composition to arrive at a desired semantics is nontrivial. They propose a technique based on modular handlers (Schrijvers et al., Reference Schrijvers, Piróg, Wu and Jaskelioff2019), which considers conditions under which the fusion of these modular handlers respect not only the laws of each of the handler’s algebraic theories but also additional interaction laws. Using this technique they provide succinct proofs of the correctness of local state handlers, constructed from a fusion of state and nondeterminism handlers.
Earlier versions. This paper refines and much expands on two earlier works of the last author.
Pauwels et al. (Reference Pauwels, Schrijvers and Mu2019) have the same goal as Section 4: it uses the state-restoring version of put to simulate local state with global state. It differs from this work in that it relies on an axiomatic (i.e., law-based), as opposed to handler-based, semantics for local and global state. This means that handler fusion cannot be used as a reasoning technique. Moreover, it uses a rather heavy-handed syntactic approach to contextual equivalence, and it assumes that no other effects are invoked.
Another precursor is the work of Seynaeve et al. (Reference Seynaeve, Pauwels and Schrijvers2020), which establishes similar results as those in Section 5.1. However, instead of generic definitions for the free monad and its fold, they use a specialized free monad for nondeterminism and ordinary recursive functions for handling. As a consequence, their proofs use structural induction rather than fold fusion. Furthermore, they did not consider other effects either.
10 Conclusion and future work
We studied the simulations of higher-level effects with lower-level effects for state and nondeterminism. We started with the translation from the local-state semantics of state and nondeterminism to the global-state semantics. Then, we further showed how to translate nondeterminism to state (a choicepoint stack), and translate multiple state effects into one state effect. Combining these results, we can simulate the local-state semantics, a high-level programming abstraction, with only one low-level state effect. We also demonstrated that we can simulate the local-state semantics using a trail stack in a similar style to the Warren Abstract Machine of Prolog. We define the effects and their translations with algebraic effects and effect handlers, respectively. These are implemented as free monads and folds in Haskell. The correctness of all these translations has been proved using the technique of program calculation, especially using the fusion properties.
In future work, we would like to explore the potential optimizations enabled by mutable states. Mutable states fit the global-state semantics naturally. With mutable states, we can implement more efficient state update and restoration operations for the simulation ${{local2global_M}}$ (Section 7), as well as more efficient implementations of the choicepoint stacks and trail stacks used by the simulations nondet2state (Section 5.2) and local2trail (Section 8), respectively. We would also like to consider the low-level simulations of other control-flow constructs used in logical programming languages such as Prolog’s cut operator for trimming the search space. Since operators like cut are usually implemented as scoped or higher order effects (Piróg et al., Reference Piróg, Schrijvers, Wu and Jaskelioff2018; Wu et al., Reference Wu, Schrijvers and Hinze2014; Yang et al., Reference Yang, Paviotti, Wu, van den Berg and Schrijvers2022; van den Berg and Schrijvers, Reference van den Berg and Schrijvers2023), we would have to adapt our approach accordingly.
Conflicts of Interest
None.
1 Proofs for get laws in local-state semantics
In this section, we prove two equations about the interaction of nondeterminism and state in the local-state semantics.
Equation (4.3): ${{get}>\!\!>{\varnothing}\mathrel{=}{\varnothing}}$
Equation (4.4): ${get}>\!\!>\!\!=(\lambda {x}\to {k}_{1}\;{x}\mathbin{\left[\!\right]}{k}_{2}\;{x})\mathrel{=}({get}>\!\!>\!\!={k}_{1})\mathbin{\left[\!\right]}({get}>\!\!>\!\!={k}_{2})$
2 Proofs for modelling local state with global state
This section proves the following theorem in Section 4.3.
Theorem 1.
Preliminary. It is easy to see that ${{run}_{{StateT}}{\circ }{h}_{{State}}}$ can be fused into a single fold defined as follows:
For brevity, we use ${{h}_{{State1}}}$ to replace ${{run}_{{StateT}}{\circ }{h}_{{State}}}$ in the following proofs.
2.1 Main proof structure
The main theorem we prove in this section is
Theorem 8. ${{h}_{{Global}}{\circ }{local2global}\mathrel{=}{h}_{{Local}}}$
Proof Both the left-hand side and the right-hand side of the equation consist of function compositions involving one or more folds. We apply fold fusion separately on both sides to contract each into a single fold:
We approach this calculationally. That is to say, we do not first postulate definitions of the unknowns above ( ${{alg}}_{{{LHS}}^{{S}}}$ and so on) and then verify whether the fusion conditions are satisfied. Instead, we discover the definitions of the unknowns. We start from the known side of each fusion condition and perform case analysis on the possible shapes of input. By simplifying the resulting case-specific expression, and pushing the handler applications inwards, we end up at a point where we can read off the definition of the unknown that makes the fusion condition hold for that case.
Finally, we show that both folds are equal by showing that their corresponding parameters are equal:
A noteworthy observation is that, for fusing the left-hand side of the equation, we do not use the standard fusion rule:
where ${{local2global}\mathrel{=}{fold}\;{Var}\;{alg}}$ . The problem is that we will not find an appropriate ${{alg'}}$ such that ${{alg'}\;({fmap}\;{h}_{{Global}}\;{t})}$ restores the state for any t of type $({State}_{F}\;{s}\mathrel{{:}{+}{:}}{Nondet}_{F}\mathrel{{:}{+}{:}}{f})\;({Free}\;({State}_{F}\;{s}\mathrel{{:}{+}{:}}{NonDetF}\mathrel{{:}{+}{:}}{f})\;{a})$ .
Fortunately, we do not need such an ${{alg'}}$ . As we have already pointed out, we can assume that the subterms of t have already been transformed by local2global, and thus all occurrences of Put appear in the ${{put}_{{R}}}$ constellation.
We can incorporate this assumption by using the alternative fusion rule:
The additional ${{fmap}\;{local2global}}$ in the condition captures the property that all the subterms have been transformed by local2global.
In order to not clutter the proofs, we abstract everywhere over this additional ${{fmap}\;{local2global}}$ application, except in the one place where we need it. That is the appeal to the key lemma:
This expresses that the syntactic transformation local2global makes sure that, despite any temporary changes, the computation t restores the state back to its initial value.
We elaborate each of these steps below.
2.2 Fusing the right-hand side
We calculate as follows:
This last step is valid provided that the fusion conditions are satisfied:
We calculate for the first fusion condition:
We conclude that the first fusion condition is satisfied by
The second fusion condition decomposes into two separate conditions:
We calculate for the first subcondition:
case ${{t}\mathrel{=}{Get}\;{k}}$
case ${{t}\mathrel{=}{Put}\;{s}\;{k}}$
We conclude that the first subcondition is met by taking:
The second subcondition can be split up in two further subconditions:
For the first of these, we calculate:
We split on op:
case ${{op}\mathrel{=}{Fail}}$
case ${{op}\mathrel{=}{Or}\;{p}\;{q}}$
From this we conclude that the definition of ${{alg}}_{{{RHS}}^{{ND}}}$ should be:
For the last subcondition, we calculate:
From this we conclude that the definition of ${{fwd}}_{{{RHS}}}$ should be:
2.3 Fusing the left-hand side
We proceed in the same fashion with fusing left-hand side, discovering the definitions that we need to satisfy the fusion condition.
We calculate as follows:
This last step is valid provided that the fusion conditions are satisfied:
We calculate for the first fusion condition:
We conclude that the first fusion condition is satisfied by
We can split the second fusion condition in three subconditions:
Let’s consider the first subconditions. It has two cases:
case ${{op}\mathrel{=}{Get}\;{k}}$
case ${{op}\mathrel{=}{Put}\;{s}\;{k}}$
We conclude that this fusion subcondition holds provided that:
Let’s consider the second subcondition. It has also two cases:
case ${{op}\mathrel{=}{Fail}}$
case ${{op}\mathrel{=}{Or}\;{p}\;{q}}$
We conclude that this fusion subcondition holds provided that:
Finally, the last subcondition:
We conclude that this fusion subcondition holds provided that:
2.4 Equating the fused sides
We observe that the following equations hold trivially.
Therefore, the main theorem holds.
2.5 Key lemma: State restoration
The key lemma is the following, which guarantees that local2global restores the initial state after a computation.
Lemma 1 (State is Restored).
Proof The proof proceeds by structural induction on t.
case ${{t}\mathrel{=}{Var}\;{y}}$
case ${{t}\mathrel{=}{Op}\;({Inl}\;({Get}\;{k}))}$
case ${{t}\mathrel{=}{Op}\;({Inr}\;({Inl}\;{Fail}))}$
case ${{t}\mathrel{=}{Op}\;({Inl}\;({Put}\;{t}\;{k}))}$
case ${{t}\mathrel{=}{Op}\;({Inr}\;({Inl}\;({Or}\;{p}\;{q})))}$
case ${{t}\mathrel{=}{Op}\;({Inr}\;({Inr}\;{y}))}$
2.6 Auxiliary lemmas
The derivations above make use of two auxiliary lemmas. We prove them here.
Lemma 2 (Naturality of ${(\mathbin{\$}{s})}$ ).
Proof
Lemma 3.
Proof
Lemma 4 (Distributivity of ${{h}_{{State1}}}$ ).
Proof The proof proceeds by induction on p.
case ${{p}\mathrel{=}{Var}\;{x}}$
case ${{p}\mathrel{=}{Op}\;({Inl}\;({Get}\;{p}))}$
case ${{p}\mathrel{=}{Op}\;({Inl}\;({Put}\;{t}\;{p}))}$
case ${{p}\mathrel{=}{Op}\;({Inr}\;{y})}$
import Data.Bitraversable (Bitraversable)
3 Proofs for modelling nondeterminism with state
In this section, we prove the theorems in Section 5.
3.1 Only nondeterminism
This section proves the following theorem in Section 5.1.
Proof. We start with expanding the definition of ${{run}_{{ND}}}$ :
Both ${{nondet2state_S}}$ and ${{h}}_{{ND}}$ are written as folds. We use the fold fusion law fusion-post’ (3.3) to fuse the left-hand side. Since the right-hand side is already a fold, to prove the equation we just need to check the components of the fold ${{h}}_{{ND}}$ satisfy the conditions of the fold fusion, i.e., the following two equations:
For brevity, we omit the last common part ${{fmap}\;{nondet2state_S}}$ of the second equation in the following proof. Instead, we assume that the input is in the codomain of ${{fmap}\;{nondet2state_S}}$ .
For the first equation, we calculate as follows:
For the second equation, we proceed with a case analysis on the input.
case Fail
case ${{Or}\;{p}\;{q}}$
In the above proof we have used several lemmas. Now we prove them.
Lemma 5 (pop-extract)
holds for all p in the codomain of the function ${{nondet2state_S}}$ .
Proof We prove this lemma by structural induction on ${{p}\mathbin{::}{Free}\;({State}}_{{F}\;({S}\;{a}))\;()}$ . For each inductive case of p, we not only assume this lemma holds for its sub-terms (this is the standard induction hypothesis) but also assume Theorem 2 holds for p and its sub-terms. This is sound because in the proof of Theorem 2, for ${({extract_S}{\circ }{h}_{{State}^\prime}{\circ }{nondet2state_S})\;{p}\mathrel{=}{h}_{{ND}}\;{p}}$ , we only apply Lemma 5 to the sub-terms of p, which is already included in the induction hypothesis so there is no circular argument.
Since we assume Theorem 2 holds for p and its sub-terms, we can use several useful properties proved in the sub-cases of the proof of Theorem 2. We list them here for easy reference:
• extract-gen: ${{extract_S}{\circ }{h}_{{State}^\prime}{\circ }{gen}\mathrel{=}{\eta}}$
• extract-alg1: ${{extract_S}\;({h}_{{State}^\prime}\;({alg}\;{Fail}))\mathrel{=}[ ]}$
• extract-alg2: ${{extract_S}\;({h}_{{State}^\prime}\;({alg}\;({Or}\;{p}\;{q})))\mathrel{=}{extract_S}\;({h}_{{State}^\prime}\;{p})+\!\!+{extract_S}\;({h}_{{State}^\prime}\;{q})}$
We proceed by structural induction on p. Note that for all p in the codomain of ${{nondet2state_S}}$ , it is either generated by the gen or the alg of ${{nondet2state_S}}$ . Thus, we only need to prove the following two equations where ${{p}\mathrel{=}{gen}\;{x}}$ or ${{p}\mathrel{=}{alg}\;{x}}$ and x is in the codomain of ${{fmap}\;{nondet2state_S}}$ .
1. ${{run}_{{State}}\;({h}_{{State}^\prime}\;({gen}\;{x}))\;({S}\;{xs}\;{stack})\mathrel{=}{run}_{{State}}\;({h}_{{State}^\prime}\;{pop_S})\;({S}\;({xs}+\!\!+{extract_S}\;({h}_{{State}^\prime}\;({gen}\;{x})))\;{stack})}$
2. ${{run}_{{State}}\;({h}_{{State}^\prime}\;({alg}\;{x}))\;({S}\;{xs}\;{stack})\mathrel{=}{run}_{{State}}\;({h}_{{State}^\prime}\;{pop_S})\;({S}\;({xs}+\!\!+{extract_S}\;({h}_{{State}^\prime}\;({alg}\;{x})))\;{stack})}$
For the case ${{p}\mathrel{=}{gen}\;{x}}$ , we calculate as follows:
For the case ${{p}\mathrel{=}{alg}\;{x}}$ , we proceed with a case analysis on x.
case Fail
case ${Or}\;{p}_{1}\;{p}_{2}$
The following four lemmas characterise the behaviours of stack operations.
Lemma 6 (evaluation-append).
Proof
Lemma 7 (evaluation-pop1).
Proof
Lemma 8 (evaluation-pop2).
Proof
Lemma 9 (evaluation-push).
Proof
3.2 Combining with other effects
This section proves the following theorem in Section 5.2.
Theorem 3.
Proof The proof is very similar to that of Theorem 2 in Appendix 3.1.
We start with expanding the definition of ${{run}_{{ND+f}}}$ :
We use the fold fusion law fusion-post’ (3.3) to fuse the left-hand side. Since the right-hand side is already a fold, to prove the equation we just need to check the components of the fold ${{h}}_{{ND}}$ satisfy the conditions of the fold fusion. The conditions can be splitted into the following three equations:
For brevity, we omit the last common part ${{fmap}\;{nondet2state_S}}$ of the second equation in the following proof. Instead, we assume that the input is in the codomain of ${{fmap}\;{nondet2state_S}}$ .
For the first equation, we calculate as follows:
For the second equation, we proceed with a case analysis on the input.
case Fail
case ${{Inl}\;({Or}\;{p}\;{q})}$
For the last equation, we calculate as follows:
In the above proof we have used several lemmas. Now we prove them.
Lemma 10 (pop-extract of).
holds for all p in the codomain of the function nondet2state.
Proof The proof structure is similar to that of Lemma 5. We prove this lemma by structural induction on ${{p}\mathbin{::}{Free}\;({State}}_{{F}\;({SS}\;{f}\;{a})\mathrel{{:}{+}{:}}{f})\;()}$ . For each inductive case of p, we not only assume this lemma holds for its sub-terms (this is the standard induction hypothesis) but also assume Theorem 3 holds for p and its sub-terms. This is sound because in the proof of Theorem 3, for ${({extract}_{{SS}}{\circ }{h}_{{State}}{\circ }{nondet2state})\;{p}\mathrel{=}{h}_{{ND+f}}\;{p}}$ , we only apply Lemma 10 the sub-terms of p, which is already included in the induction hypothesis so there is no circular argument.
Since we assume Theorem 3 holds for p and its sub-terms, we can use several useful properties proved in the sub-cases of the proof of Theorem 3. We list them here for easy reference:
• extract-gen-ext: ${{extract}_{{SS}}{\circ }{h}_{{State}}{\circ }{gen}\mathrel{=}{Var}{\circ }{\eta}}$
• extract-alg1-ext: ${{extract}_{{SS}}\;({h}_{{State}}\;({alg}\;{Fail}))\mathrel{=}{Var}\;[ ]}$
• extract-alg2-ext: ${{extract}_{{SS}}\;({h}_{{State}}\;({alg}\;({Or}\;{p}\;{q})))\mathrel{=}{liftM2}\;(+\!\!+)\;({extract}_{{SS}}\;({h}_{{State}}\;{p}))}$ ${({extract}_{{SS}}\;({h}_{{State}}\;{q}))}$
• extract-fwd ${{extract}_{{SS}}\;({h}_{{State}}\;({fwd}\;{y}))\mathrel{=}{fwd}_{{ND+f}}\;({fmap}\;({extract}_{{SS}}{\circ }{h}_{{State}})\;{y})}$
We proceed by structural induction on p. Note that for all p in the codomain of nondet2state, it is either generated by the gen, alg, or fwd of nondet2state. Thus, we only need to prove the following three equations, where x is in the codomain of ${{fmap}\;{nondet2state_S}}$ and ${{p}\mathrel{=}{gen}\;{x}}$ , ${{p}\mathrel{=}{alg}\;{x}}$ , and ${{p}\mathrel{=}{fwd}\;{x}}$ , respectively.
1.
2.
3.
For the case ${{p}\mathrel{=}{gen}\;{x}}$ , we calculate as follows:
For the case ${{p}\mathrel{=}{alg}\;{x}}$ , we proceed with a case analysis on x.
case Fail
case ${Or}\;{p}_{1}\;{p}_{2}$
For the case ${{p}\mathrel{=}{fwd}\;{x}}$ , we proceed with a case analysis on x.
The following four lemmas characterize the behaviours of stack operations.
Lemma 11 (evaluation-append-ext).
Proof
Lemma 12 (evaluation-pop1-ext).
Proof
Lemma 13 (evaluation-pop2-ext).
Proof
Lemma 14 (evaluation-push-ext).
Proof
4 Proofs for modelling two states with one state
In this section, we prove the following theorem in Section 6.1.
Theorem 4.
Proof. Instead of proving it directly, we show the correctness of the isomorphism of nest and flatten and prove the following equation:
It is obvious that ${\alpha}$ and ${\alpha^{-1}}$ form an isomorphism, i.e., ${\alpha{\circ }\alpha^{-1}\mathrel{=}{id}}$ and ${\alpha^{-1}{\circ }\alpha\mathrel{=}{id}}$ . We show that nest and flatten form an isomorphism by calculation. For all ${t}\mathbin{::}{StateT}\;{s}_{1}\;({StateT}\;{s}_{2}\;({Free}\;{f}))\;{a}$ , we show that ${({nest}{\circ }{flatten})\;{t}\mathrel{=}{t}}$ .
For all ${t}\mathbin{::}{StateT}\;({s}_{1},{s}_{2})\;({Free}\;{f})\;{a}$ , we show that ${({flatten}{\circ }{nest})\;{t}\mathrel{=}{t}}$ .
Then, we first calculate the LHS ${flatten}{\circ}{h}_{States}$ into one function ${{h}^\prime_{{States}}}$ which is defined as
For all ${t}\mathbin{::}{Free}\;({State}_{F}\;{s}_{1}\mathrel{{:}{+}{:}}{State}_{F}\;{s}_{2}\mathrel{{:}{+}{:}}{f})\;{a}$ , we show the equation ${({flatten}{\circ }{h}_{{States}})\;{t}\mathrel{=}{h}_{{States}^\prime}\;{t}}$ by the following calculation.
Now we only need to show that for any input ${t}\mathbin{::}{Free}\;({State}_{F}\;{s}_{1}\mathrel{{:}{+}{:}}{State}_{F}\;{s}_{2}\mathrel{{:}{+}{:}}{f})\;{a}$ , the equation ${{h}_{{States}^\prime}\;{t}\mathrel{=}({h}_{{State}}{\circ }{states2state})\;{t}}$ holds. Note that both sides use folds. We can proceed with either fold fusion, as what we have done in the proofs of other theorems, or a direct structural induction on the input t. Although using fold fusion makes the proof simpler than using structural induction, we opt for the latter here to show that the our methods of defining effects and translations based on algebraic effects and handlers also work well with structural induction.
case ${{t}\mathrel{=}{Var}\;{x}}$
case ${{t}\mathrel{=}{Op}\;({Inl}\;({Get}\;{k}))}$
Induction hypothesis: ${{h}_{{States}^\prime}\;({k}\;{s})\mathrel{=}({h}_{{State}}{\circ }{states2state})\;({k}\;{s})}$ for any s.
case ${{t}\mathrel{=}{Op}\;({Inl}\;({Put}\;{s}\;{k}))}$
Induction hypothesis: ${{h}_{{States}^\prime}\;{k}\mathrel{=}({h}_{{State}}{\circ }{states2state})\;{k}}$ .
case ${{t}\mathrel{=}{Op}\;({Inr}\;({Inl}\;({Get}\;{k})))}$
Induction hypothesis: ${{h}_{{States}^\prime}\;({k}\;{s})\mathrel{=}({h}_{{State}}{\circ }{states2state})\;({k}\;{s})}$ for any s.
case ${{t}\mathrel{=}{Op}\;({Inr}\;({Inl}\;({Put}\;{s}\;{k})))}$
Induction hypothesis: ${{h}_{{States}^\prime}\;{k}\mathrel{=}({h}_{{State}}{\circ }{states2state})\;{k}}$ .
case ${{t}\mathrel{=}{Op}\;({Inr}\;({Inr}\;{y}))}$
Induction hypothesis: ${{h}_{{States}^\prime}\;{y}\mathrel{=}({h}_{{State}}{\circ }{states2state})\;{y}}$ .
5 Proofs for the all in one simulation
In this section, we prove the correctness of the final simulation in Section 6.2.
Theorem 5.
Proof We calculate as follows, using all our three previous theorems Theorem 1, Theorem 3, Theorem 4, and an auxiliary lemma Lemma 15.
Lemma 15
Proof As shown in Appendix 4, we can combine ${{flatten}{\circ }{h}_{{States}}}$ into one function ${{h}_{{States}^\prime}}$ defined as follows:
Then we show that for any input ${t}\mathbin{::}{Free}\;({State}_{F}\;({SS}\;({State}_{F}\;{s}\mathrel{{:}{+}{:}}{f})\;{a})\mathrel{{:}{+}{:}}({State}_{F}\;{s}\mathrel{:}{+}{:}{f}))\;()$ , we have ${({extract}{\circ }{h}_{{States}^\prime})\;{t}\mathrel{=}({fmap}\;({fmap}\;{fst}){\circ }{run}_{{StateT}}{\circ }{h}_{{State}}{\circ }{extract}_{{SS}}{\circ }{h}_{{State}})\;{t}}$ via the following calculation.
Note that in the above calculation, we use the parametricity (Reynolds, Reference Reynolds1983; Wadler, Reference Wadler1989) of free monads which is formally stated as follows:
for any ${{g}\mathbin{::}\forall {a}\forall {\circ}{Free}\;{F}\;{a}\to {Free}\;{G}\;{a}}$ with two functors F and G.
6 Proofs for modelling local state with undo
In this section, we prove the following theorem in Section 7.
Theorem 6. Given Functor f and Undo s u, the equation
holds for all programs a that do not use the operation Op ().
The proof structure is very similar to that in Appendix 2. We start with the following preliminary fusion.
Preliminary. It is easy to see that ${run}_{StateT}{\circ}{h}_{Modify}$ can be fused into a single fold defined as follows:
For brevity, we use ${h}_{Modify1}$ to replace ${run}_{StateT}{\circ}{h}_{Modify}$ in the following proofs.
6.1 Main proof structure
The main proof structure of Theorem 6 is as follows.
Proof Both the left-hand side and the right-hand side of the equation consist of function compositions involving one or more folds. We apply fold fusion separately on both sides to contract each into a single fold:
Finally, we show that both folds are equal by showing that their corresponding parameters are equal:
We elaborate each of these steps below.
6.2 Fusing the right-hand side
We calculate as follows:
This last step is valid provided that the fusion conditions are satisfied:
For the first fusion condition (1), we define ${{gen}}_{{{RHS}}}$ as follows
We show that (1) is satisfied by the following calculation.
By a straightforward case analysis on the two cases Inl and Inr, the second fusion condition (2) decomposes into two separate conditions:
For the subcondition (3), we define ${{alg}}_{{{RHS}}^{{S}}}$ as follows.
We prove its correctness by a case analysis on the shape of input ${{t}\mathbin{::}{State}}_{{F}\;{s}\;({s}\to {Free}\;({Nondet}_{{F}}\mathrel{{:}{+}{:}}{f})\;({a},{s}))}$ .
case ${{t}\mathrel{=}{Get}\;{k}}$
case ${{t}\mathrel{=}{MUpdate}\;{r}\;{k}}$
case ${{t}\mathrel{=}{MRestore}\;{r}\;{k}}$
For the subcondition (4), we define ${{alg}}_{{{RHS}}^{{ND}}}$ as follows.
To show its correctness, given ${{op}\mathbin{::}{Nondet}_{{F}}\;({s}\to {Free}\;({Nondet}_{{F}}\mathrel{{:}{+}{:}}{f})\;({a},{s}))}$ with ${{Functor}\;{f}}$ , we calculate:
We proceed by a case analysis on op:
case ${{op}\mathrel{=}{Fail}}$
case ${{op}\mathrel{=}{Or}\;{p}\;{q}}$
For the last subcondition (5), we define ${{fwd}}_{{{RHS}}}$ as follows.
To show its correctness, given input ${{op}\mathbin{::}{f}\;({s}\to {Free}\;({Nondet}_{{F}}\mathrel{{:}{+}{:}}{f})\;({a},{s}))}$ , we calculate:
6.3 Fusing the left-hand side
We calculate as follows:
This last step is valid provided that the fusion conditions are satisfied:
The first subcondition (1) is met by
as established in the following calculation:
We can split the second fusion condition (2) in three subconditions:
For brevity, we omit the last common part ${{fmap}\;{local2global_M}}$ of these equations in the following proofs. Instead, we assume that the input is in the codomain of ${{fmap}\;{local2global_M}}$ . Also, we use the condition in Theorem 6 that the input program does not use the restore operation.
For the first subcondition (3), we can define ${{alg}}_{{{LHS}}^{{S}}}$ as follows.
We prove it by a case analysis on the shape of input ${{op}\mathbin{::}{Modify}_{{F}}\;{s}\;{r}\;({Free}\;({Modify}_{{F}}\;{s}\;{r}\mathrel{{:}{+}{:}}{Nondet}_{{F}}\mathrel{{:}{+}{:}}{f})\;{a})}$ . Note that we only need to consider the case that op is of form MGet k or ${{MUpdate}\;{r}\;{k}}$ where restore is also not used in the continuation k.
case ${{op}\mathrel{=}{MGet}\;{k}}$
case ${{op}\mathrel{=}{MUpdate}\;{r}\;{k}}$ From op is in the codomain of ${{fmap}\;{local2global_M}}$ we obtain k is in the codomain of ${{local2global_M}}$ .
For the second subcondition (4), we can define ${{alg}}_{{{LHS}}^{{ND}}}$ as follows.
We prove it by a case analysis on the shape of input ${{op}\mathbin{::}{Nondet}_{{F}}\;({Free}\;({Modify}_{{F}}\;{s}\;{r}\mathrel{{:}{+}{:}}{Nondet}_{{F}}\mathrel{{:}{+}{:}}{f})\;{a})}$ .
case ${{op}\mathrel{=}{Fail}}$
case ${{op}\mathrel{=}{Or}\;{p}\;{q}}$ From op is in the codomain of ${{fmap}\;{local2global_M}}$ , we obtain p and q are in the codomain of ${{local2global_M}}$ .
For the last subcondition (5), we can define ${{fwd}}_{{{LHS}}}$ as follows.
We prove it by the following calculation for input ${{op}\mathbin{::}{f}\;({Free}\;({Modify}_{{F}}\;{s}\;{r}\mathrel{{:}{+}{:}}{Nondet}_{{F}}\mathrel{{:}{+}{:}}{f})\;{a})}$ .
6.4 Equating the fused sides
We observe that the following equations hold trivially.
Therefore, the main theorem (Theorem 6) holds.
6.5 Key lemma: State restoration
Similar to Appendix 2, we have a key lemma saying that ${{local2global_M}}$ restores the initial state after a computation.
Lemma 16 (State is Restored).
For any program ${{p}\mathbin{::}{Free}\;({Modify}_{{F}}\;{s}\;{r}\mathrel{{:}{+}{:}}{Nondet}_{{F}}\mathrel{{:}{+}{:}}{f})\;{a}}$ that do not use the operation ${{OP}\;({Inl}\;{MRestore}\;\_\,\_ )}$ , we have
Proof The proof follows the same structure of Lemma 1. We proceed by induction on t.
case ${{t}\mathrel{=}{Var}\;{y}}$
case ${{t}\mathrel{=}{Op}\;({Inl}\;({MGet}\;{k}))}$
case ${{t}\mathrel{=}{Op}\;({Inr}\;({Inl}\;{Fail}))}$
case ${{t}\mathrel{=}{Op}\;({Inl}\;({MUpdate}\;{r}\;{k}))}$
case ${{t}\mathrel{=}{Op}\;({Inr}\;({Inl}\;({Or}\;{p}\;{q})))}$
case ${{t}\mathrel{=}{Op}\;({Inr}\;({Inr}\;{y}))}$
6.6 Auxiliary lemmas
The derivations above made use of several auxliary lemmas. We prove them here.
Lemma 17 (Distributivity of ${{h}_{{Modify1}}}$ )
Proof The proof follows the same structure of Lemma 4. We proceed by induction on p.
case ${{p}\mathrel{=}{Var}\;{x}}$
case ${{p}\mathrel{=}{Op}\;({Inl}\;({MGet}\;{p}))}$
case ${{p}\mathrel{=}{Op}\;({Inl}\;({MUpdate}\;{r}\;{p}))}$
case ${{p}\mathrel{=}{Op}\;({Inl}\;({MRestore}\;{r}\;{p}))}$
case ${{p}\mathrel{=}{Op}\;({Inr}\;{y})}$
7 Proofs for modelling local state with trail stack
In this section, we prove the following theorem in Section 8.
Theorem 7. Given Functor f and Undo s u, the equation
holds for all programs a that do not use the operation Op .
The proof follows a similar structure to those in Appendix 2 and Appendix 6.
As in Appendix 6, we fuse ${run}_{StateT}{\circ}{h}_{Modify}$
into ${{h}_{{Modify1}}}$ and use it instead in the following proofs.
7.1 Main proof structure
The main proof structure of Theorem 7 is as follows.
Proof The left-hand side is expanded to
Both the left-hand side and the right-hand side of the equation consist of function compositions involving one or more folds. We apply fold fusion separately on both sides to contract each into a single fold:
Finally, we show that both folds are equal by showing that their corresponding parameters are equal:
We elaborate each of these steps below.
7.2 Fusing the right-hand side
We have already fused ${{h}_{{LocalM}}}$ in Appendix 6.2. We just show the result here for easy reference.
7.3 Fusing the left-hand side
As in Appendix 2, we fuse ${{run}_{{StateT}}{\circ }{h}_{{State}}}$ into ${{h}_{{State1}}}$ . For brevity, we define
The left-hand side is simplified to
We calculate as follows:
This last step is valid provided that the fusion conditions are satisfied:
The first subcondition (1) is met by
as established in the following calculation:
We can split the second fusion condition (2) in three subconditions:
For brevity, we omit the last common part ${{fmap}\;{local2global_M}}$ of these equations. Instead, we assume that the input is in the codomain of ${{fmap}\;{local2global_M}}$ .
For the first subcondition (3), we define ${{alg}}_{{{LHS}}^{{S}}}$ as follows.
We prove it by a case analysis on the shape of input ${{op}\mathbin{::}{Modify}_{{F}}\;{s}\;{r}\;({Free}\;({Modify}_{{F}}\;{s}\;{r}\mathrel{{:}{+}{:}}{Nondet}_{{F}}\mathrel{{:}{+}{:}}{f})\;{a})}$ . We use the condition in Theorem 6 that the input program does not use the restore operation. We only need to consider the case that op is of form ${{MGet}\;{k}}$ or ${{MUpdate}\;{r}\;{k}}$ , where restore is also not used in the continuation k.
case ${{op}\mathrel{=}{MGet}\;{k}}$ In the corresponding case of Appendix 6.3, we have calculated that .
case ${{op}\mathrel{=}{MUpdate}\;{r}\;{k}}$ From op is in the codomain of ${{fmap}\;{local2global_M}}$ we obtain k is in the codomain of ${{local2global_M}}$ .
For the second subcondition (4), we can define ${{alg}}_{{{LHS}}^{{ND}}}$ as follows.
We prove it by a case analysis on the shape of input ${{op}\mathbin{::}{Nondet}_{{F}}\;({Free}\;({Modify}_{{F}}\;{s}\;{r}\mathrel{{:}{+}{:}}{Nondet}_{{F}}\mathrel{{:}{+}{:}}{f})\;{a})}$ .
case ${{op}\mathrel{=}{Fail}}$ In the corresponding case of Appendix 6.3, we have calculated that .
case ${{op}\mathrel{=}{Or}\;{p}\;{q}}$ From op is in the codomain of ${{fmap}\;{local2global_M}}$ , we obtain p and q are in the codomain of ${{local2global_M}}$ .
For the last subcondition (5), we can define ${{fwd}}_{{{LHS}}}$ as follows.
We prove it by the following calculation for input ${{op}\mathbin{::}{f}\;({Free}\;({Modify}_{{F}}\;{s}\;{r}\mathrel{{:}{+}{:}}{Nondet}_{{F}}\mathrel{{:}{+}{:}}{f})\;{a})}$ . In the corresponding case of Appendix 6.3, we have calculated that ${{h}_{{GlobalM}}\;({Op}\;({Inr}\;({Inr}\;{op})))\mathrel{=}\lambda {s}\to {Op}\;({fmap}\;(\mathbin{\$}{s})\;({fmap}\;{h}_{{GlobalM}}\;{op}))}$ (*).
7.4 Equating the fused sides
We observe that the following equations hold trivially.
Therefore, the main theorem (Theorem 7) holds.
7.5 Lemmas
In this section, we prove the lemmas used in Appendix 7.3.
The following lemma shows the relationship between the state and trail stack. Intuitively, the trail stack contains all the deltas (updates) that have not been restored in the program. Previous elements in the trail stack do not influence the result and state of programs.
Lemma 18 (Trail stack tracks state). For ${{t}\mathbin{::}{Stack}\;({Either}\;{r}\;())}$ , ${{s}\mathbin{::}{s}}$ , and ${{p}\mathbin{::}{Free}\;({Modify}_{{F}}\;{s}\;{r}\mathrel{{:}{+}{:}}{Nondet}_{{F}}\mathrel{{:}{+}{:}}{f})\;{a}}$ which does not use the restore operation, we have
for some . The functions extend and fplus are defined as follows:
Note that an immediate corollary of Lemma 18 is that in addition to replacing the stack t with the empty stack ${{Stack}\;[ ]}$ , we can also replace it with any other stack. The following equation holds.
We will also use this corollary in the proofs.
Proof We proceed by induction on p.
case ${{p}\mathrel{=}{Var}\;{y}}$
case ${{t}\mathrel{=}{Op}{\circ }{Inr}{\circ }{Inl}\mathbin{\$}{Fail}}$
case ${{t}\mathrel{=}{Op}\;({Inl}\;({MGet}\;{k}))}$
case ${{t}\mathrel{=}{Op}\;({Inl}\;({MUpdate}\;{r}\;{k}))}$
case ${{t}\mathrel{=}{Op}{\circ }{Inr}{\circ }{Inl}\mathbin{\$}{Or}\;{p}\;{q}}$
case ${{t}\mathrel{=}{Op}{\circ }{Inr}{\circ }{Inr}\mathbin{\$}{y}}$
The following lemma shows that the untrail function restores all the updates in the trail stack until it reaches a time stamp ${{Right}\;()}$ .
Lemma 19 (UndoTrail undos). For ${{t}\mathrel{=}{Stack}\;({ys}+\!\!+({Right}\;()\mathbin{:}{xs}))}$ and ${ys}\mathrel{=}[ {Left}\;{r}_{1},\mathbin{...},{Left}\;{r}_{n}]$ , we have
The function fminus is defined as follows:
Proof We first calculate as follows:
Then, we proceed by an induction on the structure of ys.
case ${{ys}\mathrel{=}[ ]}$
case ${{ys}\mathrel{=}({Left}\;{r}\mathbin{:}{ys'})}$
The following lemma is obvious from Lemma 18 and Lemma 19. It shows that we can restore the previous state and stack by pushing a time stamp on the trail stack and use the function untrail afterwards.
Lemma 20 (State and stack are restored). For ${{t}\mathbin{::}{Stack}\;({Either}\;{r}\;())}$ , ${{s}\mathbin{::}{s}}$ , and ${{p}\mathbin{::}{Free}\;({Modify}_{{F}}\;{s}\;{r}\mathrel{{:}{+}{:}}{Nondet}_{{F}}\mathrel{{:}{+}{:}}{f})\;{a}}$ which does not use the restore operation, we have
Proof Suppose ${{t}\mathrel{=}{Stack}\;{xs}}$ . We calculate as follows.
Discussions
No Discussions have been published for this article.