Haskell'15: proceedings of 8th 2015 ACM SIGPLAN Haskell workshop 978-1-4503-3808-0, 1450338089

344 77 6MB

English Pages 212 [219] Year 2015

Report DMCA / Copyright

DOWNLOAD FILE

Polecaj historie

Haskell'15: proceedings of  8th 2015 ACM SIGPLAN Haskell workshop
 978-1-4503-3808-0, 1450338089

Citation preview

September 3-4, 2015 Vancouver, BC, Canada

Haskell’15 Proceedings of the 8th ACM SIGPLAN Symposium on

Haskell Edited by:

Ben Lippmeier Sponsored by:

ACM SIGPLAN Co-located with:

ICFP’15

The Association for Computing Machinery, Inc. 2 Penn Plaza, Suite 701 New York, NY 10121-0701 c 2015 by the Association for Computing Machinery, Inc (ACM). Permission to make digital or hard Copyright copies of portions of this work for personal or classroom use is granted without fee provided that the copies are not made or distributed for profit or commercial advantage and that copies bear this notice and the full citation on the first page. Copyrights for components of this work owned by others than ACM must be honored. Abstracting with credit is permitted. To copy otherwise, to republish, to post on servers or to redistribute to lists, requires prior specific permission and/or a fee. Request permission to republish from: Publications Dept. ACM, Inc. Fax +1-212-869-0481 or E-mail [email protected]. For other copying of articles that carry a code at the bottom of the first or last page, copying is permitted provided that the per-copy fee indicated in the code is paid through the Copyright Clearance Center, 222 Rosewood Drive, Danvers, MA 01923. Notice to Past Authors of ACM-Published Articles ACM intends to create a complete electronic archive of all articles and/or other material previously published by ACM. If you have written a work that was previously published by ACM in any journal or conference proceedings prior to 1978, or any SIG Newsletter at any time, and you do NOT want this work to appear in the ACM Digital Library, please inform [email protected], stating the title of the work, the author(s), and where and when published. ACM ISBN: 978-1-4503-3808-0 Additional copies may be ordered prepaid from:

ACM Order Department P.O. BOX 11405 Church Street Station New York, NY 10286-1405

Phone: 1-800-342-6626 (U.S.A. and Canada) +1-212-626-0500 (All other countries) Fax: +1-212-944-1318 E-mail: [email protected]

Cover photo by Kenny Louie / Licensed under Creative Commons 2.0 / Cropped from original at https://secure.flickr.com/photos/kwl/2432868269

Production: Conference Publishing Consulting D-94034 Passau, Germany, [email protected]

Prelude Welcome to the 2015 ACM Haskell Symposium. This is the eighth Haskell Symposium, and the third year where the Symposium is organized as a two day event. This year the program committee introduced an early track to the submissions process, so that some papers could gain early feedback. We had 11 papers submitted to the early track, of which we accepted two outright and invited authors of the others to incorporate the reviewer’s feedback and resubmit improved versions. Most of these early track papers were resubmitted to the regular track, along with 25 new papers, for 36 submissions in total (not including demo submissions). Resubmitted early track papers were sent back to the same reviewers. All papers received at least three reviews. After a week long online meeting, we accepted 18 papers in total. On behalf of the Program Committee, I would like to thank all the authors for submitting their papers to the Haskell Symposium. Thanks also to the Program Committee members and external reviewers for their hard work and vigorous discussion. Thanks also to the Haskell Symposium Steering Committee, as well as the ICFP organizers including workshop co-chairs Tom Schrijvers and Nicolas Wu for their support.

Ben Lippmeier Haskell 2015 Program Chair


iii

Haskell Symposium 2015 Organization Program Chair: Ben Lippmeier Steering Committee Chair: Janis Voigtländer Steering Committee: Kathleen Fisher

Zhenjiang Hu Bruno Oliveira Wouter Swierstra Chung-chieh Shan

(Vertigo Technology / University of New South Wales) (University of Bonn, Germany) (Tufts University, USA) (National Institute of Informatics, Japan) (University of Hong Kong) (Universiteit Utrecht, the Netherlands)
 (Indiana University, USA)

Program Committee: Mathieu Boespflug (Tweag I/O, France)


Edwin Brady (University of St Andrews, UK)
 Atze Dijkstra (Utrecht University, Netherlands)
 Tom DuBuisson (Galois, USA)
 Torsten Grust (Universität Tübingen, Germany)
 Patrik Jansson (Chalmers University of Technology, Sweden) Patricia Johann (Appalachian State University, USA) Oleg Kiselyov (Tohoku University, Japan)
 Edward Kmett (McGraw Hill Financial, USA)
 Neelakantan Krishnaswami (University of Birmingham, UK) Hai (Paul) Liu (Intel Labs, USA) Garrett Morris (University of Edinburgh, UK) Dominic Orchard (Imperial College London,UK)
 Matt Roberts (Macquarie University, Australia)
 Tim Sheard (Portland State University, USA) Joel Svensson (Indiana University, USA) Edsko de Vries (Well-Typed, Ireland)
 Bernie Pope 
 Didier Rémy
 Chad Scherrer Tom Schrijvers
 Chung-chieh Shan
 Dominic Steinitz Dimitrios Vytiniotis
 Simon Winwood

Additional reviewers: Patrick Bahr

Bob Carpenter Manuel Chakravarty
 Julien Lange
 Sam Lindley
 Johan Nordlander
 Liam O’Connor 
 Conrad Parker


Sponsor:

!

iv

Contents Frontmatter Foreword . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

iii

Type Checking Improving Haskell Types with SMT Iavor S. Diatchki — Galois, USA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . A Typechecker Plugin for Units of Measure: Domain-Specific Constraint Solving in GHC Haskell Adam Gundry — Well-Typed, UK . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

1 11

Verification Reasoning with the HERMIT: Tool Support for Equational Reasoning on GHC Core Programs Andrew Farmer, Neil Sculthorpe, and Andy Gill — University of Kansas, USA; Swansea University, UK . . . . . . . Formally Proving a Compiler Transformation Safe Joachim Breitner — KIT, Germany . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

23 35

Graphics and Distribution Bridging the GUI Gap with Reactive Values and Relations Ivan Perez and Henrik Nilsson — University of Nottingham, UK . . . . . . . . . . . . . . . . . . . . . . . . . . . The Remote Monad Design Pattern Andy Gill, Neil Sculthorpe, Justin Dawson, Aleksander Eskilson, Andrew Farmer, Mark Grebe, Jeffrey Rosenbluth, Ryan Scott, and James Stanton — University of Kansas, USA; Swansea University, UK . . . . . . . . . . . . . . . . . . .

47

59

Generics Variations on Variants J. Garrett Morris — University of Edinburgh, UK . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Modular Reifiable Matching: A List-of-Functors Approach to Two-Level Types Bruno C. d. S. Oliveira, Shin-Cheng Mu, and Shu-Hung You — University of Hong Kong, China; Academia Sinica, Taiwan; National Taiwan University, Taiwan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

71

82

Monads and Comonads Freer Monads, More Extensible Effects Oleg Kiselyov and Hiromi Ishii — Tohoku University, Japan; University of Tsukuba, Japan . . . . . . . . . . . . . Functional Pearl: Getting a Quick Fix on Comonads Kenneth Foner — University of Pennsylvania, USA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

94 106

Type Classes Injective Type Families for Haskell Jan Stolarek, Simon Peyton Jones, and Richard A. Eisenberg — Politechnika Łódzka, Poland; Microsoft Research, UK; University of Pennsylvania, USA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Type Families with Class, Type Classes with Family Alejandro Serrano, Jurriaan Hage, and Patrick Bahr — Utrecht University, Netherlands; University of Copenhagen, Denmark . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

118

129

Concurrency and Parallelism Déjà Fu: A Concurrency Testing Library for Haskell Michael Walker and Colin Runciman — University of York, UK . . . . . . . . . . . . . . . . . . . . . . . . . . . Improving Implicit Parallelism José Manuel Calderón Trilla and Colin Runciman — University of York, UK . . . . . . . . . . . . . . . . . . . . .

v

141 153

Probabilistic and Linear Programming Practical Probabilistic Programming with Monads ´ Adam Scibior, Zoubin Ghahramani, and Andrew D. Gordon — University of Cambridge, UK; Microsoft Research, UK; University of Edinburgh, UK . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Embedding a Full Linear Lambda Calculus in Haskell Jeff Polakow — Awake Networks, USA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

165 177

Code Generation Guilt Free Ivory Trevor Elliott, Lee Pike, Simon Winwood, Pat Hickey, James Bielman, Jamey Sharp, Eric Seidel, and John Launchbury — Galois, USA; University of California at San Diego, USA; Willamette University, USA . . . . . . . . . . . . . . Type-safe Runtime Code Generation: Accelerate to LLVM Trevor L. McDonell, Manuel M. T. Chakravarty, Vinod Grover, and Ryan R. Newton — Indiana University, USA; UNSW, Australia; NVIDIA, USA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Author Index

vi

189

201

Improving Haskell Types with SMT Iavor S. Diatchki Galois Inc., USA [email protected]

Abstract

The approach presented in this paper uses similar tools— namely SMT solvers—to Liquid Haskell [14]. However, the design and overall goals of the two techniques are somewhat different and, indeed, it makes perfect sense to use both systems in a single project. Liquid Haskell enables Haskell programmers to augment Haskell definitions with refinements of the types, which may be used to verify various properties of the program. The refinement language is separate from the language of Haskell types—it is quite expressive, and is really aimed at formal verification. As such, a function will have its ordinary type and, in addition, a refinement type, which serves to specify some aspect of the behavior of the function. The checks performed by Liquid Haskell are entirely separate from the ones performed by GHC’s type checker. This is quite different from the technique described in this paper, which shows how to integrate an SMT solver directly into GHC’s type checker. The benefit of of extending GHC’s type checker is that we don’t need to introduce a separate language for specifications, instead we may reuse types directly. Ultimately, however, the two approaches are not at all mutually exclusive. The next section gives a brief flavor of what can be achieved using the techniques from this paper.

We present a technique for integrating GHC’s type-checker with an SMT solver. The technique was developed to add support for reasoning about type-level functions on natural numbers, and so our implementation uses the theory of linear arithmetic. However, the approach is not limited to this theory, and makes it possible to experiment with other external decision procedures, such as reasoning about type-level booleans, bit-vectors, or any other theory supported by SMT solvers. Categories and Subject Descriptors D.3.3 [Language Constructs and Features]: Constraints Keywords Type systems, SMT, constraint solving

1.

Introduction

For a few years now, there has been a steady push in the Haskell community to explore and extend the type system, slowly approximating functionality available in dependently typed languages [16– 18]. The additional expressiveness enables Haskell programmers to maintain more invariants at compile time, which makes it easier to develop reliable software, and also makes Haskell a nice language for embedding domain specific languages [15]. The Haskell compiler is not just a translator from source code to executable binaries, but it also serves as a tool that analyzes the program and helps find common mistakes early in the development cycle. Unfortunately, the extra expressiveness comes at a cost: Haskell’s type system is by no means simple. Writing programs that make use of invariants encoded in the type system can be complex and time consuming. For example, often one spends a lot of time proving simple theorems about arithmetic which, while important to convince the compiler that various invariants are preserved, contribute little to the clarity of the algorithm being implemented [18]. Given that in many cases the proofs constructed by the programmers tend to be fairly simple, we can’t help but wonder if there might be a away to automate them away, thus gaining the benefits of static checking, but without cluttering the program with trivial facts about, say, arithmetic. This paper presents a technique to help with this problem: we show one method of integrating an SMT solver with GHC’s type checker. While we present the technique in the context of Haskell and GHC, the technique should be applicable to other programming languages and compilers too.

1.1

Examples

We illustrate the utility of the functionality provided by our algorithm with a few short examples. A very common example in this area is to define a family of singleton types that links type-level natural numbers to run-time constants that represent them: data UNat :: Nat -> * where Zero :: UNat 0 Succ :: UNat n -> UNat (n + 1) Here we’ve used a unary representation of the natural numbers, and each member of the family, UNat n, has exactly one inhabitant, namely the natural number n represented in unary form. Because we are using a GADT, we can pattern match on the constructors of the type and gradually learn additional information about the value being examined. The kind Nat is inhabited by types corresponding to the natural number (e.g., 0, 1, . . . ), and (+) is a type-level function for adding natural numbers. Next, we define a function to add two such numbers: uAdd :: UNat m -> UNat n -> UNat (m + n) uAdd Zero y = y uAdd (Succ x) y = Succ (uAdd x y)

Permission to make digital or hard copies of all or part of this work for personal or classroom use is granted without fee provided that copies are not made or distributed for profit or commercial advantage and that copies bear this notice and the full citation on the first page. Copyrights for components of this work owned by others than ACM must be honored. Abstracting with credit is permitted. To copy otherwise, or republish, to post on servers or to redistribute to lists, requires prior specific permission and/or a fee. Request permissions from [email protected].

While this is a simple definition, and we are unlikely to have gotten it wrong, it is nice to know that GHC is checking our work! Had we made a mistake, for example, by mis-typing the recursive call as uAdd x x, we would get a type error:

Haskell’15, September 3-4, 2015, Vancouver, BC, Canada c 2015 ACM. 978-1-4503-3808-0/15/09...$15.00

http://dx.doi.org/10.1145/2804302.2804307

Could not deduce (((n1 + n1) + 1) ~ (m + n)) from the context (m ~ (n1 + 1))

1

the helper function go needs a type signature, because it patternmatches using GADT constructors, however the type of reverse can be inferred as usual:

It is a small detail, but it is worth pointing out that our GHC extension would have been just as happy had we defined the type of Succ like this: Succ :: UNat n -> UNat (1 + n)

*Example> :type vecReverse vecReverse :: Vec n a -> Vec n a

The difference is in the return type, in the one case it is n + 1, and in the other it is 1 + n. While to a human this looks like an insignificant difference, in many systems it is significantly easier to work with the one definition, but not the other. The unary natural numbers are handy if we are planning to do iteration. However, if we are working on some sort of divide-andconquer algorithm, we often need to split the input in two. In this situation, a different family of singletons is more useful:

1.2

data BNat :: Nat -> * where Empty :: BNat 0 Even :: (1 BNat n -> BNat (2 * n) Odd :: BNat n -> BNat (2 * n + 1) This is another singleton family, where the type BNat n is inhabited by a single member, n. However, in this case we learn different information by pattern examining the values: an Empty value may not be split, an Even value may be split into two equal non-empty parts, while an Odd value may be split into two parts, and there will be one element left over. So, how do we add such numbers? There are more cases to consider: bAdd bAdd bAdd bAdd bAdd bAdd bAdd

2.

2.1

:: BNat m -> BNat n -> BNat (m + n) Empty x = x x Empty = x (Even x) (Even y) = Even (bAdd x y) (Even x) (Odd y) = Odd (bAdd x y) (Odd x) (Even y) = Odd (bAdd x y) (Odd x) (Odd y) = Even (bSucc (bAdd x y))

:: BNat m Empty (Even x) (Odd x)

SMT Solvers

This section contains an introduction to the core functionality of a typical SMT solver, and may be skipped by readers who are already familiar with similar tools. The Core Functionality

SMT solvers, such as CVC4 [5], Yices [1], and Z3 [12], implement a wide collection of decision procedures that work together to solve a common problem. They have proved to be a useful tool in both software and hardware verification. From a user’s perspective, the core functionality of an SMT solver is fairly simple: we may declare uninterpreted constants, assert formulas, and check if the asserted formulas are satisfiable. Checking for satisfiability simply means that we are asking the question: are there concrete values for the uninterpreted constants that make all asserted formulas true. The solver may respond in one of three ways:

The correctness of this definition is much less obvious, and we did make a couple of mistakes before getting it right! In the last case, we use the auxiliary function bSucc, which increments a binary natural number by one: bSucc bSucc bSucc bSucc

Structure of the Paper

The rest of the paper is organized as follows: we start with a brief overview of SMT solvers from a user’s perspective (Section 2). Then, in Section 3, we introduce the basic concepts of GHC’s constraints solver, which is necessary for putting the rest of the paper in context. Section 4 contains the details of the algorithm for integrating an SMT solver with GHC, and Section 5 explains other theories that could be added to GHC using the same technique. Finally, Section 6 discusses the possibility of using the ideas in this paper, and previous work on the implementation of SMT solver, to engineer a modular constraint solver.

• sat, which means that a satisfying assignment exists,

-> BNat (m + 1) = Odd Empty = Odd x = Even (bSucc x)

• unsat, which means that a satisfying assignment does not exist, • unknown, which means that the solver did not find an answer,

often this is due to reaching some sort of limit (e.g, the solver used up too much time).

Note that while these examples are somewhat simplistic, they demonstrate the utility of the technology, and show that using the extension feels quite natural to a Haskell programmer. A more conventional example is the type of fixed-length vectors:

This example uses the notation of the SMTLIB standard [4]: (declare-fun x () Int) (assert (>= x 0)) (assert (= (+ 3 x) 8)) (check-sat)

data Vec :: Nat -> * -> * where Nil :: Vec 0 a Cons :: a -> Vec n a -> Vec (n + 1) a

The example declares an integer constant, x, asserts some formulas—using a prefix notation—about it, and then asks the solver if the asserted formulas are satisfiable. In this case, the answer is affirmative, as choosing 5 for x will make all formulas true. Indeed, if an SMT solver reports that a set of formulas is satisfiable, typically it will also provide a satisfying assignment, which maps the uninterpreted constants to concrete values that make the formulas true. The same machinery may also be used to prove the validity of a universally quantified formula. The idea is that we use the SMT solver to look for a counter-example to the formula, and if no such example exists, then we can conclude the formula is valid. For example, if we want to prove that ∀x.(3+x = 8) =⇒ x = 5, then we can use the SMT solver to try to find some x that contradicts it:

Note that this is very similar to UNat, except that it also stores a value in each Cons node. The operation of appending two vectors together follows exactly the same pattern as uAdd. An example of a function that is meaningful for vectors, bit no so much for the unary natural numbers is reversing a vector, which may be defined as follows using our extension: vecReverse = go Nil where go :: Vec m a -> Vec n a -> Vec (m + n) a go xs Nil = xs go xs (Cons y ys) = go (Cons y xs) ys This example also illustrates that our extension does not change any type signature requirements beyond what is already in GHC:

2

When we check the definition of isZero, we end up with an implication constraint like this:

(declare-fun x () Int) (assert (= (+ 3 x) 8)) (assert (not (= x 5))) (check-sat)

(a ~ Int) => (Num a, Eq a) Here, a is the type of the pattern variable x, the wanted constraints arise from the use of 0 and (==) respectively, and the given constraint is obtained by pattern matching with EInt because we know that the type parameter of E must be Int.

To invalidate an implication, we need to assume the premise, and try to invalidate the conclusion, which is why the second assertion is negated. In this case the SMT solver tells us that the asserted formulas are not satisfiable, which means that there are no counter examples to the original formula and, therefore, it must be valid. 2.2

3.2

Incremental Solvers

Many SMT solvers have support for asserting formulas incrementally. This means that the solver performs a little work every time a new formula is asserted, rather than collecting all formulas and doing all the work in batch mode, once we ask about satisfiability. Furthermore, incremental solvers have the ability to mark a particular solver state, and then revert back to it, using a stack discipline. Here is an example: (declare-fun x () Int) (assert (= (+ 5 x) 3)) (push 1) (assert (>= x 0)) (check-sat) (pop 1) (check-sat)

3.3

This example asks the solver two questions, and the answer to the first one is unsatisfiable, while the answer to the second one is satisfiable and the solution is x = -2. The push command instructs the solver to save its state, then proceed as normal. When the solver encounters a pop command, it reverts to the last saved state. This functionality is extremely useful when we want to ask many questions that are largely the same, and differ only in a few assertions: we can perform the majority of the work once, and then use push and pop to just assert the differences. In our small example, the work for the first assertion is shared in both calls to check-sat.

3.

Type-Checker Plugins

In this paper we describe a fairly general extension to the constraint solver, but other researchers are interested in different extensions, for example, to add support for units of measure [2]. Instead of having many ad-hoc extensions directly in GHC’s constraint solver, we collaborated to define and implement an API for extending GHC’s functionality via type-checker plug-ins. At present, we are aware of three users of this API—our work, a related plug-in that can solve numerical constraints using custom rewrite rules [3], and the work on units of measure [2]. While this infrastructure is still brand new, and not yet stable, we hope that this mechanism may be useful to other researchers too, as it makes it fairly easy to experiment with various extensions to GHC’s constraint solver. Since the framework is aimed at GHC developers, and is not intended for use in everyday Haskell programming, the plug-ins have a lot of power. Indeed, it is not difficult to write a plug-in that would violate type-safety, in much the same way one could do this by modifying GHC’s constraint solver directly. The full details of the type-checker plug-ins API may be found in the GHC User’s Guide [20]. Here we discuss only some of the design choices that we considered, as they have a direct bearing on the workings of our plug-in. An interesting question about type-checker plug-ins is: at what point in GHC’s type checker should we invoke them? The answer to this question affects their functionality, and the interface that a plug-in would have to implement. We considered a few alternatives:

GHC’s Constraint Solver

In this section, we present relevant aspects of GHC’s constraint solver. The full details of the algorithm [7] are beyond the scope of this paper. 3.1

The Constraint Solver State

Implication constraints are solved by two calls to the constraint solver: the first call processes (i.e., assumes) the given constraints, and the second one processes the wanted constraints. The constraint solver has two central pieces of state: the work queue, and the inert set. The work queue contains constraints that need to be processed, while the inert set contains constraints that have already been processed. Constraints are removed—one at a time—from the work queue, and interacted with the solver’s state. In the process of interaction we may solve constraints, generate new work, or report impossible constraints. If nothing interesting happens, then we relocate the constraint to the inert set. It is also possible that during interaction a previously inert constraint may be reactivated in a rewritten form and re-inserted in the work queue. A single invocation of the constraint solver keeps interacting constraints until the work queue is empty and all constraints become inert.

Implication Constraints

During type inference, GHC uses implication constraints, which do not appear in Haskell source code directly. An implication constraint is, roughly, of the form G =⇒ W , where W is a collection of constraints that need to be discharged, and G are assumptions that may be used while discharging W . In the GHC source code, the constraints in G are referred to as given constraints, while the ones in W are known as wanted constraints. The intuition behind an implication constraint is that W contains the constraints that were collected while checking a program fragment, while G contains local assumptions that are available only in this particular piece of code. For example, consider the following program fragment, which uses a GADT:

1. Add a pass to the solver’s pipeline, meaning that plug-ins process constraints one at a time, the way GHC does. 2. Add a call to the plug-ins after the constraints solver has reached an inert state. 3. Hand off implication constraints directly to the plug-ins, before invoking GHC’s constraint solver.

data E :: * -> * where EInt :: Int -> E Int

Option 1 has the closest integration with GHC, but since plugins need to process constraints one at a time, then they often ended up needing some state, which than has to be stored and managed somewhere, which became rather complicated.

isZero :: E a -> Bool isZero (EInt x) = x == 0

3

simplified everything as much as it could, improved using equalities, and cannot see anything else to do. The inert constraints are presented to the plug-in in three collections based on the constraints’ provenance: one group contains the given constraints (i.e., assumptions), one group contains derived constraints (see Section 3 for details), and one group contains the wanted constraints, which are the goals that need solving.

Option 3 is the most general, as it would allow for the plug-in to completely override GHC’s behavior. However, we were more interested in extending GHC’s capabilities rather than replacing them, so we opted against it. We chose option 2 as a nice middle ground. It allows a plugin to work with all constraints at once, but it has the benefit that standard work done by GHC has already happened. So, after the constraint solver reaches an inert state, it calls into the plug-ins, which examine the inert state and attempt to make progress. If they do, then the constraint solver is restarted and process repeats. 3.4

Output. The desired output of the algorithm is as follows: • Solve as many wanted constraints as possible. • Notice if the wanted constraints are inconsistent.

Improvement and Derived Constraints

• Compute new given and derived equalities to help solve con-

An improving substitution [13] allows us to instantiate variables in the constraints with types, potentially enabling further constraint simplification. Of course, we should only do so, as long as we preserve soundness and completeness. In this context, preserving soundness means that the improved constraints should imply the original constraints (i.e., we didn’t just drop some constraints), while completeness means that the original constraints imply the improved ones (i.e., we didn’t loose generality by making arbitrary assumptions). In GHC, improvement happens by rewriting with equality constraints. There are three sources of equality constraints:

straints that are outside this decision procedure’s scope. The first task is the most obvious purpose of the algorithm, but the other two are quite important also. Noticing inconsistencies avoids the inference of types with unsatisfiable constraints. Consider, for example, a single wanted constraint, (x + 5) ~ 2. Since we are working with natural numbers, this constraint has no solution, so we cannot solve it. As a result, we may end up inferring a type like this: someFun :: forall x. (x + 5) ~ 2 => ... While, technically, this is not wrong, it is undesirable because the type error is clearly in the definition of someFun, but we would delay reporting the error until the function is called. So, we’d like to notice constraints that are impossible to solve (i.e., they are logically equivalent to ⊥), so that we can report type-errors that are closer to their true location. Note that the source of a contradiction may be a combination of constraints, and not just a single one. For example, the constraints x >= 5 and 3 >= x are inconsistent together, but have solutions when considered individually. Computing new equations is also very important, as it enables collaboration between the plug-in and the rest of GHC (i.e., the main constraint solver, and other plug-ins). For example, consider an implication constraint of the form:

• given equalities are implied by the given constraints, • wanted equalities arise when solving wanted constraints, • derived equalities are implied by the given and wanted con-

straints, together. The provenance of an equality constraint determines the kinds of improvements that we can use it for. Given equalities have solid proof, and so we may use congruence to rewrite any other constraint. On the other hand, wanted constraints are goals that need to be proved, so they cannot be used to rewrite given constraints. We may still use them to rewrite other wanted or derived constraints though. Finally, derived equalities are implied by the given and wanted constraints jointly, so they may not be used in proofs directly, as doing so may lead to circular reasoning. Instead, derived equalities help with type inference, by guiding the instantiation of unification variables. In general, it is always sound to instantiate a unification variable with whatever type we want: doing so may reject valid programs, but it will not accept invalid ones, because we still need to solve all necessary constraints. Of course, we don’t want to reject valid programs, and this is where derived equalities help us: since they are implied by the goals and assumptions together, we know that we are not loosing any generality when instantiating as suggested by a derived constraint. So, for example, if we compute a derived constraint x ~ Int, and we have a wanted constraint Eq x, and x is a unification variable, then we may rewrite Eq x to Eq Int, which we would proceed to solve as usual. This is bit subtle: a derived equality is never used directly in a proof—instead it allows us to change what we are trying to prove by instantiating unification variables, which may help discharge all constraints that need solving.

4.

forall x. (x + 5) ~ 8

=>

KnownNat x

In this case, we’d like to use the SMT solver to compute a new given equality, x ~ 3. Then, this equality can be used by GHC to rewrite the wanted constraint KnownNat x to KnownNat 3 which, in turn, can be discharged by the custom solver for the KnownNat class1 . Such collaboration between different solvers is quite common. Interestingly, this looks a lot like the collaboration between decision procedures in an SMT solver! We discuss this observation further in Section 6. 4.1

The Language of Constraints

Our first task is to identify constraints that are relevant to our solver. In the current implementation, we consider equalities and inequalities between a subset of the type-expressions of kind Nat. The kind Nat is inhabited by an infinite family of type-constants: 0, 1, 2, .. :: Nat These constants may be combined and compared using the following type-level functions:

Integrating GHC with an SMT Solver

type family (+) :: Nat -> Nat -> Nat type family (*) :: Nat -> Nat -> Nat type family ( Nat -> Bool

We describe the algorithm in the context of the theory of linear arithmetic over the natural numbers, as having a concrete theory makes it easier to explain the process and illustrate it with examples. In the next section, we discuss how, and why, we might want to consider other theories also.

These functions have no user-specified definitions: instead, we extended the core GHC simplifier with support for forward evaluation

Input. Currently, the algorithm is implemented as a type-checker plug-in, and so the input to the algorithm is a collection of constraints that GHC has determined to be inert. This means that GHC

1 The

class KnownNat is defined in module GHC.TypeLits. It is used to implement singleton types linking type-level literals to their run-time representations as Integer.

4

on concrete values, so it will evaluate concrete expressions, such as 2 + 3. This simple forward evaluation cleans up the constraints, leaving the more complex reasoning—involving variables—to the algorithm being presently described. Technically, the clean-up is not necessary because the plug-in will perform as much evaluation as it needs to solve the constraints, but it is a lot more efficient to simply evaluate known constants without consulting the external solver. Also, at present there is no standard way to declare that these type function are handled by a special solver—we could use a closed type family with no equations to prohibit user defined instances, but this is a bit of an abuse of an unrelated feature of the type system. The declaration for (

Expr -> IO () IO Result IO () IO ()

These correspond directly to the commands used in the examples in Section 2. 4.4

Checking for Consistency

Given some constraints in our theory, we would like to know if a solution exists. If the constraints are inconsistent, then we can terminate the constraint solving problem early, and report an error, as discussed previously. To check for consistency, we assert all constraints, and ask the SMT solver if the result is satisfiable. If this is not the case, then we report an error, otherwise we proceed with the algorithm.

Lemma. If C(t) is satisfiable, then C(x) is also satisfiable. Proof. This follows because, by definition, C(t) ⇐⇒ C(x)∧x = t. Therefore, if σ is satisfying assignment for C(t), then σ ∪ {x = σt} is satisfying assignment for C(x).

2 GHC’s

“flattening” pass names the results of all type functions. This is done before passing constraints to GHC’s internal solver, but the process is reverted before passing the constraints to the external plug-ins.

Corollary. If C(x) is not satisfiable, then neither is C(t). This is simply the contrapositive form of the previous Lemma.

5

straint that caused the conflict is added to the yes set, and then we examine the previously asserted constraint, to check if they are necessary for the conflict. The reduction process performs in O(n2 ) time, where n is the number of constraints. This has not been a big problem, as this reduction happens only when we report errors and, typically, the number of constraints is fairly low. Also note that the reduction algorithm may not produce the smallest collection of constraints to cause a contradiction. For example, consider the constraints A, B, C, and suppose that A, B lead to a contradiction together, and also C causes a contradiction on its own. Now, depending on the order in which we process the constraints, we may either compute C as the result of the reduction, or A, B. This is OK—in this case the context happens to contain multiple errors, so the reduction algorithm will simply pick one of them to report. Of course, a program would have to eventually fix both errors before the program is accepted. The important property of the algorithm is that the reduced set does not contain redundant constraints, in the sense that if we removed some of the constraints the resulting constraint set would be satisfiable.

checkConsistent :: Solver -> [Expr] -> IO Bool checkConsistent s cs = do mapM_ (solverAssert s) cs res [Expr] -> -- part of conflict [Expr] -> -- search for conflict IO [Expr] reduce s yes [] = return yes reduce s yes mb = do solverPush s search s yes [] mb

Improve to Constant. As we know that the constraints are consistent, we can ask the SMT solver for a satisfying assignment. This assignment contains one possible solution to constraints, but we should not emit an improving equality unless we are sure that this is the the only possible solution. So, if x = v is in the satisfying assumption, then we try to prove that the currently asserted constraints imply this fact. We do this by temporarily asserting x 6= v, and checking for satisfiability. If the resulting is unsatisfiable, then we know that v is the only possible value for x, and we can emit the corresponding improving equation.

search :: Solver -> [Expr] -> -- part of conflict [Expr] -> -- currently asserted [Expr] -> -- search for conflict IO [Expr] search s yes mb (c : cs) = do solverAssert s c res do solverPop s solverAssert s c reduce s (c : yes) mb

solverProve :: Solver -> Expr -> IO Bool solverProve s p = do solverPush s solverAssert s (SMT.not p) res search s yes (c : mb) cs

mustBeK :: Solver -> Name -> Value -> IO Bool mustBeK s x v = solverProve s (eq (SMT.const x) (value v))

search s yes mb [] = error "Impossible!" The algorithm keeps track of constraints that we are certain are part of the conflict in the variable yes. In addition, the algorithm has the invariant that the constraints in yes are always asserted in the solver’s state, represented by the variable s. Initially, yes starts off empty. We search for conflicts in the function search, which asserts constraints one at a time, until a conflict is discovered. The con-

This process takes O(n) time, where n is the number of variables. Note that here we are making use of the incremental capabilities of the solver, and reusing all asserted constraints, just doing one additional assert per variable. To see the process in action, consider the constraint (2 * x) ~ 16. The steps that we’d perform are:

6

(assert (= (* 2 (check-sat) ; SMT response: (get-value (x)) ; SMT response: (push 1) (assert (not (= (check-sat) ; SMT response: (pop 1) ; We generate a

To see when this improvement is needed, consider the following program fragment:

x) 16)) SAT

f :: Proxy (a + 1) f = Proxy

(8)

g :: Proxy (b + 2) g = f

x 8)))

To check this program, GHC needs to solve the constraint (?a + 1) ~ (b + 2). The question mark in ?a emphasizes that this is a unification variable, standing-in for a type that needs to be inferred. We cannot discharge this constraint immediately, because it does not hold for all possible values of ?a and b. However, since ?a is a unification variable, we have the option of instantiating it to change the constraint. Of course, we have to be careful that in doing so we do not loose any generality and, in this case, the safe instantiation is ?a := b + 1. The purpose of the linear-relation improvement is to discover these kinds of instantiations. At first sight, this example may appear artificial, however it is an abstraction of a very common pattern: namely when one function calls another, and the type of the provided argument does not match syntactically the type of the expected argument. As another example, consider the function vecHead, which requires that its parameter has at least one element:

UNSAT computed improvement: x ~ 8

Improve to Variable. We may also use the satisfying assignment to look for improvements of the form x = y, where x and y are both variables. We look at the satisfying assignment, ignoring variables that were already improved to constants, and consider pairs of variables that happen to have the same value in this assignment. If x and y are two such variables, then we try to prove that x = y must hold under the current assumptions: mustEqual :: Solver -> Name -> Name -> IO Bool mustEqual s x y = solverProve s (eq (SMT.const x) (SMT.const y))

vecHead :: Vec (n + 1) a -> a vecHead (Cons x _) = x

This process take O(n2 ) time, where n is the number of variables, but we only need to call the solver for pairs of variables that have the same value—if they do not, then the current assignment provides a counter example indicating that the variables do not need to be equal in general.

Now, if we tried to call vecHead with an expression of type Vec (b + 2) a, we’d end up in exactly the same situation as the previous example, where we’d have to infer that ?n = b + 1. Our current implementation only considers linear relations between pairs of variables as we were concerned about the performance penalty if we tried combinations of more variables: the math will work out, but the algorithm might become too slow to be practical, and alternative solutions based on rewriting might be more suitable. We admit that this improvement is somewhat ad-hoc, but as the previous examples illustrate, it helps in situations that are quite common in practice. It was also fairly simple to implement using the solver.

Improve Using a Linear Relation. This improvement is a generalization of the previous example, that is specific to linear arithmetic. The idea is to try to discover improving equations of the form y = A ∗ x + B. This type of improvement is a little different from the others, in that the right-hand side of the equation contains terms that are in the theory that we are solving. In general, such constraints are not very useful for the rest of GHC, as other parts of the constraint solver do not know about our theory (i.e., the functions * or +) and, if we can prove it, then we must already know about this improvement! This sort of improvement, is useful in one very common case, however: if y is a unification variable, then having this type of improving equation enables GHC to instantiate the variable, which leads to better type inference and simpler constraints, so we attempt this improvement only when the previous two have failed, and only for the unification variables in the constraints. We can compute the values for A and B, if we have two examples for x and two examples for y. It is quite standard to ask the SMT solver for additional examples: this is done by adding additional constraints that force the models to differ. In our setting we know that we can always get another such examples—if this was not the case, a variable would have exactly one possible values, and we would have improved it to a constant, as describe previously. Furthermore, since we do the linear relation improvement last, we usually can avoid extra calls to the solver because by this point we typically already have two models where the variables differ. It is also important that the x values be different, but we can always find such examples: if we cannot find such an example, then x would have only one possible value, and it would have been improved already. Once we compute candidate values for A and B, we still need to invoke the solver to validate that the computed relation holds in all instances of the constraints, and not just the two that we happened to pick.

Custom Improving Rewrites. All the improvements that we’ve defined so far work by only using the solver—note that we were just looking at satisfying assignments, and did not have any special rules about the shapes of the constraints. It also makes sense to extend the system with custom rewrite rules, which has the potential of speeding up performance, and also adding support for features that go beyond the solver’s capabilities. We have experimented with various custom rules, but it is as yet unclear what constitutes a good set of rewrites. Concrete custom rules that would help are various normalization rules, for example applying distributivity repeatedly, and also rules that rewrite equations so that there is just a unification variable on the one side of the equation, which would help it get instantiated. 4.7

Solving Constraints

The process of solving constraints is a straight-forward call to the solver. The only thing we need to do before solving constraints is to remove the assertions that were added during the consistency and improvement stages. Recall that GHC solves implication constraints with two calls to the constraint solver: the first one asserts the given constraints, while the second one actually solves goals. While we are asserting the assumption, we just check for consistency and improvement (to generate new given equalities), but we perform no solving as there is nothing to solve. During the solving stage, we mark the solver’s state after we’ve asserted the givens,

7

then we assert the new goals, check for consistency, and improvement, and then, before solving, revert back to the state where only the givens are asserted.

• DataKinds enables natural number literals at the type level.

solverSimplify :: Solver -> [Expr] -> IO ([Expr],[Expr]) solverSimplify s wanteds = solverPrepare s wanteds $ \others our_wanteds -> do res > >

Typed functional programming and units of measure are a natural combination, as F# ably demonstrates. However, encoding staticallychecked units in Haskell’s type system leads to inevitable disappointment with the usability of the resulting system. Extending the language itself would produce a much better result, but it would be a lot of work! In this paper, I demonstrate how typechecker plugins in the Glasgow Haskell Compiler allow users to define domain-specific constraint solving behaviour, making it possible to implement units of measure as a type system extension without rebuilding the compiler. This paves the way for a more modular treatment of constraint solving in GHC.

and the system will correctly infer the units of distance: val distance : floathmi = 15.0 In addition, Kennedy’s system supports unit polymorphism: definitions can be checked abstractly, with the concrete units being determined at the use sites. For example, one can define the function > let sqr (x : floath i) = x ∗ x ; ; val sqr : x : floathui → floathu ∧ 2i

Categories and Subject Descriptors D.3.3 [Programming Languages]: Language Constructs and Features; F.3.3 [Logics and Meanings of Programs]: Studies of Program Constructs

which is polymorphic in a unit variable u. The type annotation on the definition is necessary because overloaded arithmetic operators in F# do not have units by default.

Keywords Dimensions, type inference, modular typechecking

1.

[] type m; ; [] type s; ; let time = 3.0 hsi; ; let speed = 5.0 hm/si; ; let distance = time ∗ speed ; ;

Modern GHC Haskell supports a range of language features (in particular, type families) that make it possible to encode quite complex properties at the type level. Correspondingly, in the Haskell world there have been various attempts to encode units of measure, in particular the robust and expressive units library by Muranushi and Eisenberg (2014). This allows one to write2

Introduction

Dimensions (such as length and time) and units of measure (such as metres, feet and seconds) are a very useful mechanism to reduce the chances of making a costly error1 , and make it easier to perform calculations. As Kennedy (2010) put it, “Units-of-measure are to science what types are to programming.” It is natural, therefore, to consider the extension of typed programming languages with support for units of measure. At a minimum, such support should allow the programmer to declare the units of quantities, and prevent them making errors such as adding incompatible quantities.

time = 3.0 % [si| s |] speed = 5.0 % [si| m/s |] distance = time  speed

although the inferred type of distance is not F#’s floathmi but distance :: Qu [F Length One] DefaultLCSU Double .

There has been much work in this direction, notably by Kennedy (2010) in the context of the F# functional programming language. He has shown that units of measure fit particularly well with HindleyMilner type inference, leading to a simple but powerful system.

This work is an impressive demonstration of advanced type-level Haskell programming that provides a very expressive system, but it is inevitably limited by the GHC features available to programmers. The main limitations are the inferior type inference behaviour and error messages produced by units of measure libraries compared to genuine language extensions (as in F#).

1 It

is traditional here to cite the Mars Climate Orbiter, or the Gimli Glider (http://lamar.colostate.edu/~hillger/unit-mixups.html).

How might we go about extending the Haskell language, as implemented in GHC, with units of measure? Ideally, we want a modular design that does not unnecessarily bake features into the compiler, and allows their impact on the type system to be understood in isolation. Moreover, the development effort required to extend GHC with a new feature is substantial. It would be much better if we were able to plug in support for units of measure to the typechecker, without changing GHC itself.

Permission to make digital or hard copies of all or part of this work for personal or classroom use is granted without fee provided that copies are not made or distributed for profit or commercial advantage and that copies bear this notice and the full citation on the first page. Copyrights for components of this work owned by others than ACM must be honored. Abstracting with credit is permitted. To copy otherwise, or republish, to post on servers or to redistribute to lists, requires prior specific permission and/or a fee. Request permissions from [email protected]. Copyright is held by the owner/author(s). Publication rights licensed to ACM.

Haskell’15, September 3-4, 2015, Vancouver, BC, Canada ACM. 978-1-4503-3808-0/15/09...$15.00 http://dx.doi.org/10.1145/2804302.2804305

2 Provided

[si| · |] is defined as a quasiquoter for SI units, so for example [si| m/s |] represents metres per second as a unit.

11

1.1

1.3

Type-Level Arithmetic

In the sequel, I will first describe uom-plugin,6 a Haskell library for units of measure, then explain the typechecker plugins feature that makes it possible. Iavor Diatchki, Eric Seidel and I implemented this feature in GHC 7.10.1.7 I will specify typechecker plugins in general, and the constraint-solving algorithm used by uom-plugin in particular, using the OutsideIn(X) type inference framework.

Additional motivation for being able to extend the typechecker comes from another desirable GHC extension: increasing the automated reasoning available to users of type-level arithmetic. For some time, thanks to the work of Iavor Diatchki3 , it has been possible to use natural number literals and arithmetic operators in types. For example, one can define vectors (lists indexed by their length):

Concretely, the contributions of this paper are:

data Vec a (n :: Nat) where Nil :: Vec a 0 Cons :: a → Vec a n → Vec a (1 + n) myVec :: Vec Char 3 myVec = Cons ’a’ (Cons ’b’ (Cons ’c’ Nil)) vhead :: Vec a (1 + n) → a vhead (Cons x ) = x

• a design for a units of measure library with good type inference

properties, showing the need for domain-specific constraint solving behaviour in the typechecker (section 2); • an explanation of the typechecker plugins interface that enables

constraint solver extension, both informally in Haskell and formally by relating it to OutsideIn(X) (section 3); and

However, further progress is stymied by the lack of support for working with numeric variables. While vhead works,

• an algorithm for solving constraints in the equational theory of

free abelian groups, which satisfies the properties required for sound and most general type inference (section 4).

vtail :: Vec a (1 + n) → Vec a n vtail (Cons xs) = xs

Section 5 compares the resulting units of measure system to other approaches in F# and Haskell, and section 6 concludes with a discussion of future directions for this work.

is not accepted by GHC 7.8.3, because it does not know that (1+) is an injective function: Could not deduce (n1 from the context ((1 bound by a pattern ... in an equation

2.

~ n) + n) ~ (1 + n1)) with constructor Cons for ‘vtail’

Units of Measure

First, I will describe how to extend the language with the syntax of units of measure, then will go on to discuss their semantics. 2.1

It would be nice if the typechecker was able to prove more equations, using domain-specific knowledge about arithmetic. One possibility is to interface GHC to an SMT solver, so that the SMT solver can solve arithmetic equations left unsolved by GHC (see section 5.3). The ghc-typelits-natnormalise4 typechecker plugin, and the inch preprocessor (Gundry 2013), demonstrate alternative approaches based on normalisation. 1.2

Summary

The Syntax of Units and Quantities

A typical approach to units of measure in programming languages is to annotate numeric types with their units, such as the inth·i and floath·i type constructors in F#. In Haskell, the natural way to do this is through the definition newtype Quantity a (u :: Unit) = MkQuantity a which makes Quantity a u use the same runtime representation as the underlying (typically numeric) type a, but tagged with a phantom type parameter (Leijen and Meijer 1999) u of kind Unit. This means that using Quantity a u has no runtime overhead compared to using plain a, but it can have additional safety guarantees.

Compiler Plugins

Max Bolingbroke and Austin Seipp implemented support for compiler plugins in GHC version 7.2.15 . Inspired by a similar concept in the GNU Compiler Collection (GCC), they were originally intended for adding custom optimisations and analyses of GHC’s internal Core language (System FC ). The basic idea is that a user package (distributed separately from GHC itself) contains a module M that exports a symbol plugin belonging to the type Plugin defined in the GHC API. Users can invoke GHC with an additional argument -fplugin=M, whereon the module will be dynamically linked into the running compiler, and invoked during compilation.

The Unit datatype is lifted to the kind level via datatype promotion (Yorgey et al. 2012). It has no constructors, but instead is accompanied by the following type-level definitions, implemented as type families without any equations: Base :: Symbol → Unit 1 :: Unit -- One in ASCII (~) :: Unit → Unit → Unit -- *: in ASCII ( ) :: Unit → Unit → Unit -- /: in ASCII

Crucially, plugins allow new compiler functionality to be added separately from the main development effort. This makes feature development quicker, as the entire system need not be recompiled when a plugin is changed, and makes it easier for programmers who are not compiler developers to contribute to and use plugins.

Base creates base units, which are represented as type-level strings (of kind Symbol) for simplicity. Dimensionless quantities are represented with 1, and the operators allow more complex units to be formed. Representing them as type families with no equations means they are essentially opaque symbols that may not be partially applied and are not injective; this avoids the equational theory of units conflicting with GHC’s built-in equality rules for types.

3 https://ghc.haskell.org/trac/ghc/wiki/TypeNats 4 https://hackage.haskell.org/package/

6 https://github.com/adamgundry/uom-plugin

ghc-typelits-natnormalise 5 https://downloads.haskell.org/~ghc/7.2.1/docs/html/ users_guide/compiler-plugins.html

7 https://downloads.haskell.org/~ghc/7.10.1/docs/html/

users_guide/compiler-plugins.html#typechecker-plugins

12

2.1.1

Constructing Quantities

For example, if we have some values mass :: Quantity Double (Base "kg") distance :: Quantity Double (Base "m")

Crucially, the MkQuantity constructor should not be used by client code, so that users of library work with a unit-safe interface. If the constructor were available, users could write code like this, which would destroy all unit safety guarantees provided by the library:

then we can we define prod :: Quantity Double (Base "kg" ~ Base "m") prod = mass ~ distance

unsafeConvertQuantity :: Quantity a u → Quantity a v unsafeConvertQuantity (MkQuantity x ) = MkQuantity x

but attempting to add mass to distance gives a type error:

Of course, users need some way to produce and consume quantities, i.e. convert between a and Quantity a u. It is fine for the library to expose

Couldn’t match type ‘Base "m"’ with ‘Base "kg"’ Expected type: Quantity Double (Base "m") Actual type: Quantity Double (Base "kg") In the first argument of ‘(+:)’, namely ‘mass’ In the expression: mass +: distance

unQuantity :: Quantity a u → a unQuantity (MkQuantity x ) = x but not

In addition to addition and multiplication, similar definitions are given for other standard numeric operations such as negation, division and square root. Since fractional units are not supported, the type of the latter is

MkQuantity :: a → Quantity a u as their composition yields unsafeConvertQuantity. The real problem here is that MkQuantity should only be monomorphic (sometimes known as ‘weakly polymorphic’) in its unit. It is fine for u to be any concrete unit, but it must not be generalised over to become a universally quantified type variable. Such variables are permitted in types in Caml (Garrigue 2004), but not in Haskell.

sqrt :: Floating a ⇒ Quantity a (u ~ u) → Quantity a u The user can define their own numeric primitives by accessing the internal MkQuantity constructor directly. They are then responsible for ensuring unit safety of the resulting code.

As a workaround, the library offers a Template Haskell quasiquoter [u| · |] that enables the user to write concrete quantities in a convenient syntax, translating them into safe applications of MkQuantity: mass = [u| 65 kg |] g = [u| 9.808 m/s∧ 2 |]

2.2

Are we done? Not quite. Our definitions so far allow us to write the syntax of units of measure, but we have not accounted for their equational theory. We would expect quantities with the units Base "kg" ~ Base "m" and Base "m" ~ Base "kg" to be interchangeable; unit multiplication should be commutative. But adding mass ~ distance to distance ~ mass gives:

-- equivalent to m/(s ∗ s)

For example, mass translates into MkQuantity 65 :: Quantity a (Base "kg") Bare numeric literals are interpreted as dimensionless constants by the quasiquoter, except for zero, which is polymorphic in its units:

Couldn’t match type ‘’Base "m" *: ’Base "kg"’ with ‘’Base "kg" *: ’Base "m"’ NB: ‘*:’ is a type function, and may not be injective Expected type: Quantity Double (’Base "kg" *: ’Base "m") Actual type: Quantity Double (’Base "m" *: ’Base "kg") In the second argument of ‘(+:)’, namely ‘(distance *: mass)’ In the expression: (mass *: distance) +: (distance *: mass)

α = [u| 0.00729735 |] :: Quantity Double 1 zero = [u| 0 |] :: Quantity Double u Omitting the numeric value yields a specialisation of MkQuantity to the appropriate type, which is useful when units need to be attached to numeric values that are not literal constants, for example: readMass :: IO (Quantity Double (Base "kg")) readMass = fmap [u| kg |] readLn 2.1.2

The Equational Theory of Units

Arithmetic Operations on Quantities

The library includes the following (written +: and *: in ASCII):

In addition to the usual GHC Haskell rules for type equality (Sulzmann et al. 2007), we would like additional equations to hold to characterise the operations. As in Kennedy’s system in F#, these equations are the standard laws of an abelian group:

(⊕) :: Num a ⇒ Quantity a u → Quantity a u → Quantity a u MkQuantity x ⊕ MkQuantity y = MkQuantity (x + y) (~) :: Num a ⇒ Quantity a u → Quantity a v → Quantity a (u ~ v ) MkQuantity x ~ MkQuantity y = MkQuantity (x ∗ y)

∀u ∀u ∀u ∀u

The (⊕) and (~) operators on quantities are analogous to the (+) and (∗) operators on numbers, except that the phantom parameter makes sure the units are kept in order. Quantities may be multiplied regardless of their units, but may be added only if the units match.8

v . ((u ~ v ) ~ w ) v . (u ~ v ) v . (u ~ 1) v . (u ~ (1 u))

∼ ∼ ∼ ∼

(u ~ (v ~ w )) (v ~ u) u 1

But how can we make them hold? GHC allows new axioms to be introduced using a type family, but type families (like functions) may pattern match only on constructors, not other type families, in the interests of checking consistency and termination of constraint solving (Schrijvers et al. 2008). In any case, type families are typically useful only if they define a terminating rewrite system, but associativity and commutativity are hardly going to do so!

8 Unfortunately

this means that Quantity a u cannot be an instance of the standard Haskell Num typeclass, which bundles addition, subtraction and multiplication together. An instance may be given only for Quantity a 1.

13

2.2.1

3.

An Attempted Solution

Domain-Specific Constraint Solving

Haskell type inference is essentially a problem of generating and solving constraints. These may be equalities, which arise from the typing rules (e.g. in the application f x , the compiler must check that f has a function type with domain equal to the type of x ), or typeclass constraints, which arise from uses of overloaded functions. Similarly, standard Hindley-Milner type inference amounts to a constraint generation and solving process in which the solver performs first-order unification (Sulzmann et al. 1999).

This is the point where most Haskell units of measure libraries give up on providing exactly the desired equational theory given above. Instead, a common approach is to write a normalisation function for concrete unit expressions (typically implemented as a type family). Provided one is very careful to talk only about equality of normal forms, not the original syntax of unit expressions, this allows some of the desired behaviour. For example, the units package defines addition of quantities with this type:9

GHC uses the OutsideIn(X) algorithm (Vytiniotis et al. 2011) to handle the constraints it generates. This is notionally parametric in the choices of constraint domain X and solving algorithm, and provides domain-independent conditions that the constraint solver must satisfy. However, in practice there is only one choice for the solver: GHC implements the solver for type equality constraints (including type families) and typeclasses also described by Vytiniotis et al. (2011). To permit domain-specific equational theories, this solver must be made user-extensible. The user is not expected to replace the solver entirely, although the capability might be interesting (e.g. to experiment with other algorithms).

() :: (d1 @∼ d2 , Num n) ⇒ Qu d1 l n → Qu d2 l n → Qu d1 l n The constraint d1 @∼ d2 means that d1 and d2 should be compared up to equality of normal forms, a weaker condition than d1 ∼ d2 , which would require d1 and d2 to be equal in GHC’s equational theory. This weaker constraint means that GHC can determine that kg ∗ m and m ∗ kg have the same normal form, and hence quantities with those units may be added. Unfortunately, this has rather drastic consequences for error messages, because they are expressed in terms of normal forms. In units, the erroneous addition gives this:

In this section, I will describe how such a plugin constraint solver works, first as a practical Haskell program interfacing with GHC, then in the more formal theoretical setting of OutsideIn(X).

Couldn’t match type ‘[F Mass One, F Length (P Zero)]’ with ‘[]’ In the expression: mass |+| distance

3.1

Plugging in to GHC

Once GHC’s built-in constraint solver has finished its work, it is left with a set of constraints that it could not solve. The job of a plugin solver is to take this set of wanted constraints and either

Apart from introducing yet another equivalence relation for the user to understand, and mystifying them with error messages, we lose something important in the shift from (⊕) to (): well-behaved unit polymorphism. The normalisation approach breaks down when there are variables or other non-canonical unit expressions present. It cannot conclude that u  v is interchangeable with v  u, because it cannot compute the normal form of variables such as u and v . If we are lucky, we may be able to postpone the constraint until we have concrete values for the variables, and hence get away with only some messy types. If not, we may not be able to write the unit-polymorphic program we want.

• identify impossible constraints that GHC has failed to reject

outright, for example kg ~ kg ∼ m; or • solve or further simplify the constraints, perhaps generating

others in the process. When a plugin yields new constraints, the main GHC constraint solver will be re-invoked in case it can make further progress, the plugin will be called again, and so on.

For example, one would like the following function to be accepted with this type signature (or without it)

To be more precise, a plugin solver is a Haskell function supplied separately with ‘given’, ‘derived’10 and ‘wanted’ constraints:

f :: Num a ⇒ Quantity a u → Quantity a v → Quantity a (u ~ v ) f x y = (x ~ y) ⊕ (y ~ x )

solve :: [Ct] → [Ct] → [Ct] → TcPluginM TcPluginResult solve givens deriveds wanteds = ... Here Ct is GHC’s internal type of constraints, TcPluginM is a monad providing effects suitable for plugins, and TcPluginResult captures possible outcomes of constraint solving:

but the units approach leads to an inferred type like this, involving several internal type families used to implement normalisation: (Num a , [ ] ∼ Normalize (Normalize (d1 @@+ Reorder d2 d1 ) @− Normalize (d2 @@+ Reorder d1 d2 )) ) ⇒ Qu d1 l a → Qu d2 l a → Qu (Normalize (d1 @@+ Reorder d2 d1 )) l a

data TcPluginResult = TcPluginOk {solved :: [(EvTerm, Ct)], new :: [Ct]} | TcPluginContradiction {impossible :: [Ct]} The TcPluginOk case includes a list of solved constraints along with associated evidence (to be discussed in subsection 3.1.2), and a list of new constraints to be processed by the main solver. Note that it is possible for ‘given’ or ‘derived’ constraints to be solved, which simply means to drop them from consideration since they provide no useful information (e.g. consider a ~ 1 ∼ a). The result TcPluginOk [ ] [ ] indicates that no progress was made: no constraints could be solved and no new constraints were generated.

The crucial observation of this paper is that we need to introduce support for a domain-specific equational theory. In the following section, we will see how this is possible. 9

Instead of annotating quantities Qu with units directly, the units package uses a combination of dimension (d) and local coherent system of units (l); the difference is discussed in section 5.2. Boxed operators are definitions from units, to distinguish them from the circled operators of uom-plugin.  is written |+| in ASCII.

10 Derived

constraints arise during the constraint solving process, e.g. from functional dependencies; they will not be considered in any detail here.

14

tcPluginIO :: IO a → TcPluginM a -- Perform arbitrary IO tcPluginTrace :: String → SDoc → TcPluginM () -- Print debug message tcLookupGlobal :: Name → TcPluginM TyThing -- Look up a type or definition in the context newFlexiTyVar :: Kind → TcPluginM TcTyVar -- Create a fresh unification variable

Typeclasses D Data types T Type families F, G Rigid variables a, b, c Unification variables α, β, γ Type variables x, y, z ::= a | α Types τ ::= x | T | F τ | τ1 τ2 | . . . Constraints Q ::=  | Q1 ∧ Q2 | τ1 ∼ τ2 | D τ Substitutions θ, φ ::= [x 7→ τ ] Top-level axiom schemes Q ::= Q | Q1 ∧ Q2 | ∀ a . Q ⇒ D τ | ∀ a . F τ ∼ τ

Figure 1. A sample of the TcPluginM interface

Figure 2. Syntax of OutsideIn(X) types and constraints

The details of the TcPluginM monad interface is not important; a few example type signatures are shown in Figure 1. These include the ability to query the context (e.g. look up the definitions of types), generate fresh variables and perform IO operations. Arbitrary IO is not used in uom-plugin, but it is useful in other plugins.

(m s) ~ s ∼ m, which can be solved directly by another run of the plugin solver. Note that even this simple example involved two runs of the built-in solver and two runs of the plugin; while that could be avoided in this case if the plugin performed substitution and type family reduction itself, in general we would not want plugins to have to reimplement GHC’s entire solver!

Of course, plugins should be essentially pure, but this is a matter for the plugin implementor. More generally, what does it mean for a plugin to be well-behaved? One would expect it to be: • pure, i.e. producing the same result for the same inputs; • order-insensitive, i.e. regarding the constraint lists passed to the

3.1.2

solve function as sets (arguably the types should enforce this!); • sound, i.e. claiming to solve constraints only if they can actually

If a plugin claims to have solved a constraint, why should we believe it? It would be very easy to produce a plugin that erroneously11 reported constraints as solved when in fact they were not, potentially introducing type unsoundness and causing runtime crashes. GHC already has a mechanism for detecting such errors: it does not merely typecheck code, but elaborates it into System FC (Sulzmann et al. 2007), a very explicit core calculus that includes easily-checked evidence for type equality. This does not prevent all compiler bugs, but it does make constraint solver misbehaviour easier to detect.

be solved, to be elaborated on in subsection 3.1.2; and • most general, i.e. solving constraints without ‘guessing’, which

I will return to in section 4.3. 3.1.1

Evidence of Soundness

Plugin-Aware Constraint Solving

The algorithm GHC uses when solving constraints in the presence of a typechecker plugin is as follows:

Thus the actual implementation of plugins demands evidence for each constraint that the plugin claims to have solved. Some plugins may not be able to generate bona fide evidence, in which case they may use the equivalent of unsafeCoerce and assert a constraint without proof. On the other hand, the author of a plugin may create their own axioms and build genuine evidence from them, in which case they can be sure of the type soundness of the resulting system (provided the axioms they introduce are consistent, of course!).

1. Run the built-in constraint solver, producing a set of constraints that it could neither solve nor show inconsistent. 2. Call the plugin with the remaining constraints: • if it returns TcPluginContradiction, report the impossible

constraints and stop; • if it returns TcPluginOk with some new constraints, remove

the solved constraints from the constraint set, add the new ones, then start again from the beginning;

In the implementation, the type EvTerm returned with a constraint in a TcPluginOk result represents terms in the evidence language. Forms of evidence include variables, axioms, typeclass dictionaries and a variety of deduction rules for equality proofs. I will not consider evidence further here, but it is discussed in more detail by Vytiniotis et al. (2012).

• if it returns TcPluginOk with no new constraints, remove

the solved constraints from the constraint set and stop. For example, suppose GHC has arrived at a point in the typechecking process where it has some type family F :: Unit → ∗, a given constraint F (m s) ∼ () , an as-yet unsolved unification variable α, and wanted constraints

3.2

Plugging in to OutsideIn(X)

Having seen how the plugin mechanism works in practice, let us step back and consider the theory justifying it. The OutsideIn(X) framework expects a constraint solver which takes four inputs (with the syntax given in Figure 2):

F α ∼ (), (α ~ s) ∼ m, that have already been simplified as far as possible by the built-in constraint solver. The plugin solver can now run and output a new wanted constraint α ∼ m s, leading to the wanted constraints

• user-defined top-level axiom schemes Q (e.g. from typeclass

and type family instances);

F α ∼ (), (α ~ s) ∼ m, α ∼ (m s) .

• ‘given’ constraints Qgiven known to be true locally (e.g. from

type signatures or GADT pattern matches);

Now the built-in solver can make further progress, substituting for α and using the given constraint to discharge the first goal, leaving

11 Or

maliciously, though plugins are assumed to be trusted: they can run arbitrary IO actions from within the typechecker, which is dangerous!

15

Q ∈ Q implies Q Q Q Q1 and Q ∧ Q1 Q2 implies Q Q2 Q Q implies θ Q θ Q

(R1) (R2) (R3)

Q Q Q Q Q

(R4) (R5) (R6) (R7) (R8)

τ ∼τ

τ1 ∼ τ2 implies Q τ2 ∼ τ1

τ1 ∼ τ2 and Q τ2 ∼ τ3 implies Q τ1 ∼ τ3

Q1 and Q Q2 implies Q Q1 ∧ Q2

τ1 ∼ τ2 implies Q [a 7→ τ1 ] τ ∼ [a 7→ τ2 ] τ

GO

Q ; Qg ; α0 `Isimp Qw Qr ; θ0 Qs Qs * Qr Q ; Qg ; α1 `Ip Qr Q ; Qg ; α2 `Ipsimp Qs Qt ; θ1 Q ; Qg ; α0 `Ipsimp Qw Qt ; θ0 ◦ θ1 | α0 S TOP

Q ; Qg ; α0 `Isimp Qw Qs Q ; Qg ; α1 `Ip Qr psimp Q ; Qg ; α0 `I Qw

Figure 3. Properties of entailment13

Qr ; θ Qs ⊆ Qr Qs ; θ

Figure 4. Plugin-extended OutsideIn(X) solver

• ‘touchable’ unification variables αtch (those for which the

algorithm is allowed to solve); and • ‘wanted’ constraints Qwanted for which solutions are to be

constraints to the plugin. If the plugin generates new constraints (i.e. Qs * Qr ), the G O rule applies and invokes the combined solver judgment again. If not (i.e. Qs ⊆ Qr ), the S TOP rule will simply return the remaining constraints.

found. In response, the constraint solver must produce two outputs:

Note that this process can be iterated, starting with the basic solver and extend it with multiple plugins.

• a substitution θ for the touchable variables αtch ; and • residual constraints Qresidual that could not be solved (but may

The combined judgment ` Ipsimp will satisfy the OutsideIn(X) conditions on the assumption that ` Isimp satisfies them, and provided that `Ip satisfies the conditions

have been simplified). The behaviour of the constraint solver is described by the judgment Q ; Qgiven ; αtch `Isimp Qwanted

(Plugin soundness) (Plugin principality)

Qresidual ; θ

which must satisfy certain conditions in order for OutsideIn(X) type inference to be sound and most general:12 (Soundness) (Principality)

i.e. Qr and Qs should be equivalent under the given constraints. In section 4.3 I will show that the units of measure plugin I am about to describe satisfies the soundness condition as-is, but satisfies only a weakened form of the principality condition.

Q ∧ Qgiven ∧ Qresidual θ Qwanted Q ∧ Qgiven ∧ Qwanted Qresidual ∧ Eθ where Eθ = {x ∼ u | [x 7→ u] ∈ θ}

That is, the constraint solver must deliver a solution that is sound, i.e. the residual constraints solve the original problem, and most general, i.e. the simplifier has not ‘guessed’ any values for variables or invented constraints not entailed by the original problem. Here Q Q is the constraint entailment relation, part of the X parameter of OutsideIn(X), which satisfies the properties given in Figure 3. 3.2.1

4.

Units of Measure as a Typechecker Plugin

Having seen the general structure of typechecker plugins, let us consider a specific example. The uom-plugin constraint solver is designed to deal with equality constraints between types of kind Unit. Essentially it performs equational unification for the theory of free abelian groups. Recalling the earlier example, GHC’s built-in constraint solver might have been left with the unsolved constraint

Defining a Plugin Constraint Solver

Base "m" ~ Base "kg" ∼ Base "kg" ~ Base "m"

A plugin constraint solver can be simpler than the description in the OutsideIn(X) framework, since it need not stand alone but will be combined with the built-in solver. In particular, the plugin need not deal with producing a substitution for unification variables directly. Instead, it may simply add constraints that define variables.

but it is easy to see that this constraint is trivial simply by normalisation up to the group axioms. For constraints involving unification variables, Kennedy (1996, 2010) describes an algorithm for AG-unification that proceeds by a variant of Gaussian elimination, and shows how to extend this to types containing units of measure. For example, given the constraint

Suppose we have a judgment form Q ; Qgiven ; αtch `Ip Qr

Q ∧ Qgiven ∧ Qs Qr Q ∧ Qgiven ∧ Qr Qs

Qs

meaning that the constraints Qr can be simplified to Qs under the given assumptions. Figure 4 shows how such a judgment can be combined with the built-in solver judgment `Isimp to produce `Ipsimp , which conforms to the OutsideIn(X) interface.14

α~α ∼ β~β~β the most general solution is α ∼ γ ~ γ ~ γ, β ∼ γ ~ γ

The basic idea is that of the implementation, discussed in subsection 3.1.1: run the main constraint solver once, then pass the residual

for some fresh unification variable γ. Since AG-unification is decidable and possesses most general unifiers, type inference in an ML-like setting is well-behaved, though the let-generalisation step is slightly subtle (Gundry 2013).15

12

There are also some technical conditions on the domain of the substitution, which require that it substitutes only for touchable variables not occurring in the given or residual constraints. 13 Slightly reformulated from Vytiniotis et al. (2011) 14 The details of how to calculate the sets of touchable variables α and 1 α2 are omitted; it is straightforward but messy to add newly generated unification variables and remove those that have been substituted away.

The situation is slightly more complex in the case of the full GHC Haskell type system, in particular because of the possible presence of 15 GHC

no longer generalises let-bindings by default in the presence of type families or GADTs, for essentially the same reason.

16

Unit constraints Unit normal forms Atoms Base units

U u r b

::= ::= ::= ::=

 | U1 ∧ U2 | u1 ∼ u2 1 | r1i1 · · · rnin x | b | F(τ ) kg | m | . . .

A SSOCIATIVE

I DENTITY

Q u1 ~ (u2 ~ u3 ) ∼ (u1 ~ u2 ) ~ u3

Q u~1∼u

Figure 5. Syntax of unit constraints

C OMMUTATIVE

I NVERSE

Q u1 ~ u2 ∼ u2 ~ u1

Q u ~ (1 u) ∼ 1

T ORSION - FREE

Q u ~ u ~ ··· ~ u ∼ 1 Q u∼1

universally quantified variables, type families and local constraints. Thus the plugin constraint solver may encounter constraints like a ~a ∼ b~b~b

Figure 6. Constraint entailment rules for units

where a and b are universally quantified variables, or Fa ~Fb ∼ Fb~Fa

In the interests of simplicity, failure is not represented explicitly here, although in practice it is useful to identify obviously impossible constraints (such as kg ∼ m), and the implementation does this using the TcPluginContradiction result (see section 3.1).

where F is a user-defined type family. Moreover, it has to deal with constraints that are ‘given’ as well as ‘wanted’, so it must simplify hypotheses as well as solving goals.

Rule (1) simply ensures that all unit equations are in the form u ∼ 1. Rule (2) solves trivial equations; since unit normal forms are being considered up to the abelian group laws, this includes cases such as α · α−1 ∼ 1.

The essence of the plugin’s constraint solving algorithm is to 1. identify unsolved equality constraints between units; 2. normalise both sides of each constraint up to the group axioms;

Rule (3) is the first to produce an output substitution, in the case where some variable can be instantiated to solve the equation. For example, m4 · α2 ∼ 1 is solved by substituting [α 7→ m2 ]. Of course, the variable must not belong to the list of fixed variables z. Again this rule is interpreted up to the group laws.

3. incrementally simplify given constraints by rewriting them to simpler, equivalent constraints; 4. incrementally simplify wanted constraints, making use of the information from simplifying the givens.

The most complex rule is (4), which shows how progress can be made in cases where rule (3) does not apply and so the equation cannot immediately be solved. It relies on the fact that any unit can be expressed as a product of distinct atoms r1i1 · · · rnin . By replacing x with a fresh variable y multiplied by a suitably-chosen unit v, the exponents of the atoms can be reduced. Note that y should be a rigid variable iff x is rigid. For example, this rule introduces a fresh variable c to simplify a2 · b−3 ∼ 1 to c2 · b ∼ 1 with θ = [a 7→ c · b2 ], φ = [c 7→ a · b−2 ].

For example, the wanted constraint α ~ α ∼ (β ~ β) ~ β equates two types of kind Unit, which normalise to give α2 ∼ β 3 . Once normalised, the constraint α2 ∼ β 3 can first be rewritten to α2 · β −3 ∼ 1. This can be simplified by substituting by α ∼ γ · β where γ is fresh, leading to γ 2 · β −1 ∼ 1. Rearranging this gives β ∼ γ 2 . Hence the solution is α ∼ γ 3 ∧ β ∼ γ 2 . Normal forms will be written in mathematical notation, as shown in Figure 5, to contrast them with Haskell type expressions. A unit normal form u is a product of distinct atoms r with nonzero integer exponents. 1 represents the empty product. If two types of kind Unit are equal under the group axioms in Figure 6, then they will have the same normal form (e.g. x ~ 1 and (y ~ x ) y both denote the same normal form x1 ). I use U for constraints Q that include only equations between units.

Rule (5) says that a conjunction of constraints can be simplified by simplifying one and applying the resulting substitution to the other. Just as units are considered up to the abelian group laws, conjunctions should be treated as sets, so this rule allows any constraint to be simplified. The rules can be iterated in the obvious way to define a relation U 7→∗z U 0 (θ, φ) that makes multiple simplification steps, composing the resulting substitutions.

The presence of universally quantified variables or type families means that some constraints may not be solved immediately, but they may become feasible once other information has become available. This motivates a dynamic unification algorithm: one that makes progress on some constraints in the hope that others may become easier to solve. Since each step replaces a constraint with an equivalent constraint (up to the equational theory), it is most general, and so we can apply simplification steps in any order. 4.1

4.2

Instantiating the OutsideIn(X) Plugin Framework

Recall that a plugin must supply a judgement Q ; Qg ; αtch `Ip Qr

Qs

that explains how the given constraints Qg and wanted constraints Qr are simplified to produce the residual constraints Qs . This judgment is defined by

The Constraint Solving Algorithm

For units of measure unification, the algorithm is given by the rules in Figure 7, which define the relation U0 7→ z U1 (θ, φ). This explains how to rewrite a unit constraint U0 into a new constraint U1 and a pair of substitutions (θ, φ) that express the equivalence of the original and simplified constraints, modulo the group laws. The list of type variables z records those that may not be modified during unification, including both rigid and unification variables.16

Ug 7→∗· Ug0 (θg , φg ) θg Uw 7→∗z Uw0 (θw , φw ) z = fv(θg Uw ) ∪ fuv(θg Uw ) \ αtch Q ; Qg ∧ Ug ; αtch `Ip Qw ∧ Uw Qw ∧ φg (Uw0 ∧ Eθw ) where Qg and Qw are the non-unit given and wanted constraints, respectively, and Eθ = {x ∼ u | [x 7→ u] ∈ θ} is the constraint form of a substitution θ.

16 A rigid

variable is one that arises from universal quantification. Unification solves for flexible unification variables, but may not choose values for rigid

ones. Parameterising the rules by forbidden rather than touchable variables is a notational shortcut, to save changing the set when adding a fresh variable.

17

u∼v

7→ z

u · v −1 ∼ 1

(·, ·)

1∼1

7→ z



(·, ·)

k

k

k

r1i1

x ·u ∼1 x ·

· · · rnin

7→ z ∼1

U0 ∧ U1

if v 6≡ 1

(2)

([x 7→ u

 k

r1i1 mod k

7→ z

y ·

7→ z

U00 ∧ θU1

· · · rnin mod k

∼1

(1)

−1

], ·)

([x 7→ y · v], [y 7→ x · v

−1

(θ, φ)

])

if x ∈ /z

(3)

if x ∈ / z, ∃j. |k| 6 |ij |, y fresh, −bi /kc −bi /kc v = r1 1 · · · rn n

(4)

if U0 7→ z U00 (θ, φ)

(5)

Figure 7. Plugin constraint-solving algorithm Type safety depends on the fact that the relation is consistent (i.e. it cannot prove that two observably distinct types are equal). Consistency is not threatened by the group laws, because they refer only to type families without equations.17 If ~ was a constructor rather than a type family, however, it would be possible to derive a contradiction.

This rule assumes without loss of generality that the unit constraints Ug and Uw are already in normal form; this is justified since every type of kind Unit is provably equal to its normal form according to the entailment relation. First, the given unit constraints Ug are rewritten according to the simplification rules in Figure 7 until no more rules apply. This produces a substitution θg that may eliminate some rigid variables, possibly generating some fresh rigid variables in the process, but with a substitution φg that relates them back to the original variables. Here z is empty because rigid variables may be simplified using the given constraints; they will contain no unification variables. The simplified givens Ug0 are discarded.

In the following, I assume that the constraint entailment relation satisfies the conditions in Figure 3 and this additional condition: Suppose Q Eθ . Then Q Q iff Q θ Q and Q ∧ Q Q0 iff Q ∧ θQ Q0 .

This says that equalities are substitutive: if the equality constraints Eθ = {x ∼ u | [x 7→ u] ∈ θ} hold, then applying the substitution θ does not change the truth of a proposition. This should be the case for any reasonable entailment relation, in particular the concrete entailment relation used by Vytiniotis et al. (2011).

Next, the substitution θg is applied to the wanted unit constraints Uw (in order to eliminate rigid variables if possible), then they are rewritten according to the algorithm, producing a simplified set of constraints Uw0 and a substitution θw . At this point, only the ‘touchable’ unification variables may be instantiated, so z contains all the free variables that are not listed in αtch .

The basic result about the rewrite system needed to show that solutions are both sound and most general is the following, which amounts to showing that rewriting produces equivalent constraints, assuming the substitutions hold as a equations as appropriate. I write Q0 ↔ Q1 to mean that the constraints Q0 and Q1 are equivalent in the sense that Q0 Q1 and Q1 Q0 .

Finally, the residual constraints returned by the rule consist of the unchanged non-unit wanteds Qw , the simplified unit wanteds Uw0 and the constraint form of the substitution θw . The substitution φg , which eliminates any fresh rigid variables introduced when simplifying the unit givens, is applied where necessary.

Lemma 1 (Soundness and generality of unification steps). If U0 7→ z U1 (θ, φ) then U1 ∧ Eθ ↔ U0 ∧ Eφ .

For example, suppose we have Ug = {a2 ∼ b3 },

Uw = {γ 3 ∼ a},

(P)

Proof. By induction on the definition of the 7→ relation. αtch = {γ}

For rule (1), we need to show u · v −1 ∼ 1 ↔ u ∼ v, which follows straightforwardly from the group axioms. Similarly, rule (2) is trivial.

where a and b are rigid variables and γ is a unification variable. Rewriting the given constraint generates a fresh rigid variable c and produces θg = [a 7→ c−3 , b 7→ c−2 ], φg = [c 7→ a·b−2 ]. Applying θg leaves us with the wanted constraint γ 3 ∼ c−3 , which is easily solved by θw = [γ 7→ c−1 ]. In order to eliminate the variable c introduced by simplifying the given constraint, we apply φg , so we end up with the solution φg θw = [γ 7→ a−1 · b2 ].

For rule (3), the interesting part is showing xk · uk ∼ 1 x ∼ u−1 . The T ORSION - FREE rule means that (x · u)k ∼ 1 implies x · u ∼ 1.

4.3

where v = r1 1 · · · rn n and w = r1i1 mod k · · · rnin which follows from the fact that r1i1 · · · rnin ∼ v −k · w.

For rule (4), we must show that y k · w ∼ 1 ∧ x ∼ y · v ↔ xk · r1i1 · · · rnin ∼ 1 ∧ y ∼ x · v −1 −bi /kc

Soundness and Generality

−bi /kc

mod k

,

For rule (5), we must show U00 ∧ θ U1 ∧ Eθ ↔ U0 ∧ U1 ∧ Eφ . By induction we have U00 ∧ Eθ ↔ U0 ∧ Eφ , and property (P) gives θ U1 ∧ Eθ ↔ U1 .

As discussed in section 3.2, OutsideIn(X) type inference is sound (i.e. it infers correct types for terms) and delivers principal types (i.e. any type that can be given to the term is an instance of the inferred type), under certain assumptions on the behaviour of the simplifier. These assumptions lead to conditions that the algorithm described above must satisfy.

In addition, the following lemma shows the relationship between the two substitutions θ and φ produced by the algorithm: applying φ to θ yields equations that follow from the input constraints U0 .

The conditions are formulated in terms of the relation, which satisfies the properties in Figure 3. To justify the soundness and generality of the plugin, additional inference rules are required stating that Unit is an abelian group, as shown in Figure 6. The A SSOCIATIVE, I DENTITY, C OMMUTATIVE and I NVERSE rules are the usual abelian group laws; the role of T ORSION - FREE (characterising free abelian groups) will be discussed later.

Lemma 2. If U0 7→ z U1 (θ, φ) then U0 Eφ◦θ . 17 GHC

7.10 does not make it possible to enforce that a type family has no equations, but the next release will support empty closed type families. Additionally, this relies on the assumption that all user-defined type families at kind Unit are well-defined (terminating).

18

Proof. By induction on the definition of the 7→ relation. The only rules that extend the substitution θ are (4), for which x ∼ u−1 follows by T ORSION - FREE, and (5), for which the composition is [y 7→ x · v −1 ] ◦ [x 7→ y · v] = [x 7→ (x · v −1 ) · v], the identity up to the group axioms.

Proof. Taking ψ = φw we must show that Q ∧ (Qg ∧ Ug ) ∧ (Qw ∧ Uw ) φw (Qw ∧ φg (Uw0 ∧ Eθw )) is derivable. Now dom(φw ) # fuv(Qw ) and Qw Qw , so it is enough to show Ug ∧ Uw φw φg (Uw0 ∧ Eθw ). Lemma 1 gives θg Uw ∧ Eφw ↔ Uw0 ∧ Eθw , so by (R3) we have φw φg (θg Uw ∧ Eφw ) ↔ φw φg (Uw0 ∧ Eθw ), which implies that φw φg θg Uw φw φg (Uw0 ∧ Eθw ). Now we must have dom(φw ) # fuv(φg θg Uw ) so φg θg Uw φw φg (Uw0 ∧ Eθw ). Moreover Lemma 2 gives Ug Eφg ◦θg so we can deduce the required entailment using property (P).

From these results, which extend inductively in the obvious way to multiple reduction steps, it follows that the constraint solver is sound in the sense required by OutsideIn(X). Theorem 1 (Soundness). If Q ; Q1 ; αtch `Ip Q2 Q ∧ Q1 ∧ Q3 Q2 .

Q3 then

Proof. Recall from section 4.2 that we define `Ip by

That is, the solution found by the algorithm may not be guessfree in the original sense, but there is some substitution for the fresh variables it introduces by which it can be transformed into a guess-free solution. I conjecture that this weaker property is in fact sufficient for the proof that OutsideIn(X) type inference (if it succeeds) delivers principal types.18

Ug 7→∗· Ug0 (θg , φg ) θg Uw 7→∗z Uw0 (θw , φw ) z = fv(θg Uw ) ∪ fuv(θg Uw ) \ αtch Q ; Qg ∧ Ug ; αtch `Ip Qw ∧ Uw Qw ∧ φg (Uw0 ∧ Eθw ) hence we have Q1 = Qg ∧ Ug , Q2 = Qw ∧ Uw , Q3 = Qw ∧ φg (Uw0 ∧ Eθw )

The underlying problem here is that OutsideIn(X) does not have a clear notion of scope for type variables: it is not the case that

and we need to show that

α2 ∼ β 3 ↔ α ∼ γ 3 ∧ β ∼ γ 2 ,

Q ∧ (Qg ∧ Ug ) ∧ (Qw ∧ φg (Uw0 ∧ Eθw )) Qw ∧ Uw .

but rather we must contextualise the variables, as in

Since Qw Qw it is sufficient to show Ug ∧

φg (Uw0

∃α.∃β. α2 ∼ β 3 ↔ ∃α.∃β.∃γ. α ∼ γ 3 ∧ β ∼ γ 2 .

∧ Eθw ) Uw .

In fact the same problem shows up in the algorithm described by Vytiniotis et al. (2011), which reduces the wanted constraint F(G(x)) ∼ y to F(β) ∼ y ∧ G(x) ∼ β where F and G are type families and β is a fresh unification variable; it would appear that

We are justified in reasoning up to unit normal forms since if u and v are equivalent normal forms then  u ∼ v so (R8) gives  [a 7→ u] τ ∼ [a 7→ v] τ . Lemma 1 gives θg Uw ∧ Eφw ↔ Uw0 ∧ Eθw , so from (R3) we have φg (Uw0 ∧ Eθw ) φg (θg Uw ). Moreover Lemma 2 gives Ug Eφg ◦θg , so property (P) gives the required entailment.

F(G(x)) ∼ y 6 F(β) ∼ y ∧ G(x) ∼ β contrary to their Lemma 7.2. On another note, observe that the proofs relied on an additional rule, T ORSION - FREE, beyond the usual laws of an abelian group. This is crucial for proving both that solutions to wanted constraints are most general, and that simplifications of given constraints are sound. It amounts to restricting models of Unit to being free abelian groups, i.e. those generated by the base units and abelian group laws but with no other equations.19

Principality is more interesting, however. This requires that the constraint solver delivers most general solutions, which intuitively means that it makes no ‘guesses’ that are not implied by the original wanted constraints. Vytiniotis et al. (2011) define ‘guessfree solutions’ as those where the wanted constraints entail the residual constraints; for the plugin this amounts to requiring Q ∧ (Qg ∧ Ug ) ∧ (Qw ∧ Uw ) Qw ∧ φg (Uw0 ∧ Eθw ).

Without T ORSION - FREE, the addition of an axiom kg ~ kg ∼ 1 would be consistent, but then it would no longer be most general to solve the wanted α ~ α ∼ 1 with α ∼ 1, nor would it be sound to simplify the given a ~ a ∼ 1 to a ∼ 1, as in either case kg is an alternative solution.

Unfortunately, this is not true! Consider the sole wanted constraint α2 ∼ β 3 , which according to the algorithm in Figure 7 can be solved by θ = [α 7→ γ 3 , β 7→ γ 2 ] where γ is a fresh unification variable. The guess-free solution condition would require us to show α2 ∼ β 3 α ∼ γ 3 ∧ β ∼ γ 2 ,

5.

and this is simply not derivable, because the fresh variable γ has been conjured out of thin air.

Related Work

The design of uom-plugin owes a lot to Andrew Kennedy’s implementation of units of measure in F#, and Richard Eisenberg’s units Haskell library. I compare it with each of them in turn. While there are several other Haskell libraries for units of measure20 , making slightly different design choices, units represents the state of the art and the comparison is broadly representative.

So what has become of the claim that the steps of the algorithm described above are all most general? The unifier θ is indeed the most general unifier, in the sense that any unifying substitution for this equation must determine γ and agree with θ up to the abelian group laws. The fresh variable γ must be α · β −1 , but the definition of guess-free solutions does not make use of this knowledge.

18 Theorems

3.2 and 5.2 of Vytiniotis et al. (2011) abelian groups are always torsion-free, and torsion-free finitely generated abelian groups are free. 20 Notably dimensional-dk by Björn Buckwalter and Douglas McClean (https://github.com/bjornbm/dimensional-dk)

Instead, we can prove a weaker result:

19 Free

Qs then Theorem 2 (Generality). If Q ; Qg ; αtch `Ip Qr Qg ∧ Qr ψ Qs for some subsitution ψ for the freshly introduced variables, i.e. with domain fuv(Qs ) \ fuv(Qr ).

19

5.1

Units of Measure in F#

dimensions, but the approach described in this paper should be able to scale to handle dimensions. The best way to represent them, and provide features such as automatic conversion between units of the same dimension, is a matter of ongoing work.

The plugin described in this paper provides support for units of measure that is inspired by, and broadly comparable with, Kennedy’s implementation in F#.21 Constants and numeric types can be annotated with units, units may be polymorphic, and unit equations that arise during typechecking are solved by abelian group unification.

5.3

This paper described a plugin to support units of measure by providing a special-purpose constraint solving algorithm based on abelian group unification. In contrast, Diatchki (2015) describes type-nat-solver,22 a plugin that interfaces with an SMT solver to handle constraints arising from type-level natural numbers. In principle, the SMT solver approach could be extended to deal with other domains, such as abelian groups.

Working in Haskell introduces many new feature interactions to explore, notably with typeclasses, GADTs, type families, higherkinded and higher-rank types. For example, Haskell allows definitions that are polymorphic in type constructors of kind Unit → ∗. On the other hand, while the GHC typechecker plugins support makes some exciting new things possible, a plugin cannot (yet) extend GHC with a completely new language feature. In particular, Template Haskell quasiquotation allows the introduction of new syntax (e.g. for expressions containing quantities with units, or types mentioning units), but this syntax will not be used in output (such as error messages or inferred types). Thus the user can write

However, an SMT solver is designed to determine whether or not a given collection of constraints is satisfiable. If so, it will typically produce a satisfying assignment of values to variables. This is not immediately enough for use in type inference, which requires finding most general solutions to constraints involving unification variables.

[u| 5 m/s |] :: Quantity Int [u| m/s |]

For example, a constraint like α · β ∼ 1 has many satisfying assignments (such as α ∼ 1, β ∼ 1) but we need to determine the most general solution (namely α ∼ β −1 ). It is possible to ‘improve’ constraints in an ad-hoc or theory-specific way, by guessing a candidate constraint and testing whether it follows from the other constraints, but this makes it hard to specify exactly which type inference problems will be solved by the system.

but the inferred type of this expression is the less easy to read Quantity Int (Base "m" Base "s") Moreover, there is no way to simplify an inferred type in a domainspecific manner. Thus a type may sometimes be presented as Num a ⇒ Quantity a (Base "s" ~ (Base "m" Base "s"))

Thus there is room for experimentation with both special-purpose unification algorithms (such as that described in the present paper) and application of general SMT solvers to type inference. The typechecker plugins framework described in section 3 offers a common theoretical basis for both techniques. A more radical step is to change the type system so that typechecking generates verification conditions directly, rather than unification problems, as in work on refinement types (Vazou et al. 2014).

rather than the (equivalent) Num a ⇒ Quantity a (Base "m") It should be relatively straightforward to extend GHC’s plugin support to allow extensions to pretty-printing and presentation of inferred types, but there will always be limitations of the plugin technique compared to building support into the language, as in F#. 5.2

Plugging in an SMT Solver

The units Package

6.

Another key inspiration for this work is the units library (Muranushi and Eisenberg 2014), which is the state of the art as far as units of measure in Haskell are concerned. As discussed in subsection 2.2.1, uom-plugin is able to achieve better type inference behaviour and more comprehensible error messages than units thanks to the use of a typechecker plugin, rather than encoding everything using type families and other existing GHC features. On the other hand, since units does not require a plugin it is more broadly compatible and avoids the potential for plugin-introduced bugs. Moreover, it makes use of Template Haskell to permit a relatively nice input syntax.

Conclusion

In this paper, I have introduced the notion of typechecker plugins both as an implementation technique in GHC and in terms of the OutsideIn(X) framework. I have made use of this to define a library for units of measure with good type inference properties, in particular the ability to find most general solutions to constraints arising from unit polymorphism. Practical use of plugins is still at an early stage, as they are quite low level and closely tied to GHC’s constraint solver. There is much to do to build better abstractions on top of the low-level interface, and hence make it easier to write plugins without deep knowledge of GHC. Termination of constraint solving in the presence of plugins is a particularly tricky issue. It is quite easy for a poorly written plugin to create an infinite loop, for example by emitting a new but trivial constraint each time it is invoked. Moreover, while evidence generation gives some indication of soundness (albeit not consistency of the axiom system used to produce the evidence), it is hard to ensure that plugins deliver most general solutions to constraints.

Another crucial difference in library design is that units is based around working with dimensions (such as length and mass), rather than units directly. A dimension has a ‘canonical’ unit that determines how quantities are represented, but they may be introduced or eliminated using other units, with appropriate conversions performed automatically. There is even support for working with multiple local coherent systems of units (choices of canonical units for dimensions) in different parts of a single program. This allows code to be typechecked for dimension safety, but remain polymorphic in the particular units, and makes it easier to avoid numeric overflow errors when working with quantities at vastly different scales. In the interests of simplicity, the uom-plugin library follows F#’s approach of indexing types by units of measure alone, not including

Two main avenues for future work are extending the uom-plugin library itself, and adding features to GHC that make more powerful plugins possible. I will consider these, then suggest some possible other applications for the concept of typechecker plugins.

21 Prior

22 https://github.com/yav/type-nat-solver

to the upcoming F# 4.0, which will support fractional units.

20

6.1

Further Support for Units of Measure

Termination and completeness On a more theoretical note, it would be nice to prove that the plugin-extended constraint solver terminates, and is complete in an appropriate sense. Unfortunately, both of these are tricky issues in OutsideIn(X) even before plugins are added, and modular reasoning about termination is particularly difficult.

Evidence generation The prototype units of measure plugin does not yet support evidence generation (see subsection 3.1.2); rather, it follows the method of proof by blatant assertion. In principle it should be possible to generate proofs based on the abelian group axioms from Figure 6. This would allow GHC’s -dcore-lint option to check that the plugin is generating correct output.

Extending the algebraic structure Finally, while indexing quantities by a single abelian group of units is a reasonable point in the design space, there are other choices for the model of units and quantities. For example:

Formally, this would amount to translating the proofs of Lemma 1 and Theorem 1 into a program that generated evidence using the appropriate combination of group axioms for each constraint solving step. It would be slightly weaker than a fully mechanised correctness proof for the algorithm, however, as it would only ensure correctness on individual inputs (rather than for all possible inputs).

• Dimensions such as length and time could be tracked separately,

and their consistency checked, as in units (see section 5.2). √ Hz (i.e. Hz1/2 ), which arises when quantifying electronic noise levels.

• Fractional units are sometimes useful, such as

Automatic conversion inference As observed above, representing dimensions and inferring conversions between quantities of the same dimension is a matter of ongoing investigation. This is not essential, because the user can always write their own conversions, but it would be better if they were able to write something like

• Multiple origins need to be considered to handle units of temper-

ature, since 0C ≈ 273K. It may be possible to handle these by indexing quantities by an abelian group of translations as well as units (Atkey et al. 2013).

r = convert [u| 10 ft/min |] :: Quantity Double (Base "m" Base "s")

• Logarithmic units such as dBm require arithmetic operations

like ⊕ to be given different types.

and have the compiler automatically insert the conversion from ft/min to m/s.

There is a direct trade-off between simplicity and expressivity of the system. The example of F# suggests that the simple abelian group model of units is useful in practice.

One way to encode such automatic conversions is through the definition of a pair of additional type families: Pack, which converts a list of (base unit, integer exponent) pairs into the corresponding unit, and Unpack, which represents a fully known unit as a list of such pairs (in a canonical order). Thus we have:

6.2

Extensions to the Plugins Mechanism

Apart from the constraint solver, there are many other points where it would be useful for typechecker plugins to be able to extend the compiler with domain-specific behaviour:

type family Pack (xs :: [(Symbol, Integer)]) :: Unit where Pack [ ] =1 Pack ((b, i) : xs) = (Base b ? i) ~ Pack xs type family Unpack (u :: Unit) :: [(Symbol, Integer)]

• control over simplification and presentation of inferred types, as

discussed in section 5.1; • easily defining special reduction behaviour for type families,

Pack [("m", Pos 2), ("s", Neg 1)] = Base "m" ? 2 Base "s" Unpack (Base "m" ? 2 Base "s") = [("m", Pos 2), ("s", Neg 1)]

such as the Unpack type family described in section 6.1; • manipulating error messages, for example so that a DSL imple-

mentor can provide domain-specific guidance on likely reasons for a certain class of error, along the lines of error reflection in Idris (Christiansen 2014).

(Here ? represents exponentiation for units, and type-level integers are represented as natural numbers with a Pos or Neg constructor.) 6.3

Pack can be defined via a standard closed type family, but Unpack must be defined specially by the plugin because it observes the structure of the unit. It respects the equational theory on units, and hence does not break type soundness.

Other Applications for Plugins

Beyond units of measure and type-level numbers, there are many other potential applications for typechecker plugins:

Together, these type families make it possible to encode the convert function and other advanced features using existing GHC Haskell type-level programming techniques. However, once more it becomes a challenge to make error messages simple and comprehensible. It is an interesting challenge to extend the units of measure library further while maintaining a suitable balance between features implemented in the plugin and those encoded using existing functionality.

• permitting injective type families (Stolarek et al. 2015); • indexing a monad by the available effects, using a solver for a

theory of sets, maps or boolean rings, as in the effect-monad library of Orchard and Petricek (2014); • typeclasses such as Coercible (Breitner et al. 2014) and

Typeable (Lämmel and Peyton Jones 2003), with non-standard search strategies rather than the usual instance search;

Construction of quantities It is slightly unsatisfying that constructing literal quantities in a safe way fundamentally requires Template Haskell, rather than it providing mere syntactic sugar. One alternative is to expose the MkQuantity constructor to the user, and require them to follow a suitable syntactic discipline in its use: always instantiating its type to concrete units. A way to lift this restriction would be beneficial, but by no means essential.

• adding η-laws for type-level tuples or record types; and • record system extensions, such as extensible records via row

polymorphism, or the proposed OverloadedRecordFields.23 23 https://ghc.haskell.org/trac/ghc/wiki/Records/

OverloadedRecordFields

21

In particular, it would be interesting to try factoring out an existing piece of GHC functionality (such as the Coercible or Typeable typeclasses) into a plugin, increasing modularity. One could even imagine disabling the entire built-in constraint solver, allowing experimentation with alternative algorithms, although this is likely to be practically difficult as it would require the plugin to represent substitutions more directly.

Andrew Kennedy. Programming Languages and Dimensions. PhD thesis, University of Cambridge, 1996. URL http://research.microsoft.com/en-us/um/people/akenn/ units/ProgrammingLanguagesAndDimensions.pdf. Andrew Kennedy. Types for units-of-measure: Theory and practice. In Zoltán Horváth, Rinus Plasmeijer, and Viktória Zsók, editors, Central European Functional Programming School, volume 6299 of LNCS, pages 268–305. Springer, 2010.

More generally, the existing typechecker plugin interface is at a relatively low level, requiring the plugin implementer to have a fairly detailed knowledge of the way type inference is implemented in GHC (e.g. to generate evidence using its internal data types). A broader challenge for future work is to find a suitable interface that is both powerful enough to implement special-purpose constraint solver behaviour, and simple enough to make the creation of domainspecific constraint solvers accessible to more users. Hopefully it should be possible to build such a higher-level interface on top of the existing typechecker plugins support in GHC.

Ralf Lämmel and Simon Peyton Jones. Scrap your boilerplate: A practical design pattern for generic programming. In Proceedings of the 2003 ACM SIGPLAN International Workshop on Types in Languages Design and Implementation, TLDI ’03, pages 26–37. ACM, 2003. Daan Leijen and Erik Meijer. Domain specific embedded compilers. In Proceedings of the 2nd Conference on Domain-Specific Languages, DSL ’99, pages 109–122. ACM, 1999. Takayuki Muranushi and Richard A. Eisenberg. Experience report: Typechecking polymorphic units for astrophysics research in Haskell. In Proceedings of the 2014 ACM SIGPLAN Symposium on Haskell, Haskell ’14, pages 31–38. ACM, 2014. Dominic Orchard and Tomas Petricek. Embedding effect systems in Haskell. In Proceedings of the 2014 ACM SIGPLAN Symposium on Haskell, Haskell ’14, pages 13–24. ACM, 2014.

Acknowledgments Richard Eisenberg kindly gave detailed feedback on an early draft of this paper, in addition to offering inspiration through his units library. My thanks go to Neil Ghani and Conor McBride for ongoing discussion of this work. I’m grateful to attendees at various meetings, and to the anonymous reviewers, for their feedback. This paper was typeset using lhs2TeX.24 The work was funded by the University of Strathclyde EPSRC Impact Acceleration Account.

Tom Schrijvers, Simon Peyton Jones, Manuel Chakravarty, and Martin Sulzmann. Type checking with open type functions. In Proceedings of the 13th ACM SIGPLAN International Conference on Functional Programming, ICFP ’08, pages 51–62. ACM, 2008. Jan Stolarek, Simon Peyton Jones, and Richard A. Eisenberg. Injective type families for Haskell. In Proceedings of the 2015 ACM SIGPLAN Symposium on Haskell, Haskell ’15. ACM, 2015. In press. Martin Sulzmann, Martin Müller, and Christoph Zenger. Hindley/Milner style type systems in constraint form. Res. Rep. ACRC-99-009, University of South Australia, School of Computer and Information Science, 1999.

References

Martin Sulzmann, Manuel M. T. Chakravarty, Simon Peyton Jones, and Kevin Donnelly. System F with type equality coercions. In Proceedings of the 2007 ACM SIGPLAN International Workshop on Types in Language Design and Implementation, TLDI ’07, pages 53–66. ACM, 2007. Niki Vazou, Eric L. Seidel, Ranjit Jhala, Dimitrios Vytiniotis, and Simon Peyton Jones. Refinement types for Haskell. In Proceedings of the 19th ACM SIGPLAN International Conference on Functional Programming, ICFP ’14, pages 269–282. ACM, 2014.

Robert Atkey, Patricia Johann, and Andrew Kennedy. Abstraction and invariance for algebraically indexed types. In Proceedings of the 40th Annual ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, POPL ’13, pages 87–100. ACM, 2013. Joachim Breitner, Richard A. Eisenberg, Simon Peyton Jones, and Stephanie Weirich. Safe zero-cost coercions for Haskell. In Proceedings of the 19th ACM SIGPLAN International Conference on Functional Programming, ICFP ’14, pages 189–202. ACM, 2014. David Raymond Christiansen. Reflect on your mistakes! Lightweight domain-specific error messages, 2014. URL http://www.itu.dk/ people/drc/drafts/error-reflection-submission.pdf. Iavor S. Diatchki. Improving Haskell types with SMT. In Proceedings of the 2015 ACM SIGPLAN Symposium on Haskell, Haskell ’15. ACM, 2015. In press.

Dimitrios Vytiniotis, Simon Peyton Jones, Tom Schrijvers, and Martin Sulzmann. OutsideIn(X): Modular type inference with local assumptions. Journal of Functional Programming, 21(4–5):333–412, 2011. Dimitrios Vytiniotis, Simon Peyton Jones, and José Pedro Magalhães. Equality proofs and deferred type errors: A compiler pearl. In Proceedings of the 17th ACM SIGPLAN International Conference on Functional Programming, ICFP ’12, pages 341–352. ACM, 2012.

Jacques Garrigue. Relaxing the value restriction. In Yukiyoshi Kameyama and Peter J. Stuckey, editors, Functional and Logic Programming, volume 2998 of LNCS, pages 196–213. Springer, 2004.

Brent A. Yorgey, Stephanie Weirich, Julien Cretin, Simon Peyton Jones, Dimitrios Vytiniotis, and José Pedro Magalhães. Giving Haskell a promotion. In Proceedings of the 8th ACM SIGPLAN Workshop on Types in Language Design and Implementation, TLDI ’12, pages 53–66. ACM, 2012.

Adam Gundry. Type Inference, Haskell and Dependent Types. PhD thesis, University of Strathclyde, 2013. URL http://adam.gundry.co.uk/ pub/thesis/.

24 http://www.andres-loeh.de/lhs2tex/

22

Reasoning with the HERMIT Tool Support for Equational Reasoning on GHC Core Programs Andrew Farmer

Neil Sculthorpe

Andy Gill

Information and Telecommunication Technology Center University of Kansas, USA [email protected]

Department of Computer Science Swansea University, UK [email protected]

Information and Telecommunication Technology Center University of Kansas, USA [email protected]

Abstract

program (or program property) transliterated back into Haskell. However, the semantics of these languages differ from Haskell, sometimes in subtle ways, so the reasoning steps used may not carry over to Haskell. The most prominent example of informal equational reasoning in Haskell is type-class laws. Type-class laws are properties of typeclass methods that the class author expects any instance of the class to satisfy. However, these laws are typically written as comments in the source code, and are not enforced by a compiler; the onus is on the instance declarer to manually verify that the laws hold. A similar situation arises regarding GHC’s rewrite rules [27]. GHC applies these rules as optimisations at compile-time, without any check that they are semantically correct; the onus is again on the programmer to ensure their validity. This is a fragile situation: even if the laws (or rules) are correctly verified by pen-and-paper reasoning, any change to the implementation of the involved functions requires the reasoning steps to be updated accordingly. Such revisions can easily be neglected, and, furthermore, even if a calculation is up-to-date, a user cannot be sure of that without manually examining the individual steps herself. What is needed is a mechanical connection between the source code, the reasoning steps, and the compiled program. To address this situation, we have implemented a GHC plugin called HERMIT [11, 12, 14, 33]. HERMIT is a toolkit that supports interactive equational reasoning, and provides mechanical assurances of the correctness of that reasoning. HERMIT operates on GHC’s internal core language, part-way through the compilation process. User-specified transformations are applied to the program being compiled, and the user’s equational reasoning steps are checked. By performing equational reasoning during compilation, HERMIT is able to check that the reasoning steps correspond to the current implementation of the program, in the context of the language extensions currently being used. The initial HERMIT implementation [12] only supported equational reasoning that was transformational in nature; that is, HERMIT allowed the user to apply a sequence of correctness-preserving transformations to the Haskell program, resulting in an equivalent but (hopefully) more efficient program. This was sufficient to allow some specific instances of known program transformations to be mechanised [33], as well as for encoding prototypes of new optimisation techniques [1, 13]. However, some of the transformation steps used were only valid in certain contexts, and HERMIT had no facility for checking the necessary preconditions. Thus these preconditions had to be verified by hand. Furthermore, it was not possible to state and reason about auxiliary properties of the program being transformed, or to use inductive techniques to verify their correctness. This paper describes the addition of these facilities to HERMIT, and discusses our experiences of using them on two case studies. Specifically, the contributions of this paper are:

A benefit of pure functional programming is that it encourages equational reasoning. However, the Haskell language has lacked direct tool support for such reasoning. Consequently, reasoning about Haskell programs is either performed manually, or in another language that does provide tool support (e.g. Agda or Coq). HERMIT is a Haskell-specific toolkit designed to support equational reasoning and user-guided program transformation, and to do so as part of the GHC compilation pipeline. This paper describes HERMIT’s recently developed support for equational reasoning, and presents two case studies of HERMIT usage: checking that type-class laws hold for specific instance declarations, and mechanising textbook equational reasoning. Categories and Subject Descriptors D.3.2 [Programming Languages]: Language Classifications—Applicative (functional) languages; F.3.1 [Logics and Meanings of Programs]: Specifying and Verifying and Reasoning about Programs—Mechanical verification Keywords HERMIT, Equational Reasoning, Type Class Laws

1.

Introduction

Currently, most equational reasoning on Haskell programs is performed manually, using pen-and-paper or text editors, because of the lack of direct tool support. While some equational-reasoning tools do exist for Haskell [21, 40], they only target a subset of Haskell 98, not the full language (and certainly not the GHCextended version of Haskell that is widely used in practice). This is unfortunate, as pen-and-paper reasoning is slow, error prone, and allows the reasoner to neglect details of the semantics. For example, a common mistake is to neglect to consider partial and infinite values, which are notoriously tricky [8]. This was recently demonstrated by Jeuring et al. [22], who showed that the standard implementations of the state monad do not satisfy the monad laws. An alternative approach is to transliterate a Haskell program into a language or proof assistant that does provide support for equational reasoning, such as Agda [26] or Coq [38]. The desired reasoning can then be performed in that language, and the resultant Permission to make digital or hard copies of all or part of this work for personal or classroom use is granted without fee provided that copies are not made or distributed for profit or commercial advantage and that copies bear this notice and the full citation on the first page. Copyrights for components of this work owned by others than ACM must be honored. Abstracting with credit is permitted. To copy otherwise, or republish, to post on servers or to redistribute to lists, requires prior specific permission and/or a fee. Request permissions from [email protected]. Copyright is held by the owner/author(s). Publication rights licensed to ACM.

Haskell’15, September 3-4, 2015, Vancouver, BC, Canada ACM. 978-1-4503-3808-0/15/09...$15.00 http://dx.doi.org/10.1145/2804302.2804303

23

• We describe the new equational-reasoning infrastructure pro-

the more limited (but safer) monomorphically typed combinator language available to the REPL and scripts. We present an example of a user-defined program property in the Appendix.

vided by HERMIT, discussing the issues that arose as a consequence of our choice to work within GHC and target its internal language. (Section 2).

This paper will describe HERMIT’s new equational-reasoning infrastructure, but will not otherwise discuss its implementation or existing commands. Interested readers should consult the previous HERMIT publications [11, 12, 33], or try out the HERMIT toolkit [14] for themselves.

• We demonstrate interactive HERMIT usage by mechanically

verifying a GHC rewrite rule, giving a detailed walk-through of the reasoning steps (Section 3). • We present a case study of mechanically verifying that type-

class laws hold for specific class instances (Section 4). 2.1

• We explain how HERMIT can be integrated with Cabal such

• We present a case study of mechanising a chapter from Pearls

of Functional Algorithm Design [2], a textbook dedicated to deriving Haskell programs by calculation (Section 5).

2.

Program Properties

As discussed in Section 1, the HERMIT toolkit initially only supported program transformation, and any equational reasoning had to be structured as a sequence of transformation steps applied to the original source program [e.g. 33]. This was limiting, as equational reasoning often involves stating and verifying properties of the program being transformed, so that they can be used to validate the transformations being applied. To address this, we have added support for stating program properties, which are referred to as lemmas within HERMIT. A primitive HERMIT lemma takes the form of an equality between two GHC Core expressions, and may involve universally quantified variables. For example:

that HERMIT scripts are automatically checked and run when a package is built (Section 4.3).

Equational Reasoning using HERMIT

HERMIT is a GHC plugin that allows a user to apply custom transformations to a Haskell program amid GHC’s optimisation passes. HERMIT operates on the program after it has been translated into GHC Core, GHC’s internal intermediate language. GHC Core is an implementation of System FC , which is System F [20, 29] extended with let-binding, constructors, and first-class type equalities [37]. Type checking is performed during the translation, and GHC Core retains the typing information as annotations. The availability of typing information is a significant advantage of working with GHC Core rather than with Haskell source code. Another significant advantage is that GHC Core is a syntactically smaller language, and consequently there are far fewer cases to consider. For example, both if -then-else expressions and the special seq function are translated into explicit case expressions in GHC Core, and thus do not need to be considered. Arguably, some of this second advantage could instead be gained by working with desugared Haskell source code. However, there are limits to what can be desugared within Haskell source code: for example, while if then-else can be desugared to a Haskell case expression, the seq function cannot. (The semantics of GHC Core case expressions differ slightly from Haskell case expressions.) HERMIT provides commands for navigating a GHC Core program, applying transformations, version control, pretty printing, and invoking GHC analyses and optimisation passes. To direct and combine transformations, HERMIT uses the strategic programming language KURE [34] to provide a rich family of traversal and strategy combinators. HERMIT offers three main interfaces:

Map Fusion ∀ f g . map f ◦ map g ≡ map (f ◦ g)

Composite lemmas can be formed using the logical connectives implication, conjunction and disjunction, and universal quantifiers can scope over composite lemmas. For example: Fold Fusion ∀ f g h a b. ( f undefined ≡ undefined ∧ f a≡b ∧ ∀ x y . f (g x y) ≡ h x (f y) ) ⇒ f ◦ foldr g a ≡ foldr h b

HERMIT maintains a set of lemmas, and tracks which of them have been verified by a HERMIT calculation. Once verified, lemmas can be mechanically used to validate transformation steps that have preconditions, and primitive lemmas can be applied as program transformations (left-to-right or right-to-left). Composite lemmas can be manipulated by HERMIT commands corresponding to standard introduction and elimination rules for logical connectives, and universally quantified variables can be instantiated. A primitive lemma can be verified by applying a sequence of transformation steps to either (or both) sides of the lemma, until HERMIT is satisfied that both sides are α-equivalent. Such reasoning can either be performed interactively, or by loading a HERMIT script. We will demonstrate interactive reasoning in Section 3. The most convenient way of introducing primitive lemmas into HERMIT is by exploiting GHC rewrite-rule pragmas [27]. These pragmas are intended to allow a user to introduce custom optimisations into the GHC optimisation pipeline. For example, a rewrite rule corresponding to the Map Fusion lemma above can be added to a Haskell source file as follows:

• An interactive read-eval-print loop (REPL). This allows a user

to view and explore the program code, as well as to experiment with transformations. In Section 3 we present an example of using the REPL to verify a GHC rewrite rule; examples of using the REPL to perform program transformations can be found in Farmer et al. [12] and Sculthorpe et al. [33]. • HERMIT scripts. These are sequences of REPL commands,

which can either be loaded and run from within the REPL, or automatically applied by GHC during compilation. We present an example HERMIT script in Section 5.

{-# RULES “map-fusion”[∼] forall f g . map f . map g = map (f . g) #-}

• A domain-specific language (DSL) for transformation, em-

bedded in Haskell. This allows the user to construct a custom GHC plugin using all of HERMIT’s capabilities. The user can run transformations in different stages of GHC’s optimisation pipeline, and add custom transformations to the REPL. New transformations and program properties can be encoded by defining Haskell functions directly on the Haskell data type representing the GHC Core abstract syntax, rather than using

GHC will parse and type check this rule, and translate it into GHC Core. HERMIT exploits this mechanism by generating a lemma from the translated rule. The [∼] annotation renders this rule inactive [16, Section 7.21.1], which allows us to use this mechanism to introduce lemmas that are not intended to be used as standalone optimisations.

24

2.3

There are some restrictions on the form of the left-hand side of a GHC rewrite rule [27, Section 2.2], so this approach can only generate a subset of all possible primitive lemmas. However, in practice, all of the primitive lemmas that we needed for our case studies fell within this subset. Currently, we introduce composite lemmas using HERMIT’s transformation DSL. Primitive lemmas can also be introduced in this way, but as this requires working with the GHC Core data type directly, it tends to be less convenient and more error-prone than rewrite-rule pragmas. We present an example of such a compositelemma definition in the Appendix. As future work, we aim to allow the user to introduce composite lemmas into HERMIT using (extended) Haskell syntax, which HERMIT would then type check, parse and convert to GHC Core. 2.2

Missing Unfoldings

Equational reasoning often involves fold/unfold transformations [5]. One consequence of our choice to work within GHC is that, in order to unfold functions defined in previously compiled modules, HERMIT relies on the unfolding information present in the interface files generated by GHC. Unfortunately, for recursive functions that are not marked with an explicit INLINE pragma, GHC does not normally include their unfoldings in the interface files for their defining modules. This prevents us from unfolding those functions in HERMIT. This includes, for example, the + + and map functions. We currently have three work-arounds for this issue. The first option is to recompile the defining packages with GHC’s -fexpose-all-unfoldings flag. In the case of + + and map, this means recompiling the base package. The second option is to redefine the function with a new name, and use that function in our programs instead of the library function. For example:

Explicit Types and Dictionaries

In GHC Core, polymorphic functions have explicit type arguments (as GHC Core is based on System F), and type-class methods are implemented as functions that take a class dictionary as an argument [45]. A class dictionary is essentially a record containing a definition for each method of the type class, specialised to a concrete class parameter (or parameters). Thus each class instance generates a single dictionary (which may, in turn, be abstracted over other dictionaries). To allow us to demonstrate the presence of these implicit type and dictionary arguments, first consider the explicit types of the following polymorphic functions:

myAppend :: [a ] → [a ] → [a ] myAppend [ ] ys = ys myAppend (x : xs) ys = x : myAppend xs ys

However, this is not an option if we want to reason about preexisting code that uses the library version of that function. A third option is to define a GHC rewrite rule to convert calls to the library function into calls to the new function, and then use this rule to transform the program before beginning to reason about it. For example: {-# RULES “my-append”[∼] (+ +) = myAppend #-}

id :: forall a . a → a fmap :: forall f . Functor f ⇒ forall a b . (a → b) → f a → f b

We are not entirely happy with any of these work-arounds, and finding a cleaner solution remains as future work. GHC offers a systematic means of compiling Haskell libraries in multiple ways and installing these builds in parallel. For example, the user may desire to install both normal object files and object files that include extra information for run-time profiling. One possible solution to our problem would be to extend this mechanism by adding a new ‘way’ that generates interface files that include unfolding information for every exported function.

In GHC Core, each universally quantified type variable is an additional (type) argument to the function, and each class constraint is an additional dictionary argument to the function. For example, consider the functor identity law, expressed as a GHC rewrite rule: {-# RULES “fmap-id”[∼] fmap id = id #-}

The rewrite rule is written using Haskell source code, so the type and dictionary arguments are implicit. However, the representation of this law as a HERMIT lemma in GHC Core makes these extra arguments explicit:

2.4

Structural Induction

Haskell programs usually contain recursive functions defined over (co)inductive data types. Reasoning about such programs often requires the use of an induction principle. For example, while [] + + xs ≡ xs can be established simply by unfolding the definition of + +, establishing the similar property xs + + [ ] ≡ xs requires reasoning inductively about the structure of xs. Inductive reasoning cannot be expressed as a sequence of transformation steps: both the source and target expression must be known in advance, and the validity of rewriting one to the other is established by verifying the inductive and base cases. HERMIT provides structural induction over algebraic data types as a built-in transformation. The remainder of this section will formalise HERMIT’s induction principle, then in Section 3 we will give an example of using induction in HERMIT. → We first introduce some notation. We write − vs to denote a → sequence of variables, and ∀ (C − vs :: A) to quantify over all constructors C of the algebraic data type A, fully applied to a → sequence − vs of length matching the arity of C . Let C : A B denote that C is an expression context containing one or more holes of type A, and having an overall type B . For any expression a :: A, then CJaK denotes the context C with all holes filled with the expression a. The structural-induction inference rule provided by HERMIT is defined in Figure 1. The conclusion of the rule is called the induction hypothesis. Informally, the premises require that:

fmap-id ∀ f t $dFunctor . fmap f $dFunctor t t (id t) ≡ id (f t)

Here, f and t are type variables, and $dFunctor is a dictionary variable. When generating names for dictionary variables, GHC prefixes the class name with “$d”. Also, as we will see shortly, GHC prefixes the names of dictionary instances with “$f ”. A significant advantage of using GHC rewrite rules to generate lemmas is that these type and dictionary arguments are inferred and automatically inserted by GHC. Returning to the functor identity law, note that this is not a lemma to be verified. Rather, the law is a specification that the class author expects any instance of the class to satisfy. To check that the law holds for a specific instance, we must first instantiate the type variable f and its corresponding dictionary “$dFunctor ”, thereby generating the lemma that we wish to hold. For example, instantiating this lemma to the Maybe data type would give the following: fmap-id ∀ t . fmap Maybe $fFunctorMaybe t t (id t) ≡ id (Maybe t)

HERMIT provides commands to specialise lemmas by instantiating variables in this way, whether dictionary variables or otherwise, as we will demonstrate in Section 4.1.

25

Given contexts C, D : A

B , for any algebraic data type A, and any type B , then structural induction provides the following inference rule:

CJundefined K ≡ DJundefined K

→ → → → ∀ (C − vs :: A) . (∀ (v ∈ − vs , v :: A) . CJv K ≡ DJv K) ⇒ (CJC − vs K ≡ DJC − vs K) ∀ (a :: A) . CJaK ≡ DJaK Figure 1: Structural induction.

Given contexts C, D : [A]

B , for any types A and B , then:

CJundefined K ≡ DJundefined K

CJ [ ] K ≡ DJ [ ] K ∀ (a :: A, as :: [A]) . (CJasK ≡ DJasK) ⇒ (CJa : asK ≡ DJa : asK) ∀ (xs :: [A]) . CJxsK ≡ DJxsK Figure 2: Structural induction on lists.

• the induction hypothesis holds for undefined values;

beyond a check that they produce a well-typed GHC Core term. A substantial avenue for future work is to create a mechanical connection between HERMIT’s primitive transformations and a semantic model, so that they can be formally verified. There has been recent work on translating GHC Core to first-order logic so that properties can be verified by an external automated theorem prover [44], and it seems plausible that this approach could be incorporated into HERMIT.

• the induction hypothesis holds for any fully applied constructor,

given that it holds for any argument of that constructor (of matching type). As a more concrete example, specialising structural induction to the list data type gives the inference rule in Figure 2. This form of structural induction is somewhat limited in that it only allows the induction hypothesis to be applied to a variable one constructor deep. We are currently in the process of implementing a more general induction principle that will allow the inductive hypothesis to be applied to a variable n constructors deep. 2.5

3.

Interactive Proof Example

In this section we will demonstrate HERMIT’s interactive mode by performing a calculation to validate a GHC rewrite rule. We will use the Haskell source file in Figure 3 for this example. The rule map-fusion is (a slight reformulation of) the motivating example of a rewrite rule given by Peyton Jones et al. [27, Section 2]. Note that we define map explicitly, rather than using the definition in the standard Prelude, to avoid the issues with unfolding pre-compiled definitions (as discussed in Section 2.3). We begin by invoking HERMIT on the source file:

Correctness and Equivalence

HERMIT provides a large suite of built-in primitive transformations. Some of these are transformations and optimisation passes lifted from the internals of GHC, whereas others are taken from the literature on program transformation (e.g. [5, 9, 19]). Our aim with HERMIT is to provide tool support for the kind of equational reasoning that is commonly done on pen-and-paper, and to that end we seek to support the wide range of transformation and equational-reasoning techniques that are used in practice. This has consequences for correctness, as some of the transformations provided by HERMIT only offer partial correctness: the output produced by a program before transformation is compatible with the output after transformation. By compatible we mean that the output can be either more or less defined, but that the defined output does not differ in value. For example, fold/unfold transformation can easily introduce non-termination if used carelessly [40], but is widely used in pen-and-paper reasoning. HERMIT’s equivalence relation (≡) is based on transformation steps: two GHC Core expressions are considered to be equal if one can be transformed into the other, modulo α-equality. Consequently, whether an equivalence is partially or totally correct depends on the correctness of the transformations used. The majority of HERMIT’s transformations are totally correct, and many of the remaining partially correct transformations are totally correct, given certain preconditions. These preconditions are encoded in HERMIT, but by default a user may choose to ignore them when applying a transformation. This is a pragmatic design decision: it allows a user to experiment without having to keep detouring to satisfy pre-conditions. In the next version of HERMIT, we intend to allow the user the option of disabling the set of partially correct transformations, and of enforcing that any preconditions are satisfied before a transformation can be used. The user can then choose the desired trade-off between correctness, expressiveness and convenience. Note that there is no built-in semantic model within HERMIT (or GHC). The primitive transformations are essentially axioms, and themselves have no mechanical verification of their correctness

> hermit MapFusion.hs +MapFusion

GHC begins compiling MapFusion.hs, performing parsing, type checking and desugaring, before pausing compilation and passing control to the HERMIT interactive shell. The +MapFusion flag specifies that we wish to invoke HERMIT on the MapFusion module. (In general, compiling a module with GHC may trigger the compilation of dependent modules, and we may wish to run HERMIT on any or all of them.) module main:MapFusion where map :: ∀ a b . (a  b)  [a]  [b]

HERMIT presents a summary of the module, which in this case just contains one function, map. Here we only see the type signature; to see the definition we tell HERMIT to focus on the binding: hermit> binding-of ’map map = λ M M f ds  case ds of wild N []  [] N (:) a as  (:) N (f a) (map N N f as)

Notice that the top-level pattern matching has been desugared into explicit lambdas and a case expression, and that the infix cons operator has been moved into a prefix position. Type arguments are displayed as triangles by HERMIT’s default pretty printer, but the full details can displayed if desired. For this example we will not need to manipulate any type arguments, so we choose to hide them: hermit> set-pp-type Omit

26

map = λ f ds  case ds of wild []  [] (:) a as  (:) (f a) (map f as)

module MapFusion where import Prelude hiding (map) {-# RULES “map-fusion”[∼] forall f g . map f . map g = map (f . g) #-}

To begin the calculation, we need to tell HERMIT that we want to reason about the map-fusion rule. We return to the top of the module, convert the map-fusion rule to a HERMIT lemma, and tell HERMIT that we wish to begin proving that lemma:

map :: (a → b) → [a ] → [b ] map f [ ] = [] map f (a : as) = f a : map f as

hermit> top hermit> rule-to-lemma "map-fusion" hermit> prove-lemma "map-fusion"

Figure 3: Haskell source file MapFusion.hs.

Goal: ∀ f g. (.) (map f) (map g) ≡ map ((.) f g)

[34], which underlies HERMIT. Note that the sequencing strategy requires that both its component strategies succeed, which in this case ensures that occurrences of map are only unfolded if they can subsequently be reduced by undefined-case or case-reduce.

Verifying this lemma in HERMIT requires the use of structural induction on the list data type (as per Figure 2). However, as the rule is written in a point-free style, there is initially no list argument to perform induction on. Thus we first apply extensionality to etaexpand the rule:

proof> any-bu (unfold ’map >>> (undefined-case extensionality ’xs

Goal: ∀ f g. (undefined ≡ undefined) ∧ (([] ≡ []) ∧ (∀ a b. (map f (map g b) ≡ map (λ x  f (g x)) b) ⇒ ((:) (f (g a)) (map f (map g b)) ≡ (:) ((λ x  f (g x)) a) (map (λ x  f (g x)) b))))

Goal: ∀ f g xs. (.) (map f) (map g) xs ≡ map ((.) f g) xs

We also unfold the composition operator, as this will provide us with a more convenient induction hypothesis: proof> any-call (unfold ’.) Goal: ∀ f g xs. map f (map g xs) ≡ map (λ x  f (g x)) xs

Observe that both base cases have been reduced to evident equivalences. They can be eliminated altogether using HERMIT’s simplify-lemma strategy. This strategy checks all equalities for α-equivalence, and reduces any it finds to a primitive truth clause. The strategy then attempts to apply a set of standard logical simplifications to eliminate connectives wherever possible (in this case, the unit law of conjunction is applied twice).

We now begin the inductive part of the calculation. HERMIT provides structural induction as a lemma transformation: treating the current goal as the induction hypothesis, the goal is transformed into the conjunction of the base and inductive cases. That is, the inference rule in Figure 1 is instantiated such that its conclusion matches the current goal, and then the current goal is replaced by the premise of the rule.

proof> simplify-lemma

proof> induction ’xs

Goal: ∀ f g a b. (map f (map g b) ≡ map (λ x  f (g x)) b) ⇒ ((:) (f (g a)) (map f (map g b)) ≡ (:) ((λ x  f (g x)) a) (map (λ x  f (g x)) b))

Goal: ∀ f g. (map f (map g undefined) ≡ map (λ x  f (g x)) undefined) ∧ ((map f (map g []) ≡ map (λ x  f (g x)) []) ∧ (∀ a b. (map f (map g b) ≡ map (λ x  f (g x)) b) ⇒ (map f (map g ((:) a b)) ≡ map (λ x  f (g x)) ((:) a b))))

Now all that remains is the inductive case. In HERMIT, when we navigate to the consequent of an implication, the antecedent becomes available as an assumed lemma. The HERMIT REPL displays all such in-scope antecedents to facilitate their use. proof> forall-body ; consequent Assumed lemmas: ind-hyp-0 (Built In) map f (map g b) ≡ map (λ x  f (g x)) b Goal: (:) (f (g a)) (map f (map g b)) ≡ (:) ((λ x  f (g x)) a) (map (λ x  f (g x)) b)

Here, the three clauses in the premise are the two base cases (for undefined and []), and one inductive case (for (:)), as per Figure 2. In each of the cases, two of the three occurrences of map are now applied to either undefined or an explicit list constructor. We need to unfold the definition of map and reduce the resultant expression in each of those two occurrences. Rather than doing this step by step, we build a strategy to perform these reductions in one step. We use the strategy combinators any-bu (anywhere, traversing bottom-up), >>> (sequencing) and one-td (lemma-backward ind-hyp-0)

27

Assumed lemmas: ind-hyp-0 (Built In) map f (map g b) ≡ map (λ x  f (g x)) b Goal: (:) (f (g a)) (map f (map g b)) ≡ (:) ((λ x  f (g x)) a) (map f (map g b))

All that remains is to perform a β-reduction. HERMIT’s primitive beta-reduce transformation transforms a β-redex to a nonrecursive let-binding, which can then be eliminated by inlining the binding. Rather than navigating to the redex and invoking these two transformations, we instead make use of HERMIT’s general-purpose simplify strategy. This strategy repeatedly traverses a term, applying a set of basic simplification transformations until no more are applicable. Amongst others, this set includes beta-reduce, the elimination and inlining of let-bindings where the binding is used at most once, and the inlining of the definitions of several basic function combinators such as id , const and (◦). proof> simplify

Functor fmap-id fmap-distrib

∀ g h.

fmap id ≡ id fmap (g ◦ h) ≡ fmap g ◦ fmap h

Applicative identity homomorphism interchange composition fmap-pure

∀ v. ∀f x. ∀ u y. ∀ u v w. ∀g x.

pure id ~ v ≡ v pure f ~ pure x ≡ pure (f x ) u ~ pure y ≡ pure (λf → f y) ~ u u ~ (v ~ w ) ≡ pure (◦) ~ u ~ v ~ w pure g ~ x ≡ fmap g x

Monad return-left return-right bind-assoc fmap-liftm

∀k x. ∀ k. ∀ j k l. ∀f x.

return x > >= k ≡ k x k> >= return ≡ k (j > >= k ) > >= l ≡ j > >= (λx → k x > >= l) liftM f x ≡ fmap f x

In order to observe the effect of instantiation on the types of the lemma quantifiers, we begin by instructing HERMIT’s pretty printer to display detailed type information. We then copy the general law, which has already been loaded from a rewrite-rule pragma, in preparation for instantiation.

The two sides are now equivalent, so the calculation is complete.

hermit> set-pp-type Detailed hermit> copy-lemma return-left return-left-list

proof> end-proof Successfully proven: map-fusion

return-left-list (Not Proven) ∀ (m :: *  *) (a :: *) (b :: *) ($dMonad :: Monad m) (k :: a  m b) (x :: a). (>>=) m $dMonad a b (return m $dMonad a x) k ≡ k x

The lemma is now available for use in further calculations. The sequence of reasoning steps that we performed can also be saved as a script, and thence re-run in future HERMIT sessions. This was a toy example, but we will now present two more realistic case studies, each of which contains a multitude of lemmas.

Case Study: Type-Class Laws

Next, we instantiate the type variable m to the list type constructor:

In this case study we use equational reasoning to verify that a number of type-class instances for common Haskell data types satisfy the expected type-class laws. We consider the laws in Figure 4. The data types we consider are lists, Maybe, and Map from the containers package, as well as Identity and Reader from the transformers package. Our approach was to state each law as a GHC rewrite rule, and load it into HERMIT as a lemma (as we did for the example in Section 3). We instantiated the laws for each data type, and then transformed the instantiated laws until HERMIT was satisfied that they held. Note that we used the actual data types and class instances defined in the base, containers, and transformers packages. We present the case study as follows. Section 4.1 demonstrates the full details of verifying a single law. Section 4.2 then discusses some practical issues that arose as a consequence of using GHC Core as the object language. Section 4.3 describes how to modify the containers Cabal file to cause pre-written reasoning to be automatically loaded, checked and applied during compilation. Finally, Section 4.4 reflects on the overall success of the case study. 4.1

∀x. mempty  x ≡ x ∀x. x  mempty ≡ x ∀ x y z . (x  y)  z ≡ x  (y  z )

Figure 4: Laws used in the ‘Type-Class Laws’ case study.

Assumed lemmas: ind-hyp-0 (Built In) map f (map g b) ≡ map (λ x  f (g x)) b Goal: (:) (f (g a)) (map f (map g b)) ≡ (:) (f (g a)) (map f (map g b))

4.

Monoid mempty-left mempty-right mappend-assoc

hermit> inst-lemma return-left-list ’m [| [] |] return-left-list (Not Proven) ∀ (a :: *) (b :: *) ($dMonad :: Monad []) (k :: a  [b]) (x :: a). (>>=) [] $dMonad a b (return [] $dMonad a x) k ≡ k x

(The [| |] syntax are delimiters enclosing manually written Core expressions, which HERMIT then parses and resolves.) The type of the dictionary binder has now been fully determined, so we instantiate it as well: hermit> prove-lemma return-left-list proof> inst-dictionaries Goal: ∀ (a :: *) (b :: *) (k :: a  [b]) (x :: a). (>>=) [] $fMonad[] a b (return [] $fMonad[] a x) k ≡ k x

Example: return-left Monad Law for Lists

To give a flavour of the work involved in checking that a typeclass law holds for a specific instance, we present the calculation for the return-left monad law for lists. The reasoning steps in this calculation involve more complex transformations than our Map Fusion example, which allows us to demonstrate the advantages of using KURE’s strategy combinators for directing transformations.

Next we note that the application of return can be simplified to a singleton list. We achieve this by unfolding return, which will expose a case expression that scrutinises the $fMonad[] dictionary. This can be simplified away by using HERMIT’s smash strategy, which is a more aggressive version of the simplify strategy. This will leave the actual instance method defining return for lists,

28

Test-suite hermit-proofs hs-source-dirs: laws, . main-is: Laws.hs type: exitcode-stdio-1.0

which can also be unfolded. Rather than doing this step by step, we direct HERMIT to focus on the application of return and repeatedly unfold and smash the expression. (The { } brackets limit the scope of the focus change.) proof> { application-of ’return ; repeat (unfold = 4.2 && < 5, array, deepseq >= 1.2 && < 1.4, ghc-prim, hermit == 1.0.* ghc-options: -fexpose-all-unfoldings -fplugin=HERMIT -fplugin-opt=HERMIT:Main:laws/Functor.hec -fplugin-opt=HERMIT:Main:laws/Monoid.hec -fplugin-opt=HERMIT:Main:resume

Goal: ∀ (a :: *) (b :: *) (k :: a  [b]) (x :: a). (>>=) [] $fMonad[] a b ((:) a x ([] a)) k ≡ k x

Now we need to simplify away the >>= application. Unfolding >>= directly results in a locally defined recursive worker named go, in terms of which the list instance of >>= is defined. Reasoning in the context of this recursive worker is tedious and brittle. We find it cleaner to state and verify the following pair of lemmas separately, then apply them as necessary during this proof:

Figure 5: Additions to the Cabal configure file for containers in order to automatically re-run the HERMIT scripts.

∀ k. [] > >= k ≡ [] ∀ x xs k . (x : xs) > >= k ≡ k x + + (xs > >= k )

bind-left-nil bind-left-cons

4.2.2

proof> one-td (lemma-forward bind-left-cons) Goal: ∀ (a :: *) (b :: *) (k :: a  [b]) (x :: a). (++) b (k x) ((>>=) [] $fMonad[] a b ([] a) k) ≡ k x proof> one-td (lemma-forward bind-left-nil) Goal: ∀ (a :: *) (b :: *) (k :: a  [b]) (x :: a). (++) b (k x) ([] b) ≡ k x

To eliminate the list append we appeal to another auxiliary lemma, which can itself be verified by straightforward induction. append-right

∀ xs . xs + + [ ] ≡ xs

4.3

We apply append-right to complete the calculation:

Goal: ∀ (a :: *) (b :: *) (k :: a  [b]) (x :: a). k x ≡ k x proof> end-proof

Reasoning in GHC

Equational reasoning in HERMIT is performed in the context of GHC Core. While this is a small, relatively stable, typed intermediate language, it was designed for compilation and optimisation, not equational reasoning. Consequently, there are a few practical concerns and limitations regarding reasoning with this language. 4.2.1

Configuring Cabal

As a GHC plugin, HERMIT integrates with GHC’s existing ecosystem, including the Cabal packaging system. Cabal packages feature a single (per-package) configuration file. This file describes how Cabal should build the package, including how to build test cases. We co-opt this ability to direct HERMIT to run and check our scripts whenever the package is rebuilt. As an example, we added a laws/ directory to the containers package, containing three files. The first is Laws.hs, which provides the RULES pragmas representing the type-class laws. The other two files are the HERMIT scripts: one for the functor-law calculations, the other for the monoid-law calculations. We then added a Test-suite section, seen in Figure 5, to the Cabal configuration file for containers. This defines the target code for the test, which is Laws.hs, along with build dependencies. The build dependencies shown are those of the containers library, plus an additional dependency on hermit. Note that this additional dependency does not change the dependencies of the containers library itself. Cabal runs the HERMIT scripts by providing GHC with the required series of flags. The -fexpose-all-unfoldings flag was described in Section 2.3. The -fplugin=HERMIT flag invokes HERMIT, and the remaining three flags direct HERMIT to target the Main module (found in Laws.hs) with two scripts, resuming compilation on successful completion. The HERMIT scripts should also be added to the configuration file’s extra-source-files section, so that they are included in source distributions. In order to run the scripts, we use the normal Cabal testing work-flow:

proof> one-td (lemma-forward append-right)

4.2

Newtypes

GHC’s newtype declaration offers the benefits of type abstraction with no runtime overhead [4, 43]. This is accomplished by implementing newtype constructors in GHC Core as type casts around the enclosed expression, rather than as algebraic data constructors. These casts are erased before code generation. Reasoning in the presence of newtypes must deal with these casts explicitly. HERMIT’s smash strategy attempts to float-out and eliminate type casts where possible, and was effective at doing so in the majority of our examples. In the few cases where the smash strategy did not eliminate all the casts, the resultant expressions were still α-equivalent and thus this did not pose a problem.

Implications

The Monoid instance for Maybe a requires a Monoid instance to exist for the type a: instance Monoid a ⇒ Monoid (Maybe a) where . . .

Correspondingly, the calculation to verify that the monoid associativity lemma holds for Maybe a relies on the monoid associativity lemma for a. Thus, we used the following lemma for Maybe: mappend-assoc-impl ∀ m . (∀ (x :: m) y z . (x  y)  z ≡ x  (y  z )) ⇒ (∀ (i :: Maybe m) j k . (i  j )  k ≡ i  (j  k ))

HERMIT cannot generate such an implication lemma from the original mappend-assoc lemma automatically. Although it can spot the superclass constraint, the associated laws are not part of the Haskell language, and thus are not available within GHC. Instead, we constructed the implication lemma ourselves using HERMIT’s transformation DSL.

> cabal configure --enable-tests > cabal build

Note that we do not actually have to run the generated test, as the HERMIT scripts are run at compile time.

29

5.

Table 1: Script lengths in the ‘Type-Class Laws’ case study.

To assess how well HERMIT supports general-purpose equational reasoning, we decided to mechanise some existing textbook reasoning as a case study. We selected the chapter Making a Century from the textbook Pearls of Functional Algorithm Design [2, Chapter 6]. The book is dedicated to reasoning about Haskell programs, with each chapter calculating an efficient program from an inefficient specification program. The program in Making a Century computes the list of all the ways that the addition and multiplication operators can be inserted into the list of digits [1 . . 9], such that the resultant expression evaluates to 100. For example, one solution is:

Data Type List Maybe Map Identity Reader mempty-left 7 5 7 mempty-right 6 5 7 N/A mappend-assoc 15 15 fmap-id 9 7 12 5 10 fmap-distrib 10 8 16 5 10 identity 7 8 5 15 homomorphism 8 5 5 15 interchange 18 5 5 15 composition 23 5 5 15 N/A fmap-pure 20 5 5 15 return-left 9 5 5 12 return-right 18 5 5 12 bind-assoc 14 5 5 12 Law (see Fig. 4)

4.4

Case Study: Making a Century

12 + 34 + 5 × 6 + 7 + 8 + 9 = 100 The details of the program are not overly important to the presentation of our case study, and we refer the interested reader to the textbook for details [2, Chapter 6]. What is important, is that the derivation of an efficient program involves a substantial amount of equational reasoning, and the use of a variety of reasoning techniques, including fold/unfold transformation [5], structural induction (Section 2.4), and fold fusion [25]. We will not present the entire case study in this paper. Instead, we will give a representative extract, and then discuss the aspects of the mechanisation that proved challenging. The HERMIT scripts for the complete case study are available online [15].

Reflections

Results for the case study are listed in Table 1, and the complete set of HERMIT scripts are available online [15]. The numbers in the table represent number of lines in the HERMIT script, including instantiation steps. Overall, verifying type-class laws in GHC Core appears to be viable with the simple reasoning techniques offered by HERMIT. In general, we found that reasoning about type classes and dictionaries proceeded in much the same way as the example in Section 4.1. Handling class methods requires many unfolding and simplification steps. Once this is done, any required inductive reasoning tends to be short and straightforward. The one law we did not verify was mappend-assoc for the Map data type. This was not because of any technical limitation of HERMIT, but rather because the required reasoning steps were not obvious to us. The mappend operation for Map is an efficient left-biased hedged union whose implementation relies on several functions that maintain invariants of the underlying data structure. We expect that this law could be verified by a user with a better understanding of these functions and the invariants they maintain. Unsurprisingly, stating smaller auxiliary lemmas for (re-)use in the larger calculations helped to manage complexity. In contrast, our initial attempts to perform the larger calculations directly required working at a lower level, and led to a substantial amount of duplicated effort. This was especially true of the Applicative laws, as the Applicative instances were often defined in terms of their Monad counterparts. In the case of lists, naively unfolding > >= results in a local recursive worker function. Reasoning in the presence of such workers requires many tedious unfolding and let-floating transformations. Using auxiliary lemmas about > >= allowed us to avoid this tedium. We did not attempt to quantify the robustness of the HERMIT scripts with respect to changes in the underlying source code. The types and instances that we considered are standard and relatively stable over time. However, as most of the calculations were fairly heavy on unfolding and simplification, we expect they would be sensitive to changes. To lower the burden of amending existing scripts, HERMIT’s interactive mode allows a user to pause a script midway, and to step backwards and forwards. Configuring a Cabal package to re-run scripts on recompilation is straightforward, requiring a single additional section to a package’s Cabal configuration file. End users of the package can still build and install the package as before, but the HERMIT scripts can be checked by enabling the package tests.

5.1

HERMIT Scripts

After creating a Haskell file containing the definitions from the textbook, our next task was to introduce the lemmas used in the equational reasoning. The main lemmas (specifically, those that are named in the textbook) are displayed in Figure 6, which should give an impression of their complexity. The majority of these lemmas are equivalences between expressions, so we were able to introduce them via rewrite rules in the Haskell source file (see Section 2.1). The one exception was Fold Fusion, which we introduced using HERMIT’s transformation DSL. Lemma 6.5 is also a composite lemma, but we found it more convenient to introduce a pair of lemmas rather than constructing an explicit conjunction. Throughout this case study, we took a consistent approach to mechanising the equational reasoning in the textbook. For each lemma, we first worked step-by-step in HERMIT’s interactive mode, and then, when the calculation was complete, saved it as a script that could be invoked thereafter. We took the same approach to the main program transformation (solutions), invoking the lemmas as necessary. Roughly half of the HERMIT equational reasoning in this case study was transliterated from the textbook equational reasoning, and the remaining half was calculations that we developed for ourselves (see Section 5.3). Both halves proceeded in a similar manner, but with more experimentation and backtracking during the interactive phases for the latter. As an example, compare the calculations to verify Lemma 6.8. Figure 7a presents the calculation extracted verbatim from the textbook [2, Page 36], and Figure 7b presents the corresponding HERMIT script. Note that lines beginning “--” in a HERMIT script are comments, and for readability we have typeset them differently to the monospace HERMIT code. These comments represent the current expression between transformation steps, and correspond to the output of the HERMIT REPL when working interactively. When generating a script after an interactive session, HERMIT can insert these comments if desired. The content of the comments can be configured by various pretty-printer modes — in this case we have opted to have HERMIT omit the type arguments (as in Section 3) to improve the correspondence with the textbook extract. The main difference between the two calculations is that in HERMIT we must specify where in the term we are working, and in which direction lemmas are applied. In contrast, in the textbook the

30

Fold Fusion Lemma 6.2 Lemma 6.3 Lemma 6.4 Lemma 6.5 Lemma 6.6 Lemma 6.7 Lemma 6.8 Lemma 6.9 Lemma 6.10

∀ f g h a b . (f undefined ≡ undefined ∧ f a ≡ b ∧ ∀ x y . f (g x y) ≡ h x (f y)) ⇒ filter (good ◦ value) ≡ filter (good ◦ value) ◦ filter (ok ◦ value) ∀x. filter (ok ◦ value) ◦ extend x ≡ filter (ok ◦ value) ◦ extend x ◦ filter (ok ◦ value) ∀x. map value ◦ extend x ≡ modify ◦ map value ∀ f g. fst ◦ fork (f , g) ≡ f ∧ snd ◦ fork (f , g) ≡ g ∀ f g h. fork (f , g) ◦ h ≡ fork (f ◦ h, g ◦ h) ∀ f g h k . fork (f ◦ h, g ◦ k ) ≡ cross (f , g) ◦ fork (h, k ) ∀ f g. fork (map f , map g) ≡ unzip ◦ map (fork (f , g)) ∀ f g. map (fork (f , g)) ≡ zip ◦ fork (map f , map g) ∀ f g p. map (fork (f , g)) ◦ filter (p ◦ g) ≡ filter (p ◦ snd) ◦ map (fork (f , g))

f ◦ foldr g a ≡ foldr h b

Figure 6: Main lemmas in the ‘Making a Century’ case study. -- forall f g . fork ((,) (map f ) (map g)) = (.) unzip (map (fork ((,) f g))) forall-body ; eq-rhs unzip · map (fork (f , g))

-- (.) unzip (map (fork ((,) f g)))

{ definition of unzip }

=

fork (map fst, map snd) · map (fork (f , g))

one-td (unfold ’unzip) -- (.) (fork ((,) (map fst) (map snd))) (map (fork ((,) f g)))

{ (6.6) and map (f · g) = map f · map g }

=

fork (map (fst · fork (f , g)), map (snd · fork (f , g)))

lemma-forward "6.6" ; any-td (lemma-forward "map-fusion") -- fork ((,) (map ((.) fst (fork ((,) f g)))) (map ((.) snd (fork ((,) f g)))))

{ (6.5) }

=

one-td (lemma-forward "6.5a") ; one-td (lemma-forward "6.5b")

fork (map f , map g) (a) Textbook extract.

-- fork ((,) (map f ) (map g)) (b) HERMIT script.

Figure 7: Comparison of the textbook calculation with the HERMIT script for Lemma 6.8.

On paper, associative binary operators such as function composition are typically written without parentheses. However, in GHC Core, operators are represented by nested application nodes in an abstract syntax tree, with no special representation for associative operators. Terms that are equivalent semantically because of associativity properties can thus be represented by different trees. Consequently, it is sometimes necessary to perform a tedious restructuring of the term before a transformation can be applied. For function composition, one way to avoid this problem is to unfold all occurrences of the composition operator and work with the η-expanded terms, as this always produces an abstract syntax tree consisting of a left-nested sequence of applications. However, we did not do so for this case study because the textbook calculations are written in a point-free style, and we wanted to match them as closely as possible. More generally, rewriting terms containing associative (and commutative) operators is a well-studied problem [e.g. 3, 10, 23], and it remains as future work to provide better support for manipulating such operators in HERMIT.

Figure 6, as well as several auxiliary properties, some explicit and some implicit (Figure 8). While performing reasoning beyond that presented in the textbook was not intended to be part of the case study, we decided to attempt to verify these properties in HERMIT. Of the assumed named lemmas, Fold Fusion has a straightforward inductive proof, which can be encoded fairly directly using HERMIT’s built-in structural induction. Lemmas 6.5, 6.6, 6.7 and 6.10 are properties of basic function combinators, and verifying them in HERMIT mostly consisted of unfolding definitions and simplifying the resultant expressions, with the occasional basic use of induction. The same was true for the auxiliary lemmas, which we list in Figure 8. Systematic calculations such as these are ripe for mechanisation, and HERMIT provides several strategies that perform a suite of basic simplifications to help with this. Consequently, the HERMIT scripts were short and concise. Lemmas 6.2, 6.3 and 6.4 were more challenging. For Lemma 6.2 we found it helpful to introduce the filter-split auxiliary lemma (Figure 8), which we consider to capture the essence of the key optimisation in the case study. After this, the calculation was fairly straightforward. However, we found Lemmas 6.3 and 6.4 to be non-trivial properties, without (to us) obvious proofs, and so we did not verify them in HERMIT. This did not inhibit the rest of the case study, as HERMIT allows an unverified lemma to be taken as an assumption. If such assumed lemmas are used in a calculation, by default HERMIT will issue a compiler warning. This ability to assume lemmas can be disabled by a HERMIT option, allowing the user to ensure that only verified lemmas are used. Finally, the simplification of the definition of expand is stated in the textbook without presenting any intermediate transformation steps [2, Page 40]. It is not obvious to us what those intermediate transformation steps would be, and thus we did not encode this simplification in HERMIT.

5.3

5.4

lemmas to be used or functions to be unfolded are merely named, relying on the reader to be able to deduce how it was applied. Here, forall-body and eq-rhs are navigation commands that direct HERMIT to descend into the body of the universal quantifier, and then into the right-hand side of the equivalence. one-td (once, traversing top-down) and any-td (anywhere, traversing top-down) are strategy combinators lifted from KURE [34]. In this calculation, and most others in the case study, we think that the HERMIT scripts are about as clear, and not much more verbose, than the textbook calculations. There was one exception though: manipulating the function-composition operator. 5.2

Associative Operators

Assumed Lemmas in the Textbook

Constructive Calculation

There was one proof technique used in the textbook that HERMIT does not directly support: calculating the definition of a function from an indirect specification. Specifically, the textbook postulates

As is common with pen-and-paper reasoning, several properties that are used in the textbook are assumed without an accompanying proof being given. This included some of the named lemmas from

31

comp-id-L comp-id-R comp-assoc comp-assoc4

map-id map-fusion map-strict zip-unzip filter-strict filter-split

∀f ∀f ∀f ∀f

. id ◦ f ≡ f . f ◦ id ≡ f g h. (f ◦ g) ◦ h ≡ f ◦ (g ◦ h) g h k l . f ◦ (g ◦ (h ◦ (k ◦ l))) ≡ (f ◦ (g ◦ (h ◦ k ))) ◦ l map id ≡ id ∀ f g. map (f ◦ g) ≡ map f ◦ map g ∀f. map f undefined ≡ undefined zip ◦ unzip ≡ id ∀f. filter f undefined ≡ undefined ∀ p q. (∀ x . q x ≡ False ⇒ p x ≡ False) ⇒ filter p ≡ filter p ◦ filter q

Table 2: Comparison of calculation sizes in ‘Making a Century’. Textbook HERMIT Commands Lines Transformation Navigation Fold Fusion assumed 19 20 Lemma 6.2 assumed 7 2 Lemma 6.3 assumed assumed Lemma 6.4 assumed assumed Lemma 6.5 assumed 6 4 Lemma 6.6 assumed 2 1 Lemma 6.7 assumed 3 1 Lemma 6.8 7 5 6 Lemma 6.9 1 4 4 Lemma 6.10 assumed 12 16 solutions 16 13 12 expand 19 22 21 Calculation

Figure 8: Auxiliary lemmas in the ‘Making a Century’ case study. the existence of an auxiliary function (expand ), uses that function in the conclusion of the fold-fusion rule, and then calculates a definition for that function from the indirect specification given by the fold-fusion pre-conditions. HERMIT is based around transforming existing definitions, and does not support this style of reasoning; so we were unable to replicate this calculation. However, we were able to verify the calculation by working in reverse: starting from the definition of expand , we proceeded to validate the use of the fold-fusion law by checking the corresponding pre-conditions. 5.5

During the case study we also discovered one error in the textbook. Specifically, the inferred type of the modify function [2, Page 39] does not match its usage in the program. We believe that its definition should include a concatMap, which would correct the type mismatch and give the program its intended semantics, so we have modified the function accordingly in our source code.

6.

There have been three main approaches taken to verifying properties of Haskell programs: testing, automated theorem proving, and equational reasoning. The most prominent testing tool is QuickCheck [6], which automatically generates large quantities of test cases in an attempt to find a counterexample. Other testing tools include SmallCheck [31], which exhaustively generates test values of increasing size so that it can find minimal counterexamples, and Lazy SmallCheck [28, 31], which also tests partial values. Jeuring et al. [22] have recently developed infrastructure to support using QuickCheck to test type-class laws, as well as to test the individual steps of user-provided equational reasoning. There are several tools that attempt to automatically prove properties of Haskell programs by interfacing with an automated theorem prover. These include Liquid Haskell [41, 42], Zeno [36], HALO [44], and the Haskell Inductive Prover (Hip) [30]. The general approach taken by these tools is to translate the Haskell program, via GHC Core, into a first-order logic. User-stated program properties are then checked by passing them to an external theorem prover for verification. For inductive proofs, these tools provide their own automated induction principle(s), which then invoke the external theorem prover as required. Another similar tool is HipSpec [7], which is built on top of Hip. The main novelty of HipSpec is that it infers suites of properties about programs from their definitions in a bottom-up fashion, rather than taking the goal-directed approach of the aforementioned tools which start from the userstated program properties and seek to prove them. Thus user-stated properties are optional: HipSpec can check user-stated properties against those it has inferred, but alternatively it can just generate program properties as documentation. Equational reasoning is used both to verify properties of Haskell programs and to validate the correctness of program transformations. Most equational reasoning about Haskell programs is performed manually with pen-and-paper or text editors, of which there are numerous examples in the literature [e.g. 2, 8, 17, 19, 25]. Prior to HERMIT there have been several tools for mechanical equational reasoning on Haskell programs, including the Haskell Equational Reasoning Assistant (HERA) [18], the Ulm Transformation System (Ultra) [21], and the Programming Assistant for Transforming Haskell (PATH) [40].

Calculation Sizes

As exemplified by Figure 7, the HERMIT scripts are roughly the same size as the textbook calculations. It is difficult to give a precise comparison, as the textbook uses both formal calculation and natural language. We present some statistics in Table 2, but we recommend not extrapolating anything from them beyond a rough approximation of the scale of the calculations. We give the size of the two main calculations (transforming solutions and deriving expand ), as well as those for the named lemmas. In the textbook we measure lines of natural-language reasoning as well as lines of formal calculation, but not definitions, statement of lemmas, or surrounding discussion. In the HERMIT scripts, we measure the number of transformations applied, and the number of navigation and strategy combinators used to direct the transformations to the desired location in the term. We do not measure commands for stating lemmas, loading files, switching between transformation and proof mode, or similar, as we consider these comparable to the surrounding discussion in the textbook. To get a feel for the scale of the numbers given, we recommend that the user compares the numbers for Lemma 6.8 in Table 2 to the calculation in Figure 7. 5.6

Related Work

Reflections

Our overall experience was that mechanising the textbook reasoning was fairly straightforward, and it was pleasing that we could translate most steps of the textbook calculations into an equivalent HERMIT command. The only annoyance was the occasional need to manually apply lemmas for manipulating operator associativity (see Section 5.2) so that the structure of the term would match the transformation we were applying. While having to specify where in a term each lemma must be applied does result in scripts that are a little more verbose than the textbook calculations, we do not necessarily consider that to be detrimental. Rather, we view a pen-and-paper calculation that does not specify the location as passing on that work to the reader, who must determine for herself where, and in which direction, a lemma is intended to be applied. Furthermore, when desired, strategy combinators can be used to avoid specifying precisely which sub-term the lemma should be applied to.

32

ity, but better parsing facilities would make working interactively with GHC Core much simpler for the user. In the past [1, 13] HERMIT has been used to successfully prototype GHC optimisations by encoding them as sequences of transformation steps. Now HERMIT can also be used to reason about any preconditions those transformation steps have, as well as to provide mechanical assurances about equational reasoning that is intended to prove properties of Haskell programs, including typeclass laws for instance declarations, and user optimisations stated as GHC rewrite rules. By applying and checking the user’s reasoning during compilation, HERMIT enforces a connection between the source code, the reasoning steps, and the compiled program. GHC plugins developed using HERMIT can then be deployed with Haskell’s Cabal packaging system, meaning that they integrate with a developer’s normal work-flow. HERMIT development is ongoing, and we seek to target ever-larger examples.

HERA was our own preliminary tool, and was a direct predecessor of HERMIT. HERA operated on Haskell source code, via Template Haskell [35]. However, the lack of typing information proved an obstacle to many non-syntactic transformations, such as worker/wrapper [19]. This was the primary reason for our switch to GHC Core when designing the HERMIT system, although the large size of the Template Haskell grammar was another consideration. Ultra has much in common with HERMIT in terms of functionality and available transformations. The main distinction is that Ultra operates on the source code of its own Haskell-like language. This language is a sub-language of Haskell 98 (notably excluding type classes), extended with non-executable descriptive operators (e.g. “there exists a value such that...”). The idea of the descriptive operators is to allow a user to express concise high-level specifications, which can then be transformed into executable programs. This differs from HERMIT, where our starting point is a valid GHC Core program. Ultra also comes with built-in support for a variety of algebraic structures (e.g. monoids), which makes reasoning about binary operators smoother than in HERMIT. PATH is also based on transforming a small Haskell-like language, called PATH-L. The PATH tool automatically translates from a sub-language of Haskell 98 (excluding type classes, among other things) to PATH-L. The user then performs equational reasoning on the PATH-L program, and finally PATH automatically converts the resultant program back to Haskell. PATH was designed with an emphasis on total correctness, and all PATH transformations are guaranteed not to introduce non-termination, even in the presence of infinite or partial values. Another tool similar to HERMIT is the Haskell Refactorer (HaRe) [24, 39], which supports user-guided refactoring of Haskell programs. However, the objective of HaRe is slightly different, as refactoring is concerned with program transformation, whereas HERMIT supports both transformation of programs and verification of program properties. The original version of HaRe targets Haskell 98 source code, but recently work has begun on a re-implementation of HaRe that targets GHC-extended Haskell.

7.

Acknowledgments We thank Patrik Jansson and the anonymous reviewers for their useful comments and suggestions. This material is based upon work supported by the National Science Foundation under Grant No. 1117569.

Appendix To give an idea of the complexity of defining composite lemmas using HERMIT’s transformation DSL, we present here the encoding of the filter-split lemma from the ‘Making a Century’ case study. filter-split ∀ p q. (∀ x . q x ≡ False ⇒ p x ≡ False) ⇒ filter p ≡ filter p ◦ filter q

We construct this lemma by building the corresponding GHC Core terms. This involves looking up the filter and (◦) functions, generating the universally quantified variables (of the correct types), and then constructing the GHC Core expressions and HERMIT lemma. To assist with this, HERMIT provides an assortment of smart constructors, including $$ for expression application and =⇒ and === for logical implication and equivalence. Note that implication lemmas are annotated with a name for the antecedent, so that it can be referred to when reasoning about the consequent.

Conclusions

We have presented two case studies of using HERMIT to perform equational reasoning on GHC Core programs. The first case study demonstrated that it is viable to verify type-class laws using HERMIT. The HERMIT scripts were uniformly brief, and predominantly consisted of unfolding definitions and simplification, with relatively simple reasoning steps. Additionally, we note that while we focused on type-class laws in that case study, the same approach can be used to verify GHC rewrite-rule pragmas. HERMIT now provides structural induction as a built-in transformation, and supports transformations that have preconditions, such as the fold-fusion law (used in our second case study) and the worker/wrapper transformation [19, 32]. In a prior publication [33] we described encoding the worker/wrapper transformation in HERMIT, and used it to optimise a variety of example programs. However, at the time HERMIT had no means of verifying the preconditions, so they were not mechanically checked. Using HERMIT’s new equational-reasoning infrastructure, we have updated the worker/wrapper encoding such that user scripts verifying the preconditions are checked before the transformation can be applied. All of the preconditions for the examples in that previous publication have now been verified in HERMIT, and the corresponding scripts are bundled with the HERMIT package [14]. The case studies did highlight HERMIT’s need for a good parser for GHC Core expressions and types. GHC once specified an External Core format, including a parser and pretty-printer, but External Core has recently been removed because it was not being maintained. HERMIT already features a strong pretty-printing capabil-

filterSplitLemma :: LemmaLibrary filterSplitLemma = do filterId ← findIdT "filter" compId ← findIdT "." constT $ do a ← newTyVar "a" let aTy

= mkTyVarTy a

p q x

← newVar "p" (aTy −→ boolTy) ← newVar "q" (aTy −→ boolTy) ← newVar "x" aTy

qx px filterp filterq filterpcomp filterpq

← q $$ x ← p $$ x ← filterId $$ p ← filterId $$ q ← compId $$ filterp ← filterpcomp $$ filterq

return $ newLemma "filter-split" $ mkForall [a, p, q ] $ ("filter-split-antecedent", mkForall [x ] (("qx-False", qx === falseDataConId) =⇒ (px === falseDataConId))) =⇒ filterp === filterpq

33

While this is not an ideal way of constructing composite lemmas, HERMIT can check that only well-typed GHC Core terms are produced, which is effective at catching mistakes. Furthermore, note that a lemma introduced in this way is not treated as an axiom any more than a lemma introduced via other means: the user must still verify the lemma using equational reasoning inside HERMIT. As future work, we aim to provide a parser for an extension of GHC rewrite-rule syntax, so that composite lemmas can be written in a familiar Haskell-like syntax, with type and dictionary arguments being automatically inferred and inserted.

[22] J. Jeuring, P. Jansson, and C. Amaral. Testing type class laws. In Haskell Symposium, pages 49–60. ACM, 2012. [23] H. Kirchner and P.-E. Moreau. Promoting rewriting to a programming language: A compiler for non-deterministic rewrite programs in associative-commutative theories. Journal of Functional Programming, 11(2):207–251, 2001. [24] H. Li, S. Thompson, and C. Reinke. The Haskell refactorer, HaRe, and its API. In Workshop on Language Descriptions, Tools, and Applications, volume 141 of ENTCS, pages 29–34. Elsevier, 2005. [25] E. Meijer, M. M. Fokkinga, and R. Paterson. Functional programming with bananas, lenses, envelopes and barbed wire. In Conference on Functional Programming Languages and Computer Architecture, volume 523 of LNCS, pages 124–144. Springer, 1991. [26] S.-C. Mu, H.-S. Ko, and P. Jansson. Algebra of programming in Agda: Dependent types for relational program derivation. Journal of Functional Programming, 19(5):545–579, 2009. [27] S. Peyton Jones, A. Tolmach, and T. Hoare. Playing by the rules: Rewriting as a practical optimisation technique in GHC. In Haskell Workshop, pages 203–233. ACM, 2001. [28] J. S. Reich, M. Naylor, and C. Runciman. Advances in lazy smallcheck. In International Symposium on Implementation and Application of Functional Languages, volume 8241 of LNCS, pages 53–70. Springer, 2013. [29] J. C. Reynolds. Towards a theory of type structure. In Colloque sur la Programmation, volume 19 of LNCS, pages 408–423. Springer, 1974. [30] D. Ros´en. Proving equational Haskell properties using automated theorem provers. Master’s thesis, University of Gothenburg, 2012. [31] C. Runciman, M. Naylor, and F. Lindblad. Smallcheck and Lazy Smallcheck: Automatic exhaustive testing for small values. In Haskell Symposium, pages 37–48. ACM, 2008. [32] N. Sculthorpe and G. Hutton. Work it, wrap it, fix it, fold it. Journal of Functional Programming, 24(1):113–127, 2014. [33] N. Sculthorpe, A. Farmer, and A. Gill. The HERMIT in the tree: Mechanizing program transformations in the GHC core language. In International Symposium on Implementation and Application of Functional Languages, volume 8241 of LNCS, pages 86–103. Springer, 2013. [34] N. Sculthorpe, N. Frisby, and A. Gill. The Kansas University Rewrite Engine: A Haskell-embedded strategic programming language with custom closed universes. Journal of Functional Programming, 24(4): 434–473, 2014. [35] T. Sheard and S. Peyton Jones. Template metaprogramming for Haskell. In Haskell Workshop, pages 1–16. ACM, 2002. [36] W. Sonnex, S. Drossopoulou, and S. Eisenbach. Zeno: An automated prover for properties of recursive data structures. In International Conference on Tools and Algorithms for the Construction and Analysis of Systems, volume 7214 of LNCS, pages 407–421. Springer, 2012. [37] M. Sulzmann, M. M. T. Chakravarty, S. Peyton Jones, and K. Donnelly. System F with type equality coercions. In Workshop on Types in Language Design and Implementation, pages 53–66. ACM, 2007. [38] J. Tesson, H. Hashimoto, Z. Hu, F. Loulergue, and M. Takeichi. Program calculation in Coq. In Algebraic Methodology and Software Technology, volume 6486 of LNCS, pages 163–179. Springer, 2011. [39] S. Thompson and H. Li. Refactoring tools for functional languages. Journal of Functional Programming, 23(3):293–350, 2013. [40] M. Tullsen. PATH, A Program Transformation System for Haskell. PhD thesis, Yale University, 2002. [41] N. Vazou, E. L. Seidel, and R. Jhala. LiquidHaskell: Experience with refinement types in the real world. In Haskell Symposium, pages 39– 51. ACM, 2014. [42] N. Vazou, E. L. Seidel, R. Jhala, D. Vytiniotis, and S. Peyton Jones. Refinement types for Haskell. In International Conference on Functional Programming, pages 269–282. ACM, 2014. [43] D. Vytiniotis and S. Peyton Jones. Evidence normalization in System FC. In International Conference on Rewriting Techniques and Applications, pages 20–38. Schloss Dagstuhl, 2013. [44] D. Vytiniotis, S. Peyton Jones, K. Claessen, and D. Ros´en. HALO: Haskell to logic through denotational semantics. In Symposium on Principles of Programming Languages, pages 431–442. ACM, 2013. [45] P. Wadler and S. Blott. How to make ad-hoc polymorphism less ad hoc. In Symposium on Principles of Programming Languages, pages 60–76. ACM, 1989.

References [1] M. D. Adams, A. Farmer, and J. P. Magalh˜aes. Optimizing SYB is easy! In Workshop on Partial Evaluation and Program Manipulation, pages 71–82. ACM, 2014. [2] R. Bird. Pearls of Functional Algorithm Design. Cambridge University Press, 2010. [3] T. Braibant and D. Pous. Tactics for reasoning modulo AC in Coq. In International Conference on Certified Programs and Proofs, volume 7086 of LNCS, pages 167–182. Springer, 2011. [4] J. Breitner, R. A. Eisenberg, S. Peyton Jones, and S. Weirich. Safe zero-cost coercions for Haskell. In International Conference on Functional Programming, pages 189–202. ACM, 2014. [5] R. M. Burstall and J. Darlington. A transformation system for developing recursive programs. Journal of the ACM, 24(1):44–67, 1977. [6] K. Claessen and J. Hughes. QuickCheck: A lightweight tool for random testing of Haskell programs. In International Conference on Functional Programming, pages 268–279. ACM, 2000. [7] K. Claessen, M. Johansson, D. Ros´en, and N. Smallbone. Automating inductive proofs using theory exploration. In International Conference on Automated Deduction, volume 7898 of LNCS, pages 392–406. Springer, 2013. [8] N. A. Danielsson and P. Jansson. Chasing bottoms: A case study in program verification in the presence of partial and infinite values. In International Conference on Mathematics of Program Construction, volume 3125 of LNCS, pages 85–109. Springer, 2004. [9] A. L. de M. Santos. Compilation by Transformation in Non-Strict Functional Languages. PhD thesis, University of Glasgow, 1995. [10] N. Dershowitz, J. Hsiang, N. A. Josephson, and D. A. Plaisted. Associative-commutative rewriting. In International Joint Conference on Artificial Intelligence, volume 2, pages 940–944. Morgan Kaufmann, 1983. [11] A. Farmer. HERMIT: Mechanized Reasoning during Compilation in the Glasgow Haskell Compiler. PhD thesis, University of Kansas, 2015. [12] A. Farmer, A. Gill, E. Komp, and N. Sculthorpe. The HERMIT in the machine: A plugin for the interactive transformation of GHC core language programs. In Haskell Symposium, pages 1–12. ACM, 2012. [13] A. Farmer, C. H¨oner zu Siederdissen, and A. Gill. The HERMIT in the stream: Fusing Stream Fusion’s concatMap. In Workshop on Partial Evaluation and Program Manipulation, pages 97–108. ACM, 2014. [14] A. Farmer, A. Gill, E. Komp, and N. Sculthorpe. http://hackage. haskell.org/package/hermit, 2015. [15] A. Farmer, N. Sculthorpe, and A. Gill. Hermit case studies: Proving Type-Class Laws & Making a Century, 2015. URL http://www. ittc.ku.edu/csdl/fpg/HERMIT/case-studies-2015/. [16] GHC Team. GHC User’s Guide, Version 7.8.4, 2014. URL http: //downloads.haskell.org/~ghc/7.8.4/docs/html. [17] J. Gibbons and G. Hutton. Proof methods for corecursive programs. Fundamenta Informaticae, 66(4):353–366, 2005. [18] A. Gill. Introducing the Haskell equational reasoning assistant. In Haskell Workshop, pages 108–109. ACM, 2006. [19] A. Gill and G. Hutton. The worker/wrapper transformation. Journal of Functional Programming, 19(2):227–251, 2009. [20] J.-Y. Girard. Interpr´etation fonctionelle et e´ limination des coupures de l’arithm´etique d’ordre sup´erieur. PhD thesis, Universit´e Paris Diderot, 1972. [21] W. Guttmann, H. Partsch, W. Schulte, and T. Vullinghs. Tool support for the interactive derivation of formally correct functional programs. Journal of Universal Computer Science, 9(2):173–188, 2003.

34

Formally Proving a Compiler Transformation Safe Joachim Breitner Karlsruhe Institute of Technology Germany [email protected]

Abstract

created a machine-checked proof not only of functional correctness, but also that the performance of the transformed program is not worse than the original one’s. The purpose of an arity analysis is to detect when function definitions can be eta-expanded to take more arguments at once, which allows the compiler to generate more efficient code when calling such a function. Call Arity [5], which was recently added to GHC, combines an arity analysis with a novel cardinality analysis based on co-call graphs to gain more precision in the presence of recursion. This precision is required to effectively allow foldl and related combinators to take part in list fusion. The cardinality analysis, which determines how often a function or a thunk is called, is required to eta-expand a thunk, as that is only safe if the thunk is called at most once. If the cardinality analysis were wrong and we would eta-expand a thunk that is called multiple times, we would lose the benefits of sharing and suddenly repeat work. A correctness proof with regard to a standard denotational semantics would not rule that out! A more detailed semantics is required instead. We use an abstract machine with an explicit heap to prove that the number of heap allocations does not increase by transforming the program, and explain why this is a suitable criterion for safety.

We prove that the Call Arity analysis and transformation, as implemented in the Haskell compiler GHC, is safe, i.e. does not impede the performance of the program. We formalized syntax, semantics, the analysis and the transformation in the interactive theorem prover Isabelle to obtain a machine-checked proof and hence a level of rigor rarely obtained for compiler optimization safety theorems. The proof is modular and introduces trace trees as a suitable abstraction in abstract cardinality analyses. We discuss the breadth of the formalization gap. Categories and Subject Descriptors D.1.1 [Programming Techniques]: Applicative (Functional) Programming; D.3.4 [Programming Languages]: Processors—Optimization; F.3.2 [Logics and Meanings of Programs]: Semantics of Programming Languages— Program analysis Keywords Functional programming, arity analysis, cardinality analysis, interactive theorem proving

1.

Introduction

A lot of the fun in working on compilers, especially those that are actively used, lies in coming up with clever analyses and transformations that make the compiler produce better code. Such developments are regularly the topic of well-received academic publications. The emphasis in such papers tends to be on the empirical side – awesome benchmark results, elegant implementations, real-world impact. A more formal, theoretical treatment is, however, not always given. Sometimes, a proof of functional correctness is included, which shows that the transformation will not change the meaning of the program. But for an optimization, we not only care about its functional correctness but also that the transformed program does not exhibit reduced performance. This operational property, here called safety (following [11]), is invisible to the semantics commonly employed in correctness proofs. And even if a proof of safety is given, this is rarely performed in a machine-verified way, which would provide the highest level of assurance on the correctness of the proof. In this work, we went all the way: We looked at the Call Arity analysis, formalized it in the interactive theorem prover Isabelle and

Our contributions are: • We provide a rigorous formal description of Call Arity and prove that it is safe, i.e. the transformed program does not perform more allocations than the original program. • Our proof is modular. We cleanly separate the arity analysis part (Sec. 4) from the cardinality part, and divide the cardinality part into a three-stage refinement proof (Sec. 5). This gives greater insight into their interaction, and provides reusable components for similar proofs. • We introduce infinite trace trees (Sec. 5.2) as a suitable domain for an abstract cardinality analysis. • We formalized a suitable semantics akin to Sestoft’s mark 1 abstract machine, the Call Arity analysis, the transformation and the safety proof in the theorem prover Isabelle. This gives us very high assurance of the correctness of this work, but also provides a data point on the question of how feasible machine-checked proofs of compiler transformations currently are (Sec. 6). • Finally, and of more general interest, we critically discuss the formalization gap left by our formalization and find that the gap is not always bridgeable by meta-arguments. In particular, we explain how the interaction of Call Arity with other components of the compiler effected a serious and intricate bug, despite the proof of correctness (Sec. 6.2).

Permission to make digital or hard copies of all or part of this work for personal or classroom use is granted without fee provided that copies are not made or distributed for profit or commercial advantage and that copies bear this notice and the full citation on the first page. Copyrights for components of this work owned by others than ACM must be honored. Abstracting with credit is permitted. To copy otherwise, or republish, to post on servers or to redistribute to lists, requires prior specific permission and/or a fee. Request permissions from [email protected]. Copyright is held by the owner/author(s). Publication rights licensed to ACM.

Haskell’15, September 3-4, 2015, Vancouver, BC, Canada ACM. 978-1-4503-3808-0/15/09...$15.00 http://dx.doi.org/10.1145/2804302.2804312

35

2.

Overview and Example

the resulting graph is t1 p g . In particular, we t2 can see that g and t1 are not going to be called together. Together with the fact that the body of the let-binding calls g at most once, we can describe the calls originating from the inner let with the graph t1 p t2 : Both g and t2 can be called

The remainder of the paper will necessarily be quite formal. In order to give a better intuition and overview, we first look at a small example in this section, and introduce the syntax, semantics, transformations and analyses more rigorously in the subsequent sections. A more elaborate motivation and explanation of the Call Arity analysis, including its effect on list fusion and benchmark results, can be found in [5]. 2.1

multiple times, but the absence of a loop at t1 implies the desired cardinality information: t1 is called at most once.

From the Example. . .

2.2

Consider the following Haskell program:

. . . to the General Case

This explanation might have been convincing for this example, but how would we prove that the analysis and transformation are safe in general? In order to do so, we first need a suitable semantics. The elegant standard denotational semantics for functional programs are unfortunately too abstract and admit no observation of program performance. Therefore, we use a standard small-step operational semantics similar to Sestoft’s mark 1 abstract machine. It defines a relation (Γ, e, S) ⇒∗ (Γ0 , e0 , S 0 ) between configurations consisting of a heap, a control, i.e. the current expression under evaluation, and a stack (Sec. 3.1). With that semantics, we could follow Sands [21] and measure performance by counting evaluation steps. But that is too finegrained: Our eta-expansion transformation causes additional beta-reductions to be performed during evaluation, and without subsequent simplification – which does happen in a real compiler, but which we do not want to include in the proof – these increase the number of steps in our semantics. Therefore, we measure the performance by counting the number of allocations performed during the evaluation. This is sufficient to detect accidental duplication of work, as shown by this gedankenexperiment: Consider a program e1 , which is transformed to e2 , and a subexpression e of e1 that also occurs in e2 . By replacing e with let x1 = x1,. . . , xn = xn in e, where the variables are fresh, we can force each evaluation of e to perform at least n allocations, for an arbitrary large choice of n. So unless e2 evaluates e at most as often as e1 does, we can choose n large enough to make e2 allocate more than e1 . Conversely, if our criterion holds, we can conclude that the transformation does not duplicate work. This measure is also realistic: When working on GHC, the number of bytes allocated by a benchmark or a test case is the prime measure that developers observe to detect improvements and regressions, as in practice, it correlates very well with execution time and memory usage, while being more stable across differing environments. A transformation is safe in this sense if the transformed program performs no more allocations than the original program. The arity transformation eta-expands expressions, so in order to prove it safe, we need to identify conditions when eta-expansion itself is safe, and ensure that these conditions are always met. A sufficient condition for the safety of an n-fold eta-expansion of an expression e is that whenever e is evaluated, the top n elements on the stack are arguments, as stated in Lemma 1. The safety proof for the arity analysis (Lemma 2) keeps track of some invariants during the evaluation which ensure that we can apply Lemma 1 whenever an eta-expanded expression is about to be evaluated. The proof is first performed for a naive arity analysis without a cardinality analysis, before formally introducing the concept of a cardinality analysis in Sec. 5. We do not simply prove safety of the co-call graph based analysis directly, but split it up into a series of increasingly concrete proofs, each building on the result of the previous, for two reasons: • It is nice to separate various aspects of the proof (i.e. the interaction of the arity analysis with the cardinality analysis; the gap between the steps of the semantics and the structurally

foo :: Int → Int foo a = let t1, t2 :: Int → Int t1 = f1 a t2 = f2 a in let g :: Int → Int → Int g x = if p x then t1 else g (x + t2 x) in g 1 2 Here two thunks, t1 and t2, are called from a recursive inner function g. They are thunks, because their definition is not in head normal form, so upon their first call, f1 resp. f2 will be called with the argument a, and the resulting value will be stored and used in later invocations of t1 resp. t2. As it stands, the function invocation g 1 2 will be compiled to rather inefficient code: The caller will have to evaluate g 1, which creates and returns a function closure. This will be analyzed for the number of arguments it expects, and only then 2 will be pushed onto the stack and the closure will be entered [19]. If g would take two arguments directly, the call to g would simply push 1 and 2 onto the stack and execute the code for g, or even pass them in registers, which would be much faster. The same reasoning applies to t1 and t2. Generally, we want to eta-expand a definition to match the number of arguments it is called with. We can actually eta-expand g to take two parameters: It is called with two arguments in the first place, and – assuming g is always called with two arguments – it calls itself with two arguments as well. So we may transform the definition of g to g x y = (if p x then t1 else g (x + t2 x)) y, which would then be further simplified by the compiler to g x y = if p x then t1 y else g (x + t2 x) y. We now see that both t1 and t2 are always called with one argument. Can we eta-expand their definitions to t1 y = f1 a y resp. t2 y = f2 a y? It depends! If we eta-expand t2 then the evaluation of f2 a will no longer be shared between multiple invocations of t2. As we do not know anything about f2 we have to pessimistically assume this to be an expensive operation that we must not suddenly repeat. We expect t2 to be called multiple times here, so a conservative arity analysis must not eta-expand it. For t1, we can do better: It is definitely called at most once, so it is safe to eta-expand its definition. That is why a good arity analysis needs the help of a precise cardinality analysis. How would that analysis figure that out? The body of g on its own calls both t2 and t1 at most once, so having cardinality information for subexpressions is not enough to attain such precision, and our cardinality analysis needs to keep track of more. The Call Arity analysis comes with a cardinality analysis based on the notion of co-call graphs. In these (non-transitive) graphs edges connect variables that might be called together. Looking at the definition of g, we see that p is called together with all the other variables, and g is called together with t2. Thus,

36

recursive nature of the analysis; different treatments of recursive and non-recursive bindings) into individual steps, but more importantly • while the co-call graph data structure is sufficiently expressive to implement the analysis, it is an unsuitable abstraction for the safety proof, as it cannot describe the recursion patterns of a heap, where some expressions are calling each other in a nice, linear fashion among other, more complex recursion patterns. In the first refinement, the cardinality analysis is completely abstract: Its input is the whole configuration and its result is simply which variables on the heap are going to be called more than once. In our example, after t1, t2 and g have been put on the heap, this analysis would find out that t2 and g are called more than once, but not t1. We give conditions (Definition 6) when an arity analysis using such a cardinality analysis is safe (Lemma 3). The next refinement assumes a cardinality analysis that now looks just at expressions, not whole configurations, and returns a much richer analysis result: A trace tree, which is a (possibly) infinite tree where each path corresponds to one possible execution and the edges are labeled by the variables called during that evaluation. In our example, the tree corresponding to the right-hand-side of g, namely p

t1 t2

x, y, z : Var e, v : Expr e ::= x

expressions variable

|ex

application

| λx. e

lambda abstraction

| Ct | Cf

constructor

| e ? et : ef

case analysis

| let Γ in e Γ, ∆ : Var * Expr

mutually recursive bindings heaps, bindings

Figure 1. A simple lambda calculus

Additionally, we do not need the full expressiveness of algebraic data types. We use booleans (Ct , Cf ) with an if-then-else construct as representatives for case analysis on data types. The “other” interesting feature of data constructors, i.e. that they are values that can contain possibly unevaluated code, can already be observed with function closures. Our syntax is given in Figure 1. The bindings of a let are represented as finite maps from variables to expressions; the same type is used to describe a heap. Like Launchbury [16] and others [12, 13, 25], we require application arguments to be variables. This ensures that all bindings on the heap are created by a let and we do not have to ensure separately that the evaluation of a function’s argument is shared. We denote the set of free variables of an expression e (or another object containing expressions) with fv(e), and e[x := y] is the expression e with every free occurrence of the variable x replaced by y. The predicate isVal(e) holds iff e is a lambda abstraction or a constructor. A heap Γ is a partial map from variable names to expressions. The set dom Γ := {x | (x 7→ e) ∈ Γ} contains all names bound in Γ, while thunks Γ := {x | (x 7→ e) ∈ Γ, ¬ isVal(e)} contains just those that are bound to thunks. Note that we consider heap-bound names to be free, i.e. dom Γ ⊆ fv Γ. The proper treatment of names is the major technical hurdle when rigorously formalizing anything related to the lambda calculus. We employ Nominal Logic [27] here, so the lambda abstractions and let-bindings are proper equivalency classes, i.e. λx. x = λy. y. A configuration (Γ, e, S) consists of the heap Γ, the control e and the stack S. The stack is constructed from • the empty stack, [], • arguments, written $x·S and put on the stack during the evaluation of an application, • update markers, written #x·S and put on the stack during the evaluation of a variable’s right-hand-side, and • alternatives, written (e1 : e2 )·S and put on the stack during the evaluation of the scrutinee of an if-then-else construct. Throughout this work we assume all configurations to be good, i.e. dom Γ and #S := {x | #x ∈ S} are disjoint and the update markers on the stack are distinct.

g

can be combined with the very simple tree g from the body of the inner let to form the infinite tree g p t1 g p t1 t2 g p t1 t2 g t2 which describes the overall sequence of calls. Clearly, on every possible path, t1 is called at most once. Given such a trace tree analysis, an abstract analysis as described in the first refinement can be implemented: The trees describing the expressions in a configuration (on the heap, as the control or in the stack) can be combined to a tree describing the behavior of the whole configuration. This calculation, named s in Sec. 5.2, is quite natural for trace trees, but would be hard to define on co-call graphs only. From that tree we can determine the cardinalities of the individual variables. We specify conditions on the trace tree analysis (Definition 9) and in Lemma 4 show them to be sufficient to fulfill the specification of the first refinement. The third and final refinement assumes an analysis that returns a co-call graph for each expression. Co-call graphs can be seen as compact approximations of trace trees, with edges between variables that can occur on the same path in the tree. The specification in Definition 10 is shown in Lemma 5to be sufficient to fulfill the specification of the second refinement. Eventually, we give the definition of the real Call Arity analysis in Sec. 5.4, and as it fulfills the specification of the final refinement, the desired safety theorem (Theorem 1) follows. The following three technical sections necessarily omit some detail, especially in the proofs. But since the machine-checked formalization exists, such omissions needn’t cause worry. The full Isabelle code is available at [4]; the proof document contains a table that maps the definitions and lemmas of this paper to the corresponding entities in the Isabelle development.

3.

variables

3.1

Semantics

Following Sestoft [25], we define the semantics via the single step relation ⇒, defined in Figure 2. We write ⇒∗ for the reflexive transitive closure of this relation, which describes a particular execution. In the interest of naming hygiene, the names for the new bindings in the LET rule have to be fresh with regard to what is already present on the heap and stack, as ensured by the side-condition.

Syntax and Semantics

Call Arity operates on GHC’s intermediate language Core, but that is too large for our purposes: The analysis completely ignores types, so we would like to work on an untyped representation.

37

(Γ, e x, S) ⇒ (Γ, e, $x·S) (Γ, λy. e, $x·S) ⇒ (Γ, e[y := x], S)

APP 2

(Γ, x, S) ⇒ (Γ \ x, e, #x·S)

VAR 1

(Γ, e, #x·S) ⇒ (Γ[x 7→ e], e, S)

VAR 2

(x 7→ e) ∈ Γ =⇒ isVal(e) =⇒

(Γ, (e ? et : ef ), S) ⇒ (Γ, e, (et : ef )·S) b ∈ {t, f} =⇒ (Γ, Cb , (et : ef )·S) ⇒ (Γ, eb , S)

Tα (let Γ in e) = let TAα (Γ,e) (Γ) in Tα (e)

APP 1

The actual transformation happens at a binding, where we eta-expand bound expressions according to the result of the arity analysis. If the analysis determines that a binding is never called, we simply leave it alone:   h i e if α(x) ¯ = ⊥ Tα¯ (Γ) = x 7→ (x 7→ e) ∈ Γ . Eα (Tα (e)) if α(x) ¯ =α

IF 1 IF 2

As motivated earlier, we consider an arity analysis A safe if the transformed program does not perform more allocations than the original program. A – technical – benefit of this measure is that the number of allocations always equals the size of the heap plus the number of update markers on the stack, as no garbage collector is modeled in our semantics:

dom ∆ ∩ fv(Γ, S) = {} =⇒ (Γ, let ∆ in e, S) ⇒ (∆ · Γ, e, S)

LET

Figure 2. The operational semantics

Definition 1 (Safe transformation) A program transformation T is safe if for every execution

An interesting side-effect is that this rule, and hence the whole semantics, is not deterministic, as there is an infinite number of valid names that can be used when putting the bindings onto the heap. Consequently, our proofs cannot take short-cuts using determinism, which would be a problem if “real” nondeterminism were added to the formalism. Note that the semantics takes good configurations to good configurations. This semantics is equivalent to Launchbury’s natural semantics [16], which in turn is correct and adequate with regard to a standard denotational semantics; we have machine-verified these these proofs as well [3]. 3.2

([], e, []) ⇒∗ (Γ, v, []) with isVal(v), there is an execution ([], T(e), []) ⇒∗ (Γ0 , v 0 , []) with isVal(v 0 ) and | dom Γ0 | ≤ | dom Γ|. An arity analysis A is safe if the transformation T is safe.

Specification We begin by stating sufficient conditions for an arity analysis to be safe. In order to phrase the conditions, we also need to know the arities an expression e calls its free variables with, assuming it is itself called with α arguments:

Arities and Eta-Expansion

Eta-expansion replaces an expression e by (λx. e x). The n-fold eta-expansion is described by En (e) := (λz1 . . . zn . e z1 . . . zn ), where the zi are distinct and fresh with regard to e. We thus consider an expression e to have arity α ∈ N if we can replace it by Eα (e) without negative effect on the performance. Other analyses determine the arity based on the definition of e, i.e. its internal arity [28]. Here, we treat e as a black box and instead look at its context to determine its external arity. For that, we can give an alternative definition: An expression e has arity α if upon every evaluation of e, there are at least α arguments on the stack. If an expression has arity α, then it also has arity α0 for α0 ≤ α; every expression has arity 0. Our lattice therefore is:

Aα (e) : Var → N⊥ For notational simplicity, we define A⊥ (e) := ⊥. The specification consists of a few naming hygiene conditions and an inequality for most syntactical constructs: Definition 2 (Arity analysis specification)

z∈ / {x, y} =⇒ x, y ∈ / dom Γ =⇒

· · · < 3 < 2 < 1 < 0. For convenience, we set 0 − 1 = 0. By convention, α ¯ is a partial map from variable names to arities, and α˙ is a list of arities.

4.

2

Arity Analyses

An arity analysis is thus a function that, given a binding (Γ, e), consisting of variable names bound to right-hand-sides in Γ and the body e, determines the arity of each of the bound expressions. It depends on the number α of arguments passed to e and may return ⊥ for a name that is not called at all:

dom Aα (e) ⊆ fv e dom Aα (Γ, e) ⊆ dom Γ Aα (e[x := y]) z = Aα (e) z

(A-dom) (Ah-dom) (A-subst)

Aα (Γ[x := y], e[x := y]) = Aα (Γ, e)

(Ah-subst)

[x 7→ α] v Aα (x) Aα+1 (e) t [x 7→ 0] v Aα (e x)

(A-Var) (A-App)

Aα−1 (e) \ {x} v Aα (λx. e) A0 (e) t Aα (et ) t Aα (ef ) v Aα (e ? et : ef )

(A-Lam) (A-If)

AAα (Γ,e) (Γ) t Aα (e) v Aα (Γ, e) t Aα (let Γ in e) (A-Let) F where Aα (Γ) := A(α x) (e) (x 7→ e) ∈ Γ . 2

Aα (Γ, e) : Var → N⊥ .

These conditions come quite naturally: An expression should not report calls to names that it does not know about. Replacing one variable by another should not affect the arity of other variables. A variable, evaluated with a certain arity, should report (at most) that arity. In the rules for application and lambda abstraction we keep track of the number of arguments. As we model a forward analysis which looks at bodies before right-hand-sides, we get no useful information on how the argument x in an application e x is called by e. In rule (A-If), the scrutinee is evaluated without arguments, hence it is analyzed with arity 0.

Given such an analysis, we can run it over a program and transform it accordingly. We traverse the syntax tree, while keeping track of the number of arguments passed: Tα (x) = x Tα (e x) = Tα+1 (e) x Tα (λx. e) = (λx. Tα−1 (e)) Tα (Cb ) = Cb for b ∈ {t, f} Tα (e ? et : ef ) = T0 (e) ? Tα (et ) : Tα (ef )

38

• Aα (Γ) t Aα (e) t A˙ α˙ (S) v α, where dom Γ∪#S



The rule (A-Let) is a concise way to capture a few requirements. Note that, by (A-dom) and (Ah-dom), the domains of Aα (Γ, e) and Aα (let Γ in e) are disjoint, i.e. Aα (Γ, e) contains the information on how the names of the current binding are called, while Aα (let Γ in e) informs us about the free variables. The left-hand side contains all possible calls, both from the body of the binding and from each bound expression. These are analyzed with the arity reported by Aα (Γ, e). The occurrence of Aα (Γ, e) on both sides of the inequality anticipates the fixed-point iteration in the implementation of the analysis. Definition 2 suffices to prove functional correctness, i.e.

A˙ [] ([]) := ⊥ ˙ Aα·α˙ ((et : ef )·S) := Aα (et ) t Aα (ef ) t A˙ α˙ (S) A˙ α˙ ($x·S) := [x 7→ 0] t A˙ α˙ (S) A˙ α˙ (#x·S) := [x 7→ 0] t A˙ α˙ (S), and • α˙ . S, defined as

[] . [] α·α˙ . (et : ef )·S ⇐⇒ α˙ . S ∧ args(S) v α

JT0 (e)K = JeK, holds, but not safety, as the issue of thunks is not touched upon yet. Without the aid of a cardinality analysis, an arity analysis has to simply give up when it comes across a thunk:

⇐⇒ α˙ . S.

2

Definition 5 (No-cardinality arity annotation consistency) defined as (α, ¯ α, α) ˙ .N (Γ, e, S), iff (α, ¯ α, α) ˙ . (Γ, e, S) and α ¯ x = 0 for all x ∈ thunks Γ. 2

(Ah-thunk) 2

We do not include this requirement in definition of . as we extend it differently when we add a cardinality analysis. Clearly (⊥, 0, []) is a consistent annotation for an initial configuration ([], e, []). We will take consistently annotated configurations to consistently annotated configurations during the evaluation – with one exception, which causes a minor technical overhead: Upon evaluation of a variable x, its binding x 7→ e is always taken off the heap first, even when it is already evaluated, i.e. isVal(e):

Safety The safety of an eta-expanding transformation rests on the simple observation that, given enough arguments on the stack, an etaexpanded expression evaluates to the original expression: Lemma 1 (Γ, Eα (e), $x1 · · · $xα ·S) ⇒∗ (Γ, e, $x1 · · · $xα ·S)

⇐⇒ α˙ . S

α˙ . #x·S

As this definition does not consider the issue of thunks, we extend it to

Definition 3 (No-cardinality analysis specification) x ∈ thunks Γ =⇒ Aα (Γ, e) x = 0

α˙ . $x·S

2

P ROOF

(Γ[x 7→ e], x, S) ⇒ (Γ, e, #x·S) ⇒ (Γ[x 7→ e], e, S)

(Γ, Eα (e), $x1 · · · $xα ·S) = (Γ, (λz1 . . . zα . e z1 . . . zα ), $x1 · · · $xα ·S) ⇒∗ (Γ, e x1 . . . xα , S) ∗

⇒ (Γ, e, $x1 · · · $xα ·S)

We would not be able to prove consistency in the intermediate state. To work around this issue, assume that rule VAR1 has an additional constraint ¬ isVal(e) and that the rule

{ by APP2 } { by APP1 } 

(x 7→ e) ∈ Γ, isVal(e) =⇒ (Γ, x, S) ⇒ (Γ, e, S)

(VAR01 )

is added. This modification makes the semantics skip over one step, which is fine (and closer to what happens in reality).

So the safety proof for the whole transformation now just has to make sure that whenever we evaluate an eta-expanded value, there are enough arguments on top of the stack. Let args(S) denote the number of arguments on top of the stack. During evaluation, we need to construct the transformed configurations. Therefore, we need to keep track of the arity argument to each contained expressions: those on the heap, the control and those in alternatives on the stack. Together, these arguments form an arity annotation written (α, ¯ α, α). ˙ Given such an annotation, we can transform a configuration:

Lemma 2 Assume A fulfills the Definitions 2 and 3. If we have (Γ, e, S) ⇒∗ (Γ0 , e0 , S 0 ) and (α, ¯ α, α) ˙ .N (Γ, e, S), then there exists an arity annotation (α ¯ 0 , α0 , α˙ 0 ) with (α ¯ 0 , α0 , α˙ 0 ) .N 0 0 0 ∗ 0 0 0 (Γ , e , S ), and T(α,α, ¯ α) ˙ ((Γ, e, S)) ⇒ T(α ¯ 0 ,α0 ,α ˙ 0 ) ((Γ , e , S )).2 P ROOF by the individual steps of ⇒∗ . For APP1 we have Aα+1 (e) t A˙ α˙ ($x·S) = Aα+1 (e) t [x 7→ 0] t A˙ α˙ (S) v Aα (e x) t A˙ α˙ (S)

˙ ˙ (S)) T(α,α, ¯ (Γ), Tα (e), Tα ¯ α) ˙ ((Γ, e, S)) = (Tα

˙ So with (α, using (A-App) and the definition of A. ¯ α, α) ˙ .N (Γ, e x, S) we have (α, ¯ α + 1, α) ˙ .N (Γ, e, $x·S). Furthermore

where the stack is transformed by T˙ α·α˙ ((et : ef )·S) = (Tα (et ) : Tα (ef ))·T˙ α˙ (S) T˙ α˙ ($x·S) = $x·T˙ α˙ (S)

˙ ˙ (S)) T(α,α, ¯ (Γ), (Tα+1 (e)) x, Tα ¯ α) ˙ ((Γ, e x, S)) = (Tα ˙ ⇒ (Tα¯ (Γ), Tα+1 (e), $x·Tα˙ (S))

T˙ α˙ (#x·S) = #x·T˙ α˙ (S) T˙ α˙ ([]) = [].

= T(α,α+1, ¯ α) ˙ ((Γ, e, $x·S)) by rule APP1 . The other cases follow this pattern, where the inequalities in Definition 2 ensure the preservation of consistency. In case VAR1 the variable x is bound to a thunk. From consistency we obtain α ¯ x = 0, so we can use E0 (T0 (e)) = T0 (e). Similarly, α=α ¯ x = 0 holds in case VAR2 . The actual eta-expansion is handled is case VAR01 : We have

While carrying the arity annotation through the evaluation of our programs, we need to ensure that it stays consistent with the current configuration. Definition 4 (Arity annotation consistency) An arity annotation is consistent with a configuration, written (α, ¯ α, α) ˙ . (Γ, e, S), if • dom α ¯ ⊆ dom Γ ∪ #S, • args(S) v α,

args(T˙ α˙ (S)) = args(S) v α v Aα (x) x v α ¯ x,

39

5.1

from consistency and (A-Var) and hence 0 VAR 1

˙ ˙ (S)) { } T(α,α, ¯ (Γ), Eα ¯ x (Tα ¯ x (e)), Tα ¯ α) ˙ ((Γ, x, S)) ⇒ (Tα ∗ ˙ ⇒ (Tα¯ (Γ), Tα¯ x (e), Tα˙ (S)) { by Lemma 1 }

Abstract Cardinality Analysis

So the arity analysis needs an accompanying cardinality analysis which prognoses how often a bound variable is going to be evaluated: This is modeled as a function

= T(α, ¯ α ¯ x,α) ˙ ((Γ, e, S)).

Cα (Γ, e) : Var → Card

Case LET1 : The new variables in ∆ are fresh with regard to Γ and S, hence also with regard to α ¯ according to the naming hygiene conditions in (α, ¯ α, α) ˙ .N (Γ, let ∆ in e, S). So in order to have (Aα (∆, e) t α, ¯ α, α) ˙ . (∆ · Γ, e, S), it suffices show ¯ (AAα (∆,e) (∆) t Aα (e)) dom ∆∪dom Γ∪#S v Aα (∆, e) t α, which follows from (A-Let) and Aα (let ∆ in e) dom Γ∪#S v α. ¯ The requirement Aα (∆, e) x = 0 for x ∈ thunks ∆ holds by (Ah-thunk). 

where Card is the three element lattice ⊥ < 1 < ∞, corresponding to “not called”, “called at most once” and “no information”, respectively. We use γ for an element of Card and γ¯ for a mapping Var → Card. The expression γ¯ − x, which subtracts one call from the prognosis, is defined as ( ⊥ if y = x and γ¯ y = 1 (¯ γ − x) y = γ¯ y otherwise.

The main take-away of this lemma is the following corollary, which states that the transformed program performs the same number of allocations as the original program.

Specification We start with a very abstract specification for a safe cardinality analysis and prove that an arity transformation using it is still safe. We stay oblivious in how the analysis works and defer that to the next refinement step in Section 5.2. For the specification we not only need the local view on one binding, as provided by Cα (Γ, e), but also a prognosis on how often each variable is called by a complete and arity-annotated configuration:

Corollary 1 The arity analysis is safe (in the sense of Definition 1): If ([], e, []) ⇒∗ (Γ, v, []), then there exists Γ0 and v 0 such that ([], T0 (e), []) ⇒∗ (Γ0 , v 0 , []) where Γ and Γ0 contain the same number of bindings. 2 ¯ α and P ROOF We have (⊥, 0, []) .N ([], e, []). Lemma 2 gives us α, α˙ so that T(⊥,0,[]) (([], e, [])) ⇒∗ T(α,α, ¯ (Γ) ¯ α) ˙ ((Γ, v, [])) and Tα binds the same names as Γ.  A Concrete Arity Analysis So far, we have a specification for an arity analysis and a proof that every analysis that fulfills the specification is safe. One possible implementation is the trivial arity analysis, which does not do anything useful and simply returns the most pessimistic result: Aα (e) := [x 7→ 0 | x ∈ fv e] and Aα (Γ, e) := [x 7→ 0 | x ∈ dom Γ]. A more realistic arity analysis is defined by

C(α,α, ¯ α) ˙ ((Γ, e, S)) : Var → Card Definition 6 (Cardinality analysis specification) The cardinality prognosis and cardinality analysis fulfill some obvious naming hygiene conditions: dom Cα (∆, e) = dom Aα (∆, e) dom C(α,α, ¯ α) ˙ ((Γ, e, S)) ⊆ fv(Γ, e, S) α ¯ dom Γ = α ¯ 0 dom Γ =⇒

Aα (x) := [x 7→ α] Aα (e x) := Aα+1 (e) t [x 7→ 0]

(C-dom)

C(α,α, ¯ α) ˙ ((Γ, e, S)) = C(α ¯ 0 ,α,α) ˙ ((Γ, e, S)) (C-cong)

Aα (λx. e) := Aα−1 (e) \ {x} Aα (e ? et : ef ) := A0 (e) t Aα (et ) t Aα (ef ) Aα (Cb ) := ⊥ for b ∈ {t, f}

α ¯ x = ⊥ =⇒ C(α,α, ¯ α) ˙ ((Γ, e, S)) = C(α,α, ¯ α) ˙ ((Γ \ {x}, e, S)) (C-not-called)

Aα (let Γ in e) :=

Furthermore, the cardinality analysis is likewise a forward analysis and has to be conservative about function arguments:

(µα. ¯ Aα¯ (Γ) t Aα (e) t [x 7→ 0 | x ∈ thunks Γ]) \ dom Γ and

$x ∈ S =⇒

[x 7→ ∞] v C(α,α, ¯ α) ˙ ((Γ, e, S)) (C-args)

Aα (Γ, e) := The prognosis may ignore update markers on the stack:

(µα. ¯ Aα¯ (Γ) t Aα (e) t [x 7→ 0 | x ∈ thunks Γ]) dom Γ

C(α,α, ¯ α) ˙ ((Γ, e, #x·S)) v C(α,α, ¯ α) ˙ ((Γ, e, S)) (C-upd)

where (µα. ¯ . . .) denotes the least fixed point, which exists as the involved operations are continuous and monotone in α. ¯ Moreover, the fixed point can be found in a finite number of steps by iterating from ⊥, as the carrier of α ¯ is bounded by the finite set fv Γ ∪ fv e, and the pointwise partial order on arities has no infinite ascending chains. As this ignores the issues of thunks, it corresponds to the analysis described by Gill [10]. This implementation fulfills Definition 2 and Definition 3, so by Corollary 1, it is safe.

5.

(Ch-dom)

An imminent call is prognosed: [x 7→ 1] v C(α,α, ¯ α) ˙ ((Γ, x, S)) (C-call) Evaluation improves the prognosis: Note that in (C-Var1 ) and (CVar01 ), we account for the call to x with the − operator. C(α,α+1, ¯ α) ˙ ((Γ, e, $x·S)) v C(α,α, ¯ α) ˙ ((Γ, e x, S)) (C-App) := C(α,α−1, ((Γ, e[y x], S)) v C ((Γ, λy. e, $x·S)) ¯ α) ˙ (α,α, ¯ α) ˙ (C-Lam)

Cardinality Analyses

(x 7→ e) ∈ Γ, ¬ isVal(e) =⇒

The previous section proved the safety of a straight-forward arity analysis. But it was severely limited by not being able to eta-expand thunks, which is desirable in practice.

C(α, ¯ α ¯ x,α) ˙ ((Γ \ {x}, e, #x·S)) v C(α,α, ¯ α) ˙ ((Γ, x, S)) − x (C-Var1 )

40

(x 7→ e) ∈ Γ, isVal(e) =⇒

We also keep track of γ¯ : Var → Card, the current cardinalities of the variables on the heap:

C(α, ¯ α ¯ x,α) ˙ ((Γ, e, S)) v C(α,α, ¯ α) ˙ ((Γ, x, S)) − x (C-Var01 )

Definition 8 (Cardinality arity annotation consistency) We write (α, ¯ α, α, ˙ γ¯ , r) .C (Γ, e, S), iff

isVal(e) =⇒ C(α,0, ¯ α) ˙ ((Γ[x 7→ e], e, S)) v C(α,0, ¯ α) ˙ ((Γ, e, #x·S)) (C-Var2 ) C(α,0,α· ((Γ, e, (e : e )·S)) v C ((Γ, e ? e : ef , S)) t t f ¯ α) ˙ (α,α, ¯ α) ˙ (C-If1 ) b ∈ {t, f} =⇒ C(α,α, ¯ α) ˙ ((Γ, eb , S)) v C(α,0,α· ¯ α) ˙ ((Γ, Cb , (et : ef )·S)) (C-If2 )

Lemma 3 Assume A and C fulfill the specifications in Definitions 2 and 6. If (Γ, e, S) ⇒∗ (Γ0 , e0 , S 0 ) and (α, ¯ α, α, ˙ γ¯ , r) .C (Γ, e, S) , then there exists (α ¯ 0 , α0 , α˙ 0 , γ¯ 0 , r0 ) such that (α ¯ 0 , α0 , α˙ 0 , γ¯ 0 , r0 ) .C ∗ 0 0 0 (Γ0 , e0 , S 0 ), and T(α,α, ((Γ, e, S)−r) ⇒ T ¯ α) ˙ (α ¯ 0 ,α0 ,α ˙ 0 ) ((Γ , e , S )− # r0 ). 2

The specification for the let-bindings connects the arity analysis, the cardinality analysis and the cardinality prognosis: dom ∆ ∩ fv(Γ, S) = {}, dom α ¯ ⊆ dom Γ ∪ #S =⇒ C(Aα (∆,e)tα,α, ¯ α) ˙ ((∆ · Γ, e, S)) v Cα (∆, e) t C(α,α, ¯ α) ˙ ((Γ, let ∆ in e, S)) (C-Let) Finally, we need an equivalent to Definition 3 that now restricts the arity analysis only for thunks that might be called more than once:

The lemma is an analog to Lemma 2. The main difference, besides the extra data to keep track of, is that we produce an evaluation in the forgetful semantics, with some bindings removed.

x ∈ thunks Γ, Cα (Γ, e) x = ∞ =⇒ Aα (Γ, e) x = 0 (Ah-∞-thunk)

P ROOF by the individual steps of ⇒∗ . The preservation of the arity annotation consistency in the proof of Lemma 2 can be used here as well. Note that both the arity annotation requirement and the transformation are applied to (Γ, e, S)−r, so this goes well together. The correctness of the cardinality information (the second condition in Definition 8) follows easily from the inequalities in Definition 6. We elaborate only on the interesting cases: Case VAR1 : We cannot have γ¯ x = ⊥ because of (C-call). If γ¯ x = ∞ we get α ¯ x = 0, as before, and nothing surprising happens. If γ¯ x = 1, we know that this is the only call to x, so we set r0 = r ∪ {x}, γ¯ 0 = γ¯ − x and use DROP U PD to get rid of the mention of #x on the stack. Case VAR2 : If x ∈ / r, proceed as before. If x ∈ r, then the transformed configurations are identical and the ⇒∗# judgment follows from reflexivity. 

2

Safety The safety proof proceeds similarly to the one for Lemma 2. But now we are allowed to eta-expand thunks that are called at most once. This has considerable technical implications for the proof: • An eta-expanded expression is a value, so in the transformed

program, VAR2 occurs immediately after VAR1 . In the original program, however, an update marker stays on the stack until the expression is evaluated to a value, and then VAR2 fires without a correspondence in the evaluation of the transformed program. In particular, the update marker can interfere with uses of Lemma 1. • Because the eta-expanded expression is a value, it stays on

the heap as it is, whereas in the original program, it is first evaluated. Evaluation can reduce the number of free variables of the expression, so subsequent choices of fresh variables in LET1 in the original evaluation might not be suitable in the evaluation of the transformed program.

Corollary 2 The cardinality based arity analysis is safe for closed expressions, i.e. if fv e = {} and ([], e, []) ⇒∗ (Γ, v, []) then there exists Γ0 and v 0 such that ([], T0 (e), []) ⇒∗ (Γ0 , v 0 , []) where Γ and Γ0 contain the same number of bindings. 2

A more complicated variant of Lemma 1 and carrying a variable renaming around throughout the proof might solve these problems, but would complicate it too much. We therefore apply a small trick and simply allow unwanted update markers to disappear, by defining a variant of the semantics:

P ROOF We need fv e = {} to have C⊥,0,[] (([], e, [])) = ⊥, so that (⊥, 0, [], ⊥, []) .C ([], e, []) holds. Now Lemma 2 gives us α, ¯ α, α˙ and r so that T(⊥,0,[]) (([], e, [])) ⇒∗# T(α,α, ¯ α) ˙ ((Γ, v, []) − r). As the forgetful semantics only drops unused bindings, but does not otherwise behave any different than the real semantics, a technical lemma allows us to recover T(⊥,0,[]) (([], e, [])) ⇒∗ 0 0 0 0 T(α,α, ¯ (Γ) − r = Γ − r . As r ⊆ Γ ¯ α) ˙ ((Γ , v, [])) for a Γ where Tα and r0 ⊆ Γ0 , this concludes the proof of the corollary: Γ, Tα¯ (Γ) and Γ0 all bind the same variables. 

Definition 7 (Forgetful semantics) The relation ⇒# is defined by (Γ, e, S) ⇒ (Γ0 , e0 , S 0 ) =⇒ (Γ, e, S) ⇒# (Γ0 , e0 , S 0 ). and (Γ, e, #x·S) ⇒# (Γ, e, S)

the arity information is consistent, (α, ¯ α, α) ˙ . (Γ, e, S) − r, dom α ¯ = dom γ¯ , the cardinality information is correct, C(α,α, ¯, ¯ α) ˙ ((Γ, e, S)) v γ many-called thunks are not going to be eta-expanded, i.e. α ¯x= 0 for x ∈ thunks Γ with γ¯ x = ∞ and • only bindings that are not going to be called (¯ γ x = ⊥) are removed, i.e. r ⊆ (dom Γ ∪ #S) − dom γ¯ . 2 • • • •

DROP U PD 2

5.2

This way, a one-shot binding can disappear completely after it has been called, making it easier to relate the original program to the transformed program. Because ⇒# contains ⇒, Lemma 1 holds here as well. Afterwards, and outside the scope of the safety proof, we will recover the original semantics from the forgetful semantics. In the proof we keep track of the set of removed bindings (named r), and write (Γ, e, S) − r := (Γ \ r, e, S − r) for the configuration with bindings from the set r removed. The stack (S −r) is S without update markers #x where x ∈ r.

Trace Tree Cardinality Analysis

In the second refinement, we look – still quite abstractly – at the implementation of the cardinality analysis. For the arity information, the type of the result required for the transformation (Var → N⊥ ) was sufficiently rich to be used in the analysis as well. This is unfortunately not the case for the cardinality analysis: Even if we know that an expression calls x and y each at most once, this does not tell us whether these calls can occur together (as in e x y) or whether they are exclusive (as in e ? x : y).

41

arity α the tree Tα (e) of calls to free variables of e which are performed by evaluating e with α arguments and using the result in any way. We write Tα¯ (Γ) for the analysis lifted to bindings, returning ⊥ for variables not bound in Γ or mapped to ⊥ in α. ¯ We also need a variant Tα (Γ, e) that, given bindings Γ, an expression e and an arity α, reports the calls on dom Γ performed by e and Γ with these bindings in scope. We can now identify conditions on T that allow us to satisfy the specifications in Definition 6.

So we need a richer type that captures the future calls of an expression, can distinguish different code paths and maps easily to Var → Card: The type TTree of (possibly infinite) trees, where each edge is labeled with variable name, and a node has at most one outgoing edge for each variable name. The paths in the tree correspond to the possible executions and the labels on the edges record each occurring variable call. We use t for values of type TTree. There are other, equivalent ways to interpret this type: Each TTree corresponds to a non-empty set of (finite) lists of variable names that is prefixed-closed (i.e. for every list in the set, its prefixes are also in the set). Each such list corresponds to a (finite) path in the tree. The function paths : TTree → 2[Var] implements this correspondence. Another view is given by the function

Definition 9 (Tree cardinality analysis specification) We expect the cardinality analysis to agree with the arity analysis on which variables are called at all:

next : Var → TTree → TTree⊥ ,

dom Tα (e) = dom Aα (e)

(T-dom)

dom Tα (Γ, e) = dom Aα (Γ, e)

(Th-dom)

Inequalities for the syntactic constructs:

0

where next x t = t iff the root of t has an edge labeled x leading to t0 , and next x t = ⊥ if the root of t has no edge labeled x. In that sense, TTree represents automata with labeled transitions. The basic operations on trees are ⊕, given by paths(t ⊕ t0 ) = paths t ∪ paths t0 , and ⊗, where paths(t ⊗ t0 ) is the set of all interleavings of lists from paths t with lists from paths t0 . We write t∗ for t ⊗ t ⊗ t ⊗ · · · . A tree is called repeatable if t = t ⊗ t = t∗ . The partial order used on TTree is t v t0 ⇐⇒ paths t ⊆ paths t0 . We write for the tree with no edges and x for x , the tree with exactly one edge labeled x. The tree t \ V is t with all edges with labels in V contracted, t V is t with all edges but those labeled with variables in V contracted. If we have a binding (Γ, e), and for e as well as for all bound expressions, we have a TTree describing their calls, how would we combine that information? A first attempt might be a function s : (Var → TTree) → TTree → TTree defined by ( ⊥ if next x t = ⊥ ¯ next x (s t t) := s t¯ (t0 ⊗ t¯ x) if next x t = t0 ,

x∗ ⊗ Tα+1 (e) v Tα (e x)

(T-App)

(Tα−1 (e)) \ {x} v Tα (λx. e) (T-Lam) ∗ Tα (e[y := x]) v x ⊗ (Tα (e)) \ {y} (T-subst) x v Tα (x) T0 (e) ⊗ (Tα (et ) ⊕ Tα (ef )) v Tα (e ? et : ef )

(T-Var) (T-If)

(sthunks Γ (TAα (Γ,e) (Γ)) (Tα (e))) \ dom Γ v Tα (let Γ in e) (T-Let) For values, analyzed without arguments, the analysis is expected to return a repeatable tree: isVal(e) =⇒ T0 (e) is repeatable

(T-value)

The specification for Aα (Γ, e) is closely related to (T-Let): (sthunks Γ (TAα (Γ,e) (Γ)) (Tα (e))) dom Γ v Tα (Γ, e) (Th-s) And finally, the connection to the arity analysis: x ∈ thunks Γ, c(Tα (Γ, e)) x = ∞ =⇒ (Aα (Γ, e)) x = 0 (Th-∞-thunk)

that traverses the tree t and upon every call interleaves the tree of the called name, t¯ x, with the remainder of t. This is a good start, but it does not cater for thunks, where the first call behaves differently than later calls. Therefore, we have to tell s which variables are bound to thunks, and give them special treatment: After a variable x referring to a thunk is evaluated, we pass on a modified map where t¯ x = . Hence s : 2Var → (Var → TTree) → TTree → TTree is defined by

2

Safety If we have a tree cardinality analysis, we can define a cardinality analysis in the sense of the previous section. The definition for Cα (Γ, e) is straight forward: Cα (Γ, e) := c(Tα (Γ, e)).

next x (sT t¯ t)   if next x t = ⊥ ⊥ := sT t¯ (t0 ⊗ t¯ x) if next x t = t0 , x ∈ /T  s (t¯[x 7→ ]) (t0 ⊗ t¯ x) if next x t = t0 , x ∈ T. T

In order to define C(α,α, ¯ α) ˙ ((Γ, e, S)) we need to fold the tree cardinality analysis over the stack: T˙_ ([]) := ⊥ T˙α·α˙ ((et : ef )·S) := T˙α˙ (S) ⊗ (Tα (et ) ⊕ Tα (ef )) T˙α˙ ($x·S) := T˙α˙ (S) ⊗ x∗

The ability to define this function (relatively) easily is the main advantage of working with trace trees instead of co-call graphs at this stage. We project a TTree to a value of type (Var → Card), as required for a cardinality analysis, using c : TTree → (Var → Card) defined by   ⊥, if x does not occur in t c(t) x := 1, if on each path in t, x occurs at most once  ∞, otherwise.

T˙α˙ (#x·S) := T˙α˙ (S). With this we can define  ˙ ˙ (S)) , C(α,α, ¯ (Γ)) (Tα (e) ⊗ Tα ¯ α) ˙ ((Γ, e, S)) := c sthunks Γ (Tα and set out to prove Lemma 4 Given a tree cardinality analysis satisfying Definition 9, together with an arity analysis satisfying Definition 2, the derived cardinality analysis satisfies Definition 6. 2 P ROOF The proof follows by calculation involving c and the operations on trees.

Specification A tree cardinality analysis determines for every expression e and

42

As usual, we have inequalities for the syntactic constructs:

Condition (C-If2 ) is where the precision comes from, as we retain the knowledge that two code paths are mutually exclusive. In the proof for (C-Var2 ), we know that T0 (e) is repeatable, as isVal(e). This allows us to use that if a repeatable tree t is already contained in the second argument to s, then we can remove it from the range of the first argument: sT (t¯[x → 7 t]) (t ⊗ t0 ) = sT t¯ (t ⊗ t0 ) 5.3

Gα+1 (e) ∪ ({x} × fv(e x)) v Gα (e x) (G-App) Gα−1 (e) \ {x} v Gα (λx. e) (G-Lam) Gα (e[y := x]) \ {x, y} v Gα (e) \ {x, y} (G-subst) G0 (e) ∪ Gα (et ) ∪ Gα (ef )∪ (dom A0 (e) × (dom Aα (et ) ∪ dom Aα (ef )))



v Gα (e ? et : ef ) Gα (Γ, e) \ dom Γ v Gα (let Γ in e)

Co-Call Cardinality Analysis

The preceding section provides a framework for a cardinality analysis, but the infinite nature of the TTree data type prevents an implementation on that level. For a real implementation, we need a practically implementable data type that approximates the trees. The data type Graph used in the implementation is an undirected, non-transitive graph with loops on the set of variables. The intuition is that only the nodes of G (denoted by dom G) are called, and that an edge x—y ∈ G indicates that x and y can be called together, while the absence of an edge guarantees that calls to x resp. y are mutually exclusive. Loops thus indicate whether a variable is going to be called more y allows at most one call to y (possibly than once: The graph x y together with one call to x), while x allows any number of calls to y (but still at most one to x). We specify graphs via their edge sets, e.g.

(fv e)2 v G0 (e)

isVal(e) =⇒

(G-If) (G-Let) (G-value)

The following conditions concern Gα (Γ, e), which has to cater for the calls originating in e, Gα (e) v Gα (Γ, e),

(Gh-body)

the calls originating in the right-hand-sides, (x 7→ e0 ) ∈ Γ =⇒

GAα (Γ,e) x (e0 ) v Gα (Γ, e),

(Gh-heap)

and finally the extra edges between what is called from the righthand-side of a variable and whatever the variable is called with: (x 7→ e0 ) ∈ Γ, isVal(e0 ) =⇒ (fv e0 ) × Nx (Ga (γ, e)) v Gα (Γ, e).

(Gh-extra)

For thunks, we can be slightly more precise: Only one call to them matters, so we can ignore a possible edge x—x:

V × V 0 := {x—y | x ∈ V ∧ y ∈ V 0 ∨ y ∈ V ∧ x ∈ V 0 }

(x 7→ e0 ) ∈ Γ, ¬ isVal(e0 ) =⇒

for the Cartesian product of variable sets, and either specify their node set separately (e.g. dom(V × V 0 ) = dom V ∪ dom V 0 ) or leave it implicit. We write V 2 := V × V . The set of neighbors of a variable is Nx (G) := {y | x—y ∈ G}. The graph G \ V is G with nodes in V removed, while G V is G with only nodes in V retained. The graphs are ordered by inclusion, with ⊥ = {}. We can convert a graph to a TTree with t : Graph → TTree:

Finally, we need to ensure that the cardinality analysis is actually used by the arity analysis when dealing with thunks. For recursive bindings, we never eta-expand thunks:

paths(t(G)) := {x1 · · · xn | ∀i. xi ∈ dom G ∧ ∀j 6= i. xi —xj ∈ G}.

But for a non-recursive thunk, we only have to worry about thunks which are possibly called multiple times:

(fv e0 ) × (Nx (Ga (γ, e)) \ {x}) v Gα (Γ, e)

rec Γ, x ∈ thunks Γ, x ∈ dom Aα (Γ, e) =⇒ Aα (Γ, e) = 0

(Rec-∞-thunk)

x∈ / fv e0 , ¬ isVal(e0 ), x—x ∈ Gα (Γ, e) =⇒

We can also approximate a TTree by a Graph with the function g : TTree → Graph: [ g(t) := {g( ˙ x) ˙ | x˙ ∈ paths t}

Aα ([x 7→ e0 ], e) = 0

(Nonrec-∞-thunk) 2

Safety From a co-call analysis fulfilling Definition 10 we can derive a tree cardinality analysis fulfilling Definition 9, using

using g˙ : [Var] → Graph where dom g(x ˙ 1 · · · xn ) = {x1 , . . . , xn } and g(x ˙ 1 · · · xn ) := {xi —xj | i 6= j ≤ n}. The mappings t and g form a monotone Galois connection: g(t) v G ⇐⇒ t v t(G). It even is a Galois insertion, as g(t(G)) = G.

Tα (e) := t(Gα (e)). The definition of Tα (Γ, e) differs for nonrecursive and recursive bindings. For a non-recursive binding Γ = [x 7→ e0 ] we := have Tα (Γ, e) t(Gα (e)) dom Γ and for recursive Γ we define Tα (Γ, e) := t((dom Aα (Γ, e))2 ), i.e. the bound variables may call each other in any way.

Specification We proceed in the usual scheme, by giving a specification for a safe co-call cardinality analysis, connecting it to the tree cardinality analysis, and eventually proving that our implementation fulfills the specification. A co-call cardinality analysis determines for each expression e and incoming arity α its co-call graph Gα (e). As before, we also require a variant that analyses bindings, written Gα (Γ, e). The conditions in the following definition are obviously designed to connect to Definition 9.

Lemma 5 Given a co-call cardinality analysis satisfying Definition 10, together with an arity analysis satisfying Definition 2, the derived cardinality analysis satisfies Definition 9. 2 P ROOF Most conditions of Definition 9 follow by simple calculation from their counterpart in Definition 10 using the Galois connection

Definition 10 (Co-call cardinality analysis specification) We want the co-call graph analysis to agree with the arity analysis on what is called at all: dom Gα (e) = dom Aα (e)

(Gh-extra’)

t v t(G) ⇐⇒ g(t) v G and identities such as

(G-dom)

g(t ⊕ t0 ) = g(t) ∪ g(t0 )

43

and 0

0

The analysis result for bindings is again different for recursive and non-recursive bindings and uses the auxiliary function ( (fv e0 )2 if isVal(e0 ) ∧ x—x ∈ G 0 Gα;G ¯ (x 7→ e ) := 0 Gα¯ x (e ) otherwise,

0

g(t ⊗ t ) = g(t) ∪ g(t ) ∪ (dom t × dom t ). For (T-Let), we use (G-Let) with the following lemma about s, instantiated with T = thunks Γ, t¯ = TAα (Γ,e) (Γ), t = Tα (e) and S = dom Γ.  Lemma 6 Given • g(t) v G, • ∀x ∈ / S. t¯ x = ⊥, • ∀x ∈ S. g(t¯ x) v G, • ∀x ∈ S, x ∈ / T. dom(t¯ x) × Nx (G) v G and • ∀x ∈ S, x ∈ T. dom(t¯ x) × (Nx (G) \ {x}) v G we have g((sT t¯ t) \ S) v G. 5.4

which calculates the co-calls of an individual binding, adding the extra edges between multiple invocations of a bound variable, unless it is bound to a thunk and hence shared. • For recursive Γ we define Gα (Γ, e) as the least fixed point

fulfilling Gα (Γ, e) = Gα (e) t

2

G

GAα (Γ,e);Gα (Γ,e) (x 7→ e0 )

(x7→e0 )∈Γ

t

Call Arity, Concretely

G

(fv e0 × Nx (Gα (Γ, e))).

(x7→e0 )∈Γ

At last we can give the complete and concrete co-call analysis corresponding to GHC’s Call Arity, and establish its safety via our chain of refinements, simply by checking the conditions in Definition 10. The arity analysis is:

• For a non-recursive Γ = [x 7→ e0 ], we can distinguish between

thunks (¬ isVal(e0 )), in which case we have

Gα (Γ, e) = Gα (e) t GAα (Γ,e);Gα (e) (x 7→ e0 ) t fv e0 × (Nx (Gα (e)) \ {x})

Aα (x) := [x 7→ α] Aα (e x) := Aα+1 (e) t [x 7→ 0] Aα (λx. e) := Aα−1 (e) \ {x}

and functions (isVal(e0 )), in which case we have Gα (Γ, e) = Gα (e) t GAα (Γ,e);Gα (e) (x 7→ e0 )

Aα (e ? et : ef ) := A0 (e) t Aα (et ) t Aα (ef ) Aα (Cb ) := ⊥ for b ∈ {t, f}

t fv e0 × Nx (Gα (e)). Theorem 1 Call Arity is safe (in the sense of Definition 1).

The analysis of a let expression Aα (let Γ in e) as well as the analysis of a binding Aα (Γ, e) are defined differently for recursive and non-recursive bindings:

P ROOF By straightforward calculation (and simple induction for (G-subst)), we can show that the analyses fulfill Definition 2 and Definition 10. So by Lemma 5, Lemma 4, Lemma 3 and Corollary 1, the analyses are safe. 

• For a recursive Γ, we have

Aα (let Γ in e) := α ¯ \ dom Γ

6.

and Aα (Γ, e) := α ¯ dom Γ

The Formalization in Isabelle

On their own, the proofs presented in the previous sections are not very interesting, as they are neither very elegant nor very innovative. What sets them apart from similar work is that these proofs have been carried out in the interactive theorem prover Isabelle [22]. This provides a level of assurance that is hard to reach using pen-andpaper-proofs.

where α ¯ is the least fixed point defined by the equation α ¯ = Aα¯ (Γ) t Aα (e) t [x 7→ 0 | x ∈ thunks Γ]. • For a non-recursive binding Γ = [x 7→ e0 ] we have

Aα (let Γ in e) := (Aα0 (e0 ) t Aα (e)) \ dom Γ

6.1

and

The Formalization Effort

But it also greatly increases the effort involved in obtaining a result like Theorem 1. The Isabelle development corresponding to this paper, including the definition of the syntax and the semantics, contains roughly 12,000 lines of code with 1,200 lemmas (many small, some large) in 75 theories, created over the course of 9 months [4]. Large parts of it, however, can be re-used for other developments: The syntax and semantics, of course, but also the newly created data types like the trace trees and the co-call graphs. Much of the complexity is owed to the problem of bindings. Using Christian Urbans Isabelle implementation of Nominal logic ([27]) helped a lot here, but still incurs a technical overhead, as all involved definitions have to be proven equivariant, i.e. oblivious to variable renaming. While usually simple to prove, these lemmas still have to be stated. Another cause of overhead is ensuring that all analyses and the operators used by them are monotone and continuous, so that the fixed points are actually well-defined. Here, the HOLCF package by Brian Huffman [14] is used with good results, but again not without an extra cost compared to handwaving over such issues in pen-and-paper proofs.

Aα (Γ, e) := [x 7→ α0 ] where ( 0 if ¬ isVal(e0 ) and x—x ∈ Gα (e) α := Aα (e) x otherwise. 0

We have dom Gα (e) = dom Aα (e) and Gα (x) := {} Gα (e x) := Gα+1 (e) ∪ ({x} × fv(e x)) G0 (λx. e) := (fv e)2 \ {x} Gα+1 (λx. e) := Gα (e) \ {x} Gα (e ? et : ef ) := G0 (e) ∪ Gα (et ) ∪ Gα (ef ) ∪ (dom A0 (e) × (dom Aα (et ) ∪ dom Aα (ef ))) Gα (Cb ) := {} for b ∈ {t, f} Gα (let Γ in e) := Gα (Γ, e) \ dom Γ

44

So while the actual result shown here might not have warranted that effort on its own – after all, performance regressions due to bugs in the Call Arity analysis do not have very serious consequences – it lays ground towards formalizing more and more parts of the core data structures and algorithms in our compilers. The separation into individual theories (Isabelle’s equivalent to Haskell’s modules) as well as the use of locales ([2], Isabelle’s approximation to a module system) helps to gain insight into the structure of an otherwise very large proof, by ensuring a separation of concerns. For example, the proof of JT0 (e)K = JeK has only the conditions from Definition 2 available, which shows that the cardinality analysis is irrelevant for functional correctness. 6.2

In the code at hand, every call to f passes two arguments, i.e. case f x y of {. . . }. Therefore Call Arity determines f ’s external arity to be 2, and changes the definition to f x y = error “. . . ” y The strictness annotation on f , however, is still present, allowing the simplifier to change the code that contains the call to f to case f x of {}, as passing one argument is enough to cause the exception to be raised. It also removes all alternatives from the case, as the control flow will not return. On their own, each transformation is correct; together, havoc is created: Due to the eta-expansion, the evaluation of f x does not raise an exception. Because the case expression has no alternatives any more, the control flow in the final program continues at some other, undefined part of the program. One way to fix this would be to completely remove annotations that might no longer be true after eta-expanding a function definition, losing the benefit that these annotations provide. The actual fix was more careful and capped the reported arity at the number of arguments with which, according to the strictness signature, the function is definitely bottom.

The Formalization Gap

Every formalization – whether hand-written or machine-checked – has a formalization gap, i.e. a difference to the formalized artifact that is not (and often cannot) be formally bridged. Despite the effort that went into this formalization, the gap is not very narrow, and in at least one instance has been wide enough to fall into: • Clearly, we have not formalized the algorithm as implemented in

• There is no official semantics of GHC Core that is precise enough

GHC, but rather a mathematical description of it. Haskell code has no primitive function yielding a least fixed point, but has to find it using fixed-point iteration. Termination of the algorithm is not covered here.

to observe sharing. The closest thing is Richard Eisenberg’s work on formalizing Core [8], which includes a small step operational semantics for all of Core, but with call-by-name semantics. So the only “real” specification would be GHC’s implementation, including all later stages and the runtime system, which is not a usable definition.

• Our syntax is a much restricted variant of GHC’s intermediate

language Core. The latter is said to be simple, having just 15 constructors, but that is still a sizable chunk for a machinechecked formalization. Our meta-argument is that, for this particular theorem, our smaller syntax is representative.

• Finally, our formal notion of performance is an approximation

for real performance. Formally capturing the actual runtime of a program on modern hardware with multiple cores and complex caches is currently way out of reach.

• GHC’s Core is typed, while we work in an untyped setting. The

analysis, as implemented in GHC, ignores the types, so we argue that this is warranted. The general-purpose eta-expansion code used to implement the transformation will simply refrain from expanding a definition if its type does not obviously allow it, which can be the case with type functions. As the specifications used in the proofs require only lower bounds on the analysis results, the results still hold if one is more conservative than the analysis allows.

7.

Related Work

This work connects arity and cardinality analyses with operational safety properties, using an interactive theorem prover; as such this is a first. However, this is not the first compiler transformation proven correct in an interactive theorem prover. After all there is CompCert (e.g. [17]), a complete verified optimizing compiler for C implemented in Coq. Furthermore, a verified Java to Java bytecode compiler [18] was written using Isabelle’s code generation facilities, and the CakeML project has produced, among other things, a verified compiler from CakeML to CakeML bytecode, implemented in the HOL4 theorem prover [15]. Their theorems cover functional correctness of the compilers, though, but not performance. Using a resource aware program logic for a subset of Java bytecode, which they have implemented in Isabelle, Aspinall, Beringer and Momigliano validate local optimizations [1] to be indeed optimizations with regard to a variety of resource algebras. The Isabelle formalizations of the proofs seem to be lost. In the realm of functional programming languages, a number of formal treatments of compiler transformations exist, e.g. verification of the CPS transformation in Coq (e.g. [6], [7]), Twelf (e.g. [26]) or Isabelle (e.g. [20]). As their focus lies on finding proper techniques for handling naming, their semantics do not express heap usage and sharing. Sand’s improvement theory [23] provides an general, inequational algebra to describe the effect of program transformations on performance. Its notion of improvement is similar to our notion of safety, while the more general notion of weak improvement allows performance regressions up to a constant factor. This theory was adapted for lazy languages, both for improvement of time [21] and space [11, 12].

• In GHC, terms are part of modules and packages; this complexity

is completely ignored here. The real implementation will, for example, not collect arity and co-call information for external identifiers, as they cannot be used anyway. This implementation short-cut is ignored here. • Identifiers in GHC’s core are annotated with a wealth of addi-

tional information – inlining information, occurrence information, strictness signatures, demand information. As later phases rely on these information, they have to be considered part of the language, and should be included in a formal semantics. This actually caused a nasty bug1 that appeared in the third release candidate of GHC 7.10. The symptoms were weird: The program would skip over a call to error and simply carry on with the rest of the code. With Call Arity disabled, nothing bad happened. What went wrong? It boiled down to a function f :: a → b f x = error “. . . ” which the strictness analyzer annotates with b, indicating that once f is called with one argument, the result is definitely bottom. 1 https://ghc.haskell.org/trac/ghc/ticket/10176

45

Recently, Hackett and Hutten [13] took up on Sands’ work and built a general framework to prove worker/wrapper transformations time improving. And while neither that nor Sands’s work have yet been machine-checked, at least the semantic correctness of Hutton’s worker/wrapper framework has been verified using Isabelle [9]. Could we have built our results on theirs, especially as [13] uses almost the same abstract machine? Indeed, eta-expansion can be phrased as an instance of the worker/wrapper transformation, with abstraction and representation contexts Abs = [] and Rep = (λz1 . . . zn . ([] z1 . . . zn )). Unfortunately, the assumptions of the worker/wrapper improvement theorem are not satisfied, and this is to be expected: Sands’ notion of improvement – and hence Hackett and Hutton’s theorems – guarantee improvement in all contexts, while in our case the eta-expansion is justified by an analysis of the actual context, and is generally unsafe in other contexts. So in the current form, improvement theory is tailored to local transformations and, as Sands points out in [12], would require the introduction of context information to apply to whole-program transformations such as Call Arity. Such a grand unified improvement theory for call-by-need would be a tremendously useful thing to have. Related to the Call Arity analysis are the GHC’s “regular” arity analysis, which is described in working notes by Xu and Peyton Jones [28], and its cardinality analysis, most recently described in [24]. See [5] for a detailed discussion.

8.

[3] J. Breitner. The correctness of Launchbury’s natural semantics for lazy evaluation. Archive of Formal Proofs, Jan. 2013. http://afp.sf.net/ entries/Launchbury.shtml. [4] J. Breitner. The Safety of Call Arity. Archive of Formal Proofs, Feb. 2015. http://afp.sf.net/entries/Call Arity.shtml. [5] J. Breitner. Call Arity. In TFP’14, volume 8843 of LNCS, pages 34–50. Springer, 2015. [6] A. Chlipala. A verified compiler for an impure functional language. In POPL’10, pages 93–106. ACM, 2010. [7] Z. Dargaye and X. Leroy. Mechanized Verification of CPS Transformations. In LPAR’07, volume 4790 of LNCS, pages 211–225. Springer, 2007. [8] R. Eisenberg. System FC, as implemented in GHC, 2013. URL https://github.com/ghc/ghc/blob/master/docs/core-spec/ core-spec.pdf. [9] P. Gammie. The worker/wrapper transformation. Archive of Formal Proofs, Oct. 2009. http://afp.sf.net/entries/WorkerWrapper.shtml. [10] A. J. Gill. Cheap deforestation for non-strict functional languages. PhD thesis, University of Glasgow, 1996. [11] J. Gustavsson and D. Sands. A foundation for space-safe transformations of call-by-need programs. In HOOTS’99, volume 26 of ENTCS, pages 69–86, 1999. [12] J. Gustavsson and D. Sands. Possibilities and limitations of call-byneed space improvement. In ICFP’01, pages 265–276. ACM, 2001. [13] J. Hackett and G. Hutton. Worker/Wrapper/Makes It/Faster. In ICFP’14, pages 95–107. ACM, 2014.

Conclusion

First and foremost, we have proven that Call Arity is a safe transformation. That was initially not the case: Working towards a formally precise understanding of Call Arity uncovered a bug in the implementation, where thunks would erroneously be eta-expand when they are part of a linearly recursive binding.2 So the work was useful. But that alone does not warrant the effort put into this work – this bug would have been spotted by someone eventually, and indeed the formalization gap is still wide enough to hide bugs from our formal tools. What made this work worth it is the scarcity of formal treatments of the performance effects of compiler transformations, so it is an additional data point to the question “How practical is it, yet?”. Our answer here is, yes, it is possible, but still too tedious, and the formalization gap is a bit too wide. We have created reusable artifacts – syntax, semantics, data structures – that make similar endeavors, e.g. a safety proof of the cardinality analysis described in [24], more tractable. It would be very desirable to narrow the formalization gap and formalize GHC’s Core in Isabelle. Using Isabelle’s code generation to Haskell, even verified implementations of Core-to-Core transformations in GHC appear possible. This would be a milestone on the way to formally verified compilation of Real-World-Haskell.

[14] B. Huffman. HOLCF ’11: A Definitional Domain Theory for Verifying Functional Programs. PhD thesis, Portland State University, 2012. [15] R. Kumar, M. O. Myreen, M. Norrish, and S. Owens. Cakeml: A verified implementation of ml. In POPL’14, pages 179–191. ACM, 2014. [16] J. Launchbury. A natural semantics for lazy evaluation. In POPL, 1993. [17] X. Leroy. Mechanized semantics for compiler verification. In APLAS’12, volume 7705 of LNCS, pages 386–388. Springer, 2012. Invited talk. [18] A. Lochbihler. Verifying a compiler for java threads. In ESOP’10, volume 6012 of LNCS, pages 427–447. Springer, Mar. 2010. [19] S. Marlow and S. L. Peyton Jones. Making a fast curry: push/enter vs. eval/apply for higher-order languages. Journal of Functional Programming, 16(4-5):415–449, 2006. [20] Y. Minamide and K. Okuma. Verifying CPS Transformations in Isabelle/HOL. In MERLIN’03, pages 1–8. ACM, 2003. [21] A. K. Moran and D. Sands. Improvement in a Lazy Context: An Operational Theory for Call-By-Need. In POPL’99, pages 43–56. ACM, 1999. [22] T. Nipkow, L. C. Paulson, and M. Wenzel. Isabelle/HOL — A Proof Assistant for Higher-Order Logic, volume 2283 of LNCS. Springer, 2002. [23] D. Sands. Operational theories of improvement in functional languages (extended abstract). In Glasgow Workshop on Functional Programming, Workshops in Computing Series, pages 298–311. Springer, August 1991.

Acknowledgments This work was supported by the Deutsche Telekom Stiftung. I’d like to thank the KIT’s Programming Paradigms Group and Rebecca Schwerdt for helpful proofreading of the paper.

[24] I. Sergey, D. Vytiniotis, and S. Peyton Jones. Modular, Higher-order Cardinality Analysis in Theory and Practice. POPL, 2014. [25] P. Sestoft. Deriving a lazy abstract machine. Journal of Functional Programming, 7:231–264, 1997.

References [1] D. Aspinall, L. Beringer, and A. Momigliano. Optimisation validation. In COCV’06, volume 176(3) of ENTCS, pages 37 – 59, 2007. [2] C. Ballarin. Locales: A module system for mathematical theories. Journal of Automated Reasoning, 52(2):123–153, 2014. 2 see

[26] Y. H. Tian. Mechanically Verifying Correctness of CPS Compilation. In CATS’06, volume 51 of CRPIT, pages 41–51. ACS, 2006. [27] C. Urban and C. Kaliszyk. General Bindings and Alpha-Equivalence in Nominal Isabelle. Logical Methods in Computer Science, 8(2), 2012. . [28] D. N. Xu and S. Peyton Jones. Arity analysis, 2005. Working Notes.

GHC commit 306d255

46

Bridging the GUI Gap with Reactive Values and Relations Ivan Perez

Henrik Nilsson

School of Computer Science University of Nottingham United Kingdom {ixp,nhn}@cs.nottingham.ac.uk

Abstract

in its own right [39]. Current standard GUI toolkits associate computations to widgets (interactive visual elements) through call-back mechanisms which results in an event-oriented programming style that inverts control [12, pp. 36–37] and is hard to reason about [32]. Functional programming in itself offers potential advantages for programming GUI applications. These include abstraction facilities, which help managing some of the complexities, referential transparency, which facilitates reasoning and program transformations, and transparent parallelisation, which obviates concerns about deadlocks or rolling back unfinished transactions [18]. There are dozens of implementations of and proposals for functional GUI frameworks. Some are little more than low-level bindings to existing GUI toolkits. The result is visually appealing applications with good GUI performance. However, the price is an imperative programming style that suffers from the problems discussed above. Others seek to integrate with and capitalise from the functional setting by employing functional structuring principles. This can facilitate reasoning and, in a statically typed setting, provide good static correctness guarantees. However, there are generally caveats such as practically prohibitive maintenance costs, failure to conform to platform-specific GUI standards, and issues with modularity, scalability, and efficiency. We elaborate in section 2. The central idea of this paper is to provide light-weight abstractions that on the one hand enable easy, uniform access to arbitrary existing GUI toolkits and other external resources, and on the other seamlessly blend with the functional programming paradigm, scale well, and support modularity. This is what we refer to as “bridging the GUI gap”. The specific contributions of this paper are:

There are at present two ways to write GUIs for functional code. One is to use standard GUI toolkits, with all the benefits they bring in terms of feature completeness, choice of platform, conformance to platform-specific look-and-feel, long-term viability, etc. However, such GUI APIs mandate an imperative programming style for the GUI and related parts of the application. Alternatively, we can use a functional GUI toolkit. The GUI can then be written in a functional style, but at the cost of foregoing many advantages of standard toolkits that often will be of critical importance. This paper introduces a light-weight framework structured around the notions of reactive values and reactive relations. It allows standard toolkits to be used from functional code written in a functional style. We thus bridge the gap between the two worlds, bringing the advantages of both to the developer. Our framework is available on Hackage and has been been validated through the development of non-trivial applications in a commercial context, and with different standard GUI toolkits. Categories and Subject Descriptors D.3.2 [Programming Languages]: Language Classifications – Functional Languages; H.5.2 [Information Interfaces and Presentation]: User Interfaces – Graphical user interfaces (GUIs) Keywords GUI, pure functional programming, reactive values

1.

Introduction

Modern interactive applications are often large and complex with many interrelated elements and sophisticated Graphical User Interfaces (GUIs). The complexity stems from many sources, including the nature of specific application domains, existing software infrastructure, and orthogonal usability features like undo-redo and support for cancellation of long-running operations (requiring concurrency) [33]. Consequently, structuring such programs well is difficult, as is reasoning about them [17, 20, 29]. Yet the prevalence of this class of programs, which we refer to as GUI applications, makes it important to look for ways to ameliorate these difficulties. Let us consider some of the obstacles in more detail. GUI applications are inherently stateful and effectful, raising difficulties

• Reactive Values (RVs): an abstraction that can represent widget

properties, model fields, files on disk, network sockets and other changing entities, and further be combined and adapted using a series of combinators as well as general lenses [13]. • Reactive Relations (RRs): declarative, uni- or bi-directional

rules used to relate existing RVs, and which can be grouped and factored out into reusable libraries of choreographies • A framework implementing these ideas, available off Hackage,

and validated on a series of non-trivial examples, including some commercial examples developed by Keera Studios1 , and with different GUI backends. The rest of this paper is structured as follows. We first present, in more detail, the problems we seek to address. We then introduce reactive values and relations, illustrating with concrete examples and explaining how our proposal addresses the identified problems. We then explain how we have used our approach in real-world applications and the impact this had on the architecture of these applications. Finally, we review related work and draw conclusions.

Permission to make digital or hard copies of all or part of this work for personal or classroom use is granted without fee provided that copies are not made or distributed for profit or commercial advantage and that copies bear this notice and the full citation on the first page. Copyrights for components of this work owned by others than ACM must be honored. Abstracting with credit is permitted. To copy otherwise, or republish, to post on servers or to redistribute to lists, requires prior specific permission and/or a fee. Request permissions from [email protected].

Haskell’15, September 3-4, 2015, Vancouver, BC, Canada c 2015 ACM. 978-1-4503-3808-0/15/09...$15.00

http://dx.doi.org/10.1145/2804302.2804316

1 Startup

47

founded by the first author. See http://keera.co.uk.

2.

Background

Code that uses these GUI libraries “feels” imperative [37, p. 522–527]. Furthermore, event-driven architectures result in inversion of control [12, p. 36–37], here exemplified by the event handler on line 10, making reasoning about programs hard [32]. Further, commonly used design patterns for imperative interactive applications, such as the family of Model-View-Controller [26] patterns, move too much logic into the controller, causing quadratic growth of the size of its codebase with new features [39, p. 8] and leading to what informally is known as “callback hells” [9, p. 2]. Reasoning about the behaviour of GUI toolkits is also hard. Widget properties are not plain mutable variables [39, p. 13]. The semantics of GUI toolkits is often poorly defined. Furthermore, many of these libraries (including Gtk and wxWidgets) are not thread-safe: GUI functions must be called from the main (UI) thread, controlled by the toolkit itself7 . Applications that need to control the execution loop or do background work must thus handle concurrency explicitly, making the code even more complex. On the bright side, the resulting functional code is not substantially worse than its C/C++ equivalent, and sometimes can be better [19]. Moreover, the performance can be comparable to that obtained using imperative languages [27], making low-level bindings a customary choice for CPU-demanding multimedia.

Current solutions for functional GUI programming address two concerns: 1) description of the GUI itself, and 2) definition of patterns for interactive and stateful programs that conform to functional principles. This section gives a brief overview of the field, mainly from a Haskell perspective, and identifies three problems of current solutions: • GUIs can be written using low-level imperative APIs, but the

code exhibits the usual problems associated with imperative programming (section 2.1). • Purely Functional GUIs are a much better fit for functional pro-

gramming, but have high maintenance costs, especially if one seeks a “natural” appearance and behaviour across platforms (section 2.2). • Existing GUI toolkits and programming paradigms impose ar-

chitectural constraints that limit the scalability of applications (section 2.3). In section 3 we introduce Reactive Values and Relations to address these problems. 2.1

Imperative GUIs in Functional Languages

2.2

I/O in functional languages is often performed using libraries written in C/C++ and imported as effectful computations via a Foreign Function Interface. Many such libraries work at a very low-level, such as OpenGL2 . Functional bindings usually do minimal work, resulting in APIs that resemble the underlying C interface. In functional languages, effectful computations such as monads and applicative functors are first-class entities [28, 40]. Together with their associated laws, this enables some forms of equational reasoning [15, 22]. However, without higher-level abstractions, large programs that do input/output still tend to look imperative [39, p. 11–12]. The strict execution order of the effectful computations imposes sequential thinking and a need to mentally track the implicit program state. Reasoning thus remains hard [4]. To abstract over visual elements and provide standardised appearance and behaviour, developers use libraries that implement interactive elements or widgets. Examples include Gtk+3 , wxWidgets4 and Qt5 . Widgets have associated properties and events. Developers can modify the properties, and install event handlers to execute computations when, for instance, a button is depressed. As an example, the following code6 , builds an application with a button that prints a text when the button is clicked: 1

5

10

Functional GUI Toolkits

Functional Programming can address the concerns discussed above by defining a pure API modelling the domain of interest. Such an API does not need to resemble the underlying bindings: an evaluation function can traverse the pure data structures and do the impure work, projecting the changes onto the screen. Objects I/O [1], implemented in Clean, is an example where GUIs are pure values and event handlers apply transformations to the world state. Since interactive widgets must handle user actions, produce a visualisation and notify of changes, some purely-functional solutions adopt a function-like view of widgets themselves. Fudgets [5] is an asynchronous programming library built on top of stream processors. In the context of GUIs, a fudget is a widget with one input and one output. A number of combinators connects fudgets and determines their visual layout. However, and just for that reason, there is no way to connect visually non-adjacent widgets. Gadgets [35], a similar approach, tries to work around Fudgets’ limitation of one input and output channel per process. In Gadgets and Fudgets, code is more declarative than in imperative toolkits, but both are limited in terms of feature coverage. To cover the whole GUI domain, one would need to define a type, an implementation, and a set of operations for each kind of widget supported. As GUI toolkits are notoriously large8 , this results in very large codebases with high maintenance costs9 , rendering some projects unrealistic in the long term. Furthermore, platforms differ slightly, and creating a GUI abstraction that provides all the features of each platform under a common, clean interface has proved challenging. The opposite, maintaining several (similar) sets of code, only exacerbates the maintenance costs. A different school of thought seeks to generate GUIs automatically based on the types of expressions. GuiTV [11], a Haskell implementation for type-based GUI generation, can construct widget compositions for functions, to provide the arguments and show

import Graphics . UI . Gtk main :: IO () main = do initGUI window == < buttonF " Up " 10

count n Click = ( n +1 ,[ n +1])

Figure 1. A sample Fudgets program. intDispF is an integer text fudget that uses a text box for interaction, mapstateF keeps a counter, and buttonF is a button fudget. >==< chains fudgets from right to left, placing them next to one another in the GUI.

the result, eliminating one level of indirection between models and visualisations (see Fig. 2). 1

reverseT :: CTV ( String -> String ) reverseT = tv ( oTitle " reverse " defaultOut ) reverse

Figure 2. A Tangible Value and the GUI generated based on its type, with text boxes being used to interact with Strings. Figure 3. A screenshot of Xournal, showing different ways to change the page number.

A similar idea is used in iTask [31], a client-server, taskoriented, state-transforming web programming framework for Clean. iTasks seeks to address large-scale architectural GUI concerns, generating user interfaces automatically from types [2] and then rendering these to a browser. Mapping a type to exactly one kind of widget is arguably a bit inflexible: there may be more than one right way to interact with values of a specific type. To circumvent this, type wrappers (e.g. Haskell’s newtype) can be used, but only at the expense of additional code to handle artificial type distinctions. 2.3

The following pseudo-FRP code illustrates these mutual dependencies11 : 1

t o o l b a r B u t t o n R i g h t IO ()

Reactive Values and Relations

Our proposal for addressing the issues discussed in section 2 is based on a concept we call Reactive Values (RVs). A Reactive Value is a typed mutable value with access rights and subscribable change notification. RVs provide a uniform interface to GUI widgets, other external components such as files and network devices, and application models (the conceptual problem representation in MVC [26]). Each entity is represented as a collection of RVs, each of which encloses an individual property. RVs can be transformed and combined using a range of combinators including (n-ary) function lifting and lens application [13]. To specify how the values of RVs are related, allowing them to be synchronised during execution in response to changes, we introduce Reactive Relations (RRs). A Reactive Relation is either uni- or bi-directional. RRs can be thought of as connecting RVs, such that a change to a value will produce an update of other(s). RRs are defined separately from RVs. Indeed, relations involving the same RVs can even be defined in separate modules. This is in contrast to FRP signals, which are defined by their dependencies on other signals. Allowing RRs to be defined separately is a key strength of our approach as this promotes separation of concerns and modularity (Sec. 2.3). The work presented here addresses static RRs and provides no way of removing installed relations. So far we have not found this to be a major limitation. MVC controllers [26] can thus be seen as sets of Reactive Relations. Because the model is reactive and notifies of changes to it, the controller no longer needs to know how changes propagate within the model. This allows us to move more of the problem’s logic into the model (whenever it conceptually belongs there), while minimising data propagation from the model to the view. Our API allows RVs to be created from pure models, widget properties and external elements. As we cannot cover every possible use case, we provide a low-level API that can be used to support other widgets and external entities, implement further synchronisation abstractions and introduce other RV combinators. 3.1

class (Readable r a, Writable r a) => ReadableWritable r a ReadOnly is an instance of Readable, WriteOnly is an instance of Writable, and ReadWrite instantiates all three. Example (Reactive Values) We borrow the following examples from a posture monitoring application (Sec.4.1.2) that uses a webcam to monitor the user’s sitting posture and trigger a warning when it differs too much from a reference posture. Users can customise how much time needs to pass until a warning is triggered. In the GUI (Fig. 4) this is configured using a spin button (text entry for numbers).

Figure 4. Entry used to configure the warning delay. Following the MVC architectural pattern we would expect to have definitions like the following13 : -- UI (Gtk) module delaySpinButton :: SpinButton delaySpinButton = ... -- Model module data Model = Model { notificationDelay :: Int ... } In our approach, a reactive application would typically also include the following two definitions:

Reactive Values

A Reactive Value (RV) is characterised by a type and an access property:

delayEntry :: ReadWrite Int -- UI: Entry value delayEntry = ... -- (to be implemented)

• The type is the type of the element that it stores or represents.

notificationDelayField :: ReadWrite Int -- Model notificationDelayField = ... -- (to be implemented)

In our implementation, this can be any Haskell data type. • The access property states whether the reactive value is read-

The first of the two reactive values represents the numeric value held in delaySpinButton; the second one represents the field notificationDelay of the model.

only, write-only or read-write. We use the following types to represent reactive values, parameterised over the type of values they contain:

13 We

omit the implementation because the details of how the spin button and the model are created are irrelevant to the discussion at this point.

12 https://wiki.haskell.org/Reactive-banana

50

Gtk+ Read-only attribute (+ associated event) Write-only attribute Read-write attribute (+ associated event) Signal or event

In Section 3.4 we will connect Reactive Values to keep them in sync during program execution. The two reactive values above are both read/write and have the same type, which will make connecting them bi-directionally straightforward. But it may not always be so easy. For example, in our application, a text entry gives users the possibility of customising the sound played when the posture is incorrect. In our program we need to connect the following two RVs: soundEntryText :: ReadWrite String soundEntryText = ...

-- UI

ReadOnly Reactive value

which, for a given Gtk+ text entry, returns an RV representing the text in the entry. The text can be accessed and modified via the Reactive Value, which fires notifications when it changes. We may also be interested in detecting events that do not correspond to any property or carry data (for instance, a button being clicked). Events can always be seen as read-only RVs carrying no information, eg.:

which have different types. In sections 3.3 and 3.4 we will see how to adapt the types when they do not match and how to connect different kinds of RVs so that they stay in sync during execution. Creating Reactive Values

buttonActivateField :: Button -> ReadOnly ()

Reactive Values are created from and linked to an underlying entity. These “backing” entities can be external (widgets, files, etc.) or internal (pure values and application models). In this section we limit our discussion of GUIs to Gtk+, but our approach can be used with other toolkits such as wxWidgets or Qt. Our implementation [24] includes examples of reactive applications written with different toolkits. 3.2.1

WriteOnly Reactive value ReadWrite Reactive value

Figure 6. Correspondence between Gtk+ and our Reactive Values

soundField :: ReadWrite (Maybe FilePath) -- Model soundField = ... -- Nothing: Default sound -- Just fp: Sound in file fp

3.2

Reactive equivalent ReadOnly Reactive value

which, for a given button, defines an RV that fires a notification every time the button is clicked. Files Reactive Values can be used to interact with files and other sources/sinks of information. The predefined function fileReactive creates an RV enclosing a file: fileReactive :: FilePath -> ReadWrite String

Externally-backed Reactive Values

Some reactive values represent mutable entities that exist outside our functional code. These could be, for instance, GUI widgets and their properties, files, network connections or hardware devices.

We use a file monitoring library14 that relies on OS facilities to monitor files for changes without polling. This results in an RV that will notify dependent RVs when the file changes on disk.

Graphical User Interfaces In Gtk+ terminology, widgets (graphical components) have attributes (properties) with access rights (read/write). Widgets may trigger signals to execute event handlers when attributes change or when other events take place (clicks, key presses, etc.). Checkboxes, for instance, have attributes such as the state (checked/unchecked) and whether users can interact with them (enabled/disabled). Clicking on an enabled checkbox toggles its state and fires an event that can be handled programmatically (Fig. 5).

Example (Files) The following RV is connected to the file myFile.txt. When the RV is written to, so is the file. When the file changes, RV subscribers are notified: myFile :: ReadWrite String myFile = fileReactive "myFile.txt" Network Similarly, an experimental network reactive layer allows sockets to be seen as RVs. For instance, the function: udpSink :: HostName -> String -> IO (WriteOnly String) creates a writable reactive value that sends any text written to it to the specified host and port using User Datagram Protocol (UDP). 3.2.2

Figure 5. Checkboxes can be checked/unchecked (left/right columns) and enabled/disabled (top/bottom rows).

Internally-backed Reactive Values

Library users have access to the value constructors of different RVs, and can thus define RVs that enclose pure values. In most applications we want to be able to detect when values change, update other RVs accordingly and guarantee thread-safe access to the Reactive Value. We provide a library with a default implementation of “very light” RVs that fulfils all of these requirements. The library offers several RV constructors, of which the default one compares the value of an RV with the previous one before setting it, to break unnecessary propagation loops. This solution works well for simple programs, but it is suboptimal for very large applications: a change to only one part of a value (for instance, the first component of a tuple) will provoke forward propagations to RVs that depend only on other parts that did not change (for instance, the second component of the same tuple).

There is a one-way correspondence between Gtk+’s signals and attributes and our reactive values. In most cases, an attribute defines a reactive value, possibly accompanied by the signal (event) triggered when the attribute changes (Fig. 6). Our API covers most of the essential widget properties in gtk2hs [21]. We also provide a generic signal/attribute-based interface, suitable for widget properties not specifically supported. Additionally, we have published (experimental) reactive layers for wxWidgets and Qt [24]. Example (GUIs) To access a text entry’s text, we provide: entryTextReactive :: (EditableClass e, EntryClass e) => e -> ReadWrite String

14 https://hackage.haskell.org/package/fsnotify

51

Protected Models To address the aforementioned scalability concerns we define an abstraction that encloses an application’s model, called Protected Model, implemented as a polymorphic, thread-safe mutable variable with change detection and an event dispatching thread, parameterised over two types15 . The first type argument of ProtectedModel represents the type of values stored in it, that is, the pure model it encloses. The second argument acts as an identifiable reference to a part of the model and is used to identify what has changed16 :

Of course, a lower level API to Protected Models and Reactive Values is also available, which can be used in case the given Template Haskell is not adequate for the user’s needs. Protected Models can incorporate more machinery than simply change detection and event dispatching. For instance, in the SoOSim UI17 and Gale IDE (Sec. 4.1.1), the Protected Model incorporates a change-aware undo/redo queue. The model is extended with three operations to control the queue, which can be used by the controller. The RVs generated using protectedField are the same. Protected Models allow us to hide other design decisions, such as having a global event dispatcher vs executing events in new threads as they come in. We believe that this ability to introduce orthogonal features without affecting the rest of the codebase is another key strength of our framework.

data (Ord e, Eq e) => ProtectedModel a e = ProtectedModel { reactiveModel :: TVar (ReactiveModel a e) ... } data (Ord e, Eq e) => ReactiveModel { basicModel , eventHandlers , pendingHandlers ... }

ReactiveModel a e =

3.3

:: a :: M.Map e [IO ()] :: [IO ()]

Reactive values can only be connected if they have compatible types. We can transform and combine reactive values by lifting (nary) functions, by applying lenses, and by letting one control the other (governance).

Transforming and combining RVs

Unary lifting A function of type a → b can be applied to a reactive value in one of two ways:

We make use of STM TVars [18] to guarantee exclusive access and atomic modifications. Protected Models can be created with the function startProtectedModel. This function also starts a dispatcher thread that executes pending handlers:

• To write values of type a into an writeable RV of type b (con-

verting the a into a b before writing). • To read values of type b from a readable RV of type a (convert-

ing the values after reading).

startProtectedModel :: (Ord e, Eq e) => a -> IO (ProtectedModel a e)

This implies that: 1. Lifting one function onto a read-write reactive value will render a read-only or write-only reactive value.

In the following we will see how to define RVs that give access to only parts of a Protected Model.

2. To produce a read-write reactive value, we need to lift two functions, one for each direction of the transformation (Fig. 7).

Projecting Protected Models to Reactive Values The main difference between a plain RV and a Protected Model is that the latter is intended to be a collection of RVs. Thus, one should define RVs that project specific parts of the Protected Model. To make that extra layer as simple as possible for the users of our library, we provide a high-level API that uses Template Haskell to define RVs that represent projections of fields of the model. For instance, given:

We thus define three unary lifting combinators: liftR :: => liftW :: => liftB :: ->

data Model = Model { language :: Maybe Language ... }

Readable r (a -> b) -> r a -> ReadOnly b Writable r (b -> a) -> r a -> WriteOnly b (a -> b, b -> a) ReadWrite a -> ReadWrite b

Read-only RVs are covariant in the read end, Write-only RVs are contravariant in the write end.

data ModelChange = LanguageChanged | ... deriving (Eq, Ord) the following call to Template Haskell in a module (all the referred types, and additional RV libraries, must be in scope): protectedField "Language" [t|Maybe Language|] ’’Model ’’ModelChange generates a definition with signature:

Figure 7. rv2 = liftB (f, g) rv1

languageField :: ProtectedModel Model ModelChange -> ReadWrite (Maybe Language) Example (lifting) Continuing with our previous example, we might want to render the language selection in a label, for which we need to transform the Maybe Language from our model into a String. We might do so as follows:

15 Our

signature uses the type class Event for identifiable changes, which is an instance of Ord and Eq. Events have additional, orthogonal uses in our framework. To facilitate understanding, we present a simpler version here. 16 In some of our programs, we overload the type e with semantic information about the nature of the change itself. See [39, p. 33] for more details.

17 https://github.com/ivanperez-keera/SoOSiM-ui

52

showLangSelection showLangSelection showLangSelection showLangSelection

:: Maybe Language -> String (Just English) = "EN" (Just Spanish) = "ES" Nothing = "--"

liftW2 :: (Writable r1 b, Writable r2 c) => (a -> (b,c)) -> r1 -> r2 -> WriteOnly a N-ary lifting onto read-only values can also be achieved using applicative syntax [28].

langText :: ReadWrite (Maybe Language) -> ReadOnly String langText lang = liftR showLangSelection lang

Example (n-ary lifting) We could, for instance, render several configuration parameter in a tuple, to later show them in a label, as follows:

If we are given a function to parse the language selection, then we can easily make the reactive value writable as well: readLangSelection readLangSelection readLangSelection readLangSelection

notificationDelay :: ReadWrite Int notificationDelay = ...

:: String -> Maybe Language "EN" = Just English "ES" = Just Spanish _ = Nothing

correctionFactor :: ReadWrite Int correctionFactor = ... configurationPair :: ReadOnly String configurationPair = liftR2 (\d f -> show (d,f)) notificationDelay correctionFactor

langTextRW :: ReadWrite (Maybe Language) -> ReadWrite String langTextRW lang = liftB (showLangSelection, readLangSelection) lang Read-only Reactive Values are Functors and Applicatives. Write-only Reactive Values are Contravariant functors. Using the Applicative and Contravariant infix lifting operators, we can write clearer, less verbose code.

Lenses Lenses [13] provide a way to focus on subparts of data structures by means of a getter and a setter that read from and inject values into an existing structure. Lenses are compositional (they can be combined using a notation similar to function composition), and can be derived automatically for some type definitions. Lens application onto RVs is a specific form of lifting bijections. We provide a specific lens lifting combinator:

langText :: ReadOnly (Maybe Language) -> ReadOnly String langText lang = showLangSelection lang

() :: Lens’ a b -> ReadWrite a -> ReadWrite b

Read-write RVs are instances of our own GFunctor (an abstraction for types that are covariant in one direction and contravariant in the other), for which we have defined the operator , analogous to for applicatives. For example, the following is a more succinct definition of langTextRW:

Example (Lenses) Given the lens ( 1) :: Lens’ (a,b) a, which focuses on the first component of a pair, one can write: window1Top window1Top window1Position window1Position

langTextRW :: ReadWrite (Maybe Language) -> ReadWrite String langTextRW lang = (showLangSelection, readLangSelection) lang

:: ReadWrite Int = _1 window1Position :: ReadWrite (Int, Int) = ...

Governance Another possible way of combining RVs is by letting one control another. Consider, for instance, the case in which one wants changes to a text box to be “reported” only when the button is depressed. If we use liftR2 to combine them, both clicks on the button and text entry changes will trigger notifications. To address these situations, we provide the function:

When lifting functions onto read-write reactive values, it is often desirable that the transformation be an isomorphism (in which case we would lift the function by the functor and the inverse by the contrafunctor). Given the limitations of Haskell, we cannot but trust users in this respect, providing only a small facility for involutions: reversedText :: ReadWrite String reversedText = (involution reverse) textValue where textValue :: ReadWrite String textValue = ...

governing :: Readable r a => r -> ReadWrite b -> ReadWrite b which defines a new Reactive Value that encloses the value in the second argument, and notifies of changes only when the first RV changes. An analogous function is provided for read-only RVs.

Not using real isomorphisms may impact performance. Our default setters compare the new values to the old ones (if they are instances of Eq). This stops unnecessary data propagation and breaks loops. However, if the inverse provided is not the true inverse, the value that propagates in the inverse direction after a change may cause a new propagation. It is therefore necessary to provide inverses that will lead to a fixed point. This will be discussed further in section 6.

Examples (governance) Following the case described above, we often want the text of an entry not to be synchronised or passed around, except when a button is clicked. We can use governing to create a RV that encloses the entry’s text, but whose changes are only propagated when the user clicks the button: buttonAndEntry :: ReadWrite String buttonAndEntry = button1Clicks ‘governing‘ textEntry1Text button1Clicks :: ReadOnly () button1Clicks = ... textEntry1Text :: ReadWrite String textEntry1Text = ...

N-ary lifting Similarly, we can lift n-ary functions into RVs using analogous functions (eg. liftR2, liftW2, liftB2, etc). The signatures of liftR2 and liftW2, for instance, are: liftR2 :: (Readable r1 a, Readable r2 b) => (a -> b -> c) -> r1 -> r2 -> ReadOnly c

53

3.4

Reactive Relations

in separate libraries that can be reused within the same application and across multiple applications. We refer to these abstract patterns as choreographies. We present a very simple one that we have found both illustrative and useful: showing the file name in the title bar of the main window. Type classes capture the requirements of model and view.

So far we have given ways to create reactive values, but we have not given any way to relate readable and writable RVs to allow changes to be propagated correctly to achieve overall consistency (for instance, to synchronise two text boxes, or an RV that represents a Protected Model field with one that encloses a widget attribute). We introduce rule-building functions to capture the idea that two reactive values must be “in sync” for all time. The functions (depending on the direction of change propagation) build directional synchronisation relations. The source value (the origin of the change) must be readable, the destination must be writable, and they must contain values of the same type. To simplify code further we provide the function =:=, syntactic sugar for two directional relations. Their types are as follows:

class ModelWithFilename m e where filenameField :: ProtectedModel m e -> ReadWrite FilePath class ViewWithMainWindow v where mainWindow :: v -> Window composeTitleBar :: (ModelWithFilename model event, ViewWithMainWindow view) => String -> model -> view -> IO () composeTitleBar programName model view = (composeTitle ‘liftR‘ filenameField model) =:> (windowTitleReactive (mainWindow view)) where composeTitle fp = fp ++ " - " ++ programName

( r1 -> r2 -> IO () (=:>) :: (Readable r1 a, Writable r2 a) => r1 -> r2 -> IO () (=:=) :: (ReadableWritable r1 a, ReadableWritable r2 a) => r1 -> r2 -> IO () Example (reactive relations) In our posture monitoring application we need a GUI to manipulate the delay until a notification is presented. We have already seen how to define each reactive value (for the GUI and the model). To keep them in sync we write:

Choreographies are usually more complex and not limited to one relation. They can contain internal models and views, and spawn threads. For example, the choreography that offers to save files when a program is closed contains two rules (one to present the confirmation dialog, one to save the file), introduces one additional model type class and contains a view of its own (the dialog).

delayEntry =:= notificationDelayField

4.

This combinator installs the necessary change listeners on each RV, so that the other RV is updated when either changes. For another example, based on configurationLabel defined earlier, we can show the correction factor and the warning delay in a label:

An implementation of Reactive Values is available online as a collection of libraries, as part of the Haskell GUI Programming toolkit Keera Hails [24]. They provide definitions of Reactive Values and Reactive Rules, and bindings for a series of backends, including Protected Models, Gtk+ widgets and properties, files, network sockets, FRP signal functions (using Yampa [34]) and Nintendo Wii Controllers. It also includes libraries to simplify common architectural patterns (MVC) as well as choreographies often needed in different kinds of applications. At the time of this writing, the libraries comprise over 7K lines of code. We have used our approach to develop several real-world applications, currently amounting to slightly over 25K lines of Haskell (not counting comments, empty lines or code generated automatically by our library, using Template Haskell or the Keera Hails project generator, which generates an initial project skeleton). Examples of the software created include an interactive tool to visualise Supercomputer Operating System node simulations (Fig. 8) [3], a webcam-based posture monitor (Sect.4.1.2), a OCRbased PDF renamer and a Graphic Adventure IDE (Sect. 4.1.1). We have also published several demonstration applications and small examples, such as an SMS sender18 and a replacement for WMGUI19 , the Nintendo Wii Controller debugging GUI available on most Linux distributions (Fig. 9).

configurationPair confLabel confLabelString confLabelString

:: ReadOnly String -- Model :: GtkLabel -- UI :: ReadWrite String -- UI = labelString confLabel

rule = confLabelString Maybe FilePath stringToMaybe "" = Nothing -- Default sound stringToMaybe fp = Just fp -- Sound in file fp

4.1 4.1.1

Evaluation Gale IDE

Keera Gale is a graphic adventure development IDE written entirely in Haskell20 . The IDE uses Gtk+ for the user interface, and al-

rule = soundEntryText =:= (fromMaybe "", stringToMaybe) soundField 3.5

Experience

18 https://github.com/ivanperez-keera/

Choreographies

keera-diamondcard-sms-trayicon 19 https://github.com/keera-studios/ hails-reactive-wiimote-demo 20 http://keera.co.uk/blog/products/gale-studio/

GUI programs often contain common, re-occurring patterns. By parameterising Reactive Relations over Protected Models and Views containing certain Reactive Values, we can describe sets of rules

54

Figure 8. SoOSim UI, an interactive visualisation tool for Supercomputer Operating System node simulations.

Figure 10. GALE IDE’s object preview screen, showing an animation with each character’s default state. Double clicking on any object opens the object details screen, where users can modify the object properties by selecting states, animations, actions applicable to them and reactions to those actions.

Figure 9. A demo that shows the state of a Nintendo “Wiimote”.

lows users to create graphic adventure games without prior knowledge of programming. Users can define screens, characters, actions and reactions, enable conversations and customise the game interface. Other stages of the game design process, such as storyboarding and state transition diagrams, are also supported. The IDE is accompanied by a graphic adventure engine written in Haskell using SDL2, that has been tested on Windows, Linux and Android. The final distributable file can be generated directly from the IDE using only the GUI. Games created using Gale IDE are currently being beta-tested on Google Play. The program uses MVC as its main architecture. The IDE features, at the current time, 385 modules of Haskell code, without including the engine or other auxiliary components. 228 of those modules conform the application’s controller and contain 50 per cent of the code. Template Haskell is used to generate the View (from glade files) and the reactive layers of the model, decreasing the number of lines of code further. A separate thread is used to handle a responsive progress dialog when the distributable files for the game are being generated. The controller starts that thread, but further communication occurs only indirectly through the protected model (Fig. 12). The controller currently contains 75 reactive rules. We have ported imperative MVC Haskell code to this new reactive interface, and using Reactive Values and Rules makes the controller’s modules between 50 and 66 per cent smaller (in lines of code, without comments or empty lines) compared to code that had already been optimised to avoid code duplication due to bi-directional synchronisation21 .

Figure 11. GALE IDE can target Windows, Linux, Android and Web. This screenshot of the running application shows three nested windows: the main application, the target/distributable selection window, and the target directory selection dialog.

Compared to the whole application’s codebase, we estimate this approach to have saved us between 25 and 35 per cent of code. Combined with being able to generate UIs and Reactive Fields using Template Haskell gives us a combined estimate of 35 to 45 percent of lines of code saved. The controller makes heavy use of choreographies to eliminate boilerplate code. Re-occurring patterns include synchronising the selection on a tree view and on a tab page group (using the tree view to change the tab page), and efficient interaction with dynamic lists of elements (scenes, objects, etc.). Furthermore, because Reactive Values encapsulate both bidirectional access and the relevant notification subscription mechanisms in one unique entity, we have observed that we are less likely to make errors such as installing handlers on the wrong events. We believe that Keera Gale IDE clearly shows that our approach addresses all three problems introduced in section 2: it uses a

21 In

bi-directional synchronisation one needs to obtain the values on both sides, compare them and possibly update one side. Our original code already received the direction of the update as a parameter, so that the code that polled the view and the model could be shared for both directions.

55

4.2

Using our solution for GUI programming we have observed benefits in terms of separation of concerns, modularity, reduction of code size, and dealing with concurrency. Our MVC controllers [25] no longer know about the internals of models, nor how change propagates within them. Most viewmodel synchronisation is now done using separate, abstract, easily readable rules. Also, as callbacks are no longer explicit and many relations are bi-directional, code duplication in the controllers has been eliminated, reducing their size to less than half. The ability to state synchronisation constraints separately from reactive values through reactive relations allows the constraints to be grouped by the feature they implement rather than by the UI or model elements involved. This promotes separation of concerns and allows orthogonal features, like saving or printing, to be added or removed locally. Individual constraints can easily be disabled, which is a great debugging aid. They can also be factored out in choreographies (Sec. 3.5) that can be reused across applications. Finally, thanks to thread-safe models, our applications accommodate concurrent threads easily. This was exploited in the PDF Renamer and the Game IDE to asynchronously transfer files while showing a responsive, cancellable progress dialog. Similarly, in the posture monitor, one thread records images from the webcam while another shows warnings as popup messages when the posture is incorrect. The threads do not communicate explicitly with each other, but rather modify the application’s model. Any configuration change through the GUI’s preference panel is applied to the model and then immediately used by the posture detection thread.

Figure 12. GALE IDE’s architecture. The compilation thread is started by the Controller, but further communication takes place only indirectly, through the Protected Reactive Model.

standard GUI toolkit (Gtk+), it enables functional style through the use of reactive relations, and it is large and complex enough to prove that our approach scales well in terms of code modularity, and even enhances it. 4.1.2

Summary

Keera Posture

Keera Posture is a posture monitor written in Haskell using OpenCV for image recognition and Gtk+ for the GUI 22 . The program works by comparing the current position of the user (estimating the distance based on the size of the head) with a reference position given during program calibration. When both differ “too much”, a warning is shown. Users can customise the sensitivity, the form of the warning (popup message, message bubble and/or sound), the sound being played, the language and the webcam being used. The initial calibration uses a step-by-step assistant. Users can pause the program by clicking on its icon, located in the system tray bar. The program has been implemented for end-users and thus care has been placed on providing common usability features and an intuitive user interface. Both Windows and Linux are supported. Like Gale IDE, Keera Posture runs several threads to keep the GUI responsive while doing image recognition. Changes in the posture are communicated to the GUI only indirectly, through the protected, reactive model. Of the 53 modules included in the program, the Model contains 13 (plus 4 which are generated automatically). The Model constitutes 30% of the code (measured in lines, without comments or empty lines) and exposes 16 Protected Model fields (projections of model parts onto Reactive Values). The Controller contains 30 modules, which constitute 50% of the code and comprise 29 Reactive Relations. The image recognition module contains 10% of the code, and the View (generated during compile time from a Gtk+ Glade file, using Template Haskell) contains only 4%. Keera Posture is a clear demonstration of how, using the right abstraction, one can write software that addresses real problems, in a purely functional way, with minimal boilerplate code. Also, through the use of Reactive Values and Relations, it exemplifies how one can limit the side effects of using imperative bindings mainly to the GUI, without sacrificing any of the features that standard GUI toolkits offer.

5.

Related work

5.1

Comparison to Functional GUIs and FRP

Fudgets [5] is a functional GUI framework structured around the notion of fudgets: visual, interactive data transformers with one input and one output. Fudgets was reviewed in Sec. 2.2. Limitations of Fudgets include not supporting connection of visually nonadjacent widgets and that mutually interconnected fudgets must be defined together. Our approach overcomes such issues by using one Reactive Value (RV) per widget property and by allowing separately defined RVs definitions to be related through Reactive Relations (RR). Gadgets [35] is similar to Fudgets, but tries to overcome some of its limitations. However, as discussed in Sec. 2.2, by their nature, both Fudgets and Gadgets need to provide a fudget/gadget definition for every single GUI widget of interest, meaning that such libraries necessarily become very large. This leads to high maintenance costs, which is one reason Fudgets is only available on a selection of Unix-like platforms and has not seen any major update since the late 1990’s. In contrast, RVs and RRs have a much smaller footprint and are designed to work in conjunction with existing GUI toolkits on any platform, thus side-stepping this issue. A key difference between Functional Reactive Programming (FRP) [6, 10] and our approach is that ours allows separately defined reactive entities to be related, while an FRP signal is defined in terms of the signals it depends on once and for all. As discussed in Sec. 2.3, this aspect of FRP often leads to scalability issues in large applications, in particular for mutually recursive signals. Unlike FRP, our approach is agnostic about time, thus not lending itself to reasoning about temporal properties. Soft real-time guarantees have been studied for at least some FRP variants [23]. It may be possible to give a semantics for RVs in terms of FRP. This would provide one way to reason about RVs, which certainly would be interesting. Nevertheless, so far, we have not experienced any issues with timeliness of responses intrinsic to our framework. Some FRP implementations, like Elerea [38], take special precautions to break change propagation loops. We use equality tests

22 http://github.com/keera-studios/keera-posture

56

middle ground, using a global dispatcher per Protected Model, but allowing different Protected Models to co-exist and even coordinate during execution. Our reactive rules constitute a data dependency language not unlike the data-binding facilities of frameworks like AngularJS26 and EmberJS27 . There are, however, structural differences. AngularJS, for instance, merges data-binding, function lifting, and view declaration into a single, annotated XML tree. We believe our approach results in a more modular and abstract design, partly because it maximises separation of concerns, and partly because it allows factoring choreographies out into libraries. As we have discussed, our framework uses equality tests to minimise change propagation and break loops. This approach is typically more efficient than the dirtychecking used in AngularJS, but further research is needed to determine how our solution compares to the aforementioned frameworks in terms of performance.

in setters to minimise change propagation. For loops, this means that propagation stops when reaching a fixed point. It is thus crucial that the functions provided for transforming read-write RVs are each others inverses, or propagation could go on indefinitely. This problem also exists in other frameworks such as Yampa [34] or object-oriented GUI toolkits like Qt. Our approach does not provide further guarantees, but specifying both directions of the transformation in a single place may facilitate discovering bugs quickly. There are some similarities between RVs and iTask’s [31] Uniform Data Sources (UDS) [30], but UDS has no support for subscription to change notification. Further, a central aspect of iTask is automatic generation of GUIs from types with a particular focus on Web applications, whereas RVs and RRs provide generic, re-usable infrastructure for GUI programming and more. There are also similarities to Lenses [41]. However, RVs are not lenses as they in general do not satisfy any lens-like laws. Nevertheless, RVs can beneficially be used together with lenses and we view them as complementary. Recent developments in monadic lenses applied to User Interfaces23 and lenses with notifications [8] could help simplify our formalisation to its true core objective: a data-binding language between typed reactive elements. Parametric Views [8] are based on the same basic operations (get, put, subscription) as RVs. Further, like Parametric Views, our Protected Models make change a first-class entity to minimise data propagation and screen refreshes. One difference is that our setters compare previous and new values when possible to minimise change propagation and break propagation loops. Parametric Views provides a versatile notion of invalidation function to that end. Our goal, however, is to make change detection as transparent as possible for which we are experimenting with automatically deriving change definitions for Haskell datatypes [39]. 5.2

6.

Summary and Future work

In this paper we have described a functional, compositional, reactive framework that provides a uniform interface to widget properties, files, sockets, application models and other external entities. We have demonstrated how reactive values can be defined, transformed and connected. Our solution works well with different GUI toolkits, and we have implemented several non-trivial applications. Our work has been guided by industrial experience. We have not yet undertaken formal analysis of temporal properties, but we plan to do this in the future; for example, through a semantics based on FRP. We expect to be able to reason about delays, change propagation and temporal inconsistencies. Our solution sacrifices consistency across a network of possibly duplicated values in favour of responsiveness and scalability [16]. We rely on always being able to break circular dependencies to achieve eventual consistency [42]. We believe that, so long as the only circular dependencies are due to direct bi-directional liftings and relations, it is sufficient if there is a limited number of compositions of the function in one direction and the one in the other that converges to a fixed point. We have observed constant memory consumption while profiling some applications. However, we expect the introduction of dynamic reactive relations to impact garbage collection, which we will need to take into account to avoid memory leaks. In this paper we have not described all the tools and libraries in our framework. This includes choreographies to update dynamic lists efficiently, support undo/redo, check for updates and log errors to a visual console. Our framework also includes tools to help with internationalization and to generate application skeletons. Eventually, out framework could evolve towards a compositional application toolkit structured around the concepts of model, view, controller, threads and relations, and a set of well-defined combinators. In such a setting choreographies could have a more precise meaning. We would like to give more importance to the change type associated to Protected Models, similarly to what is done in Parametric Lenses. We are experimenting with automatic instance derivation solutions, to abstract users from the details of defining custom types for change/focus. We have sometimes overloaded this type to carry information about the nature of the change; a high-level abstraction over value differences might help us address these concerns [39].

Comparison with OO and Reactive Programming

From an Imperative or Object-Oriented perspective, our work is closest in spirit to Reactive Programming24 , and then in particular to change subscription facilities and data binding languages. Reactive Values are similar to widget properties in Qt5 , which are typed, mutable attributes paired with a change event. Qt’s signals and slots can be seen as read-only and write-only RVs and are versatile enough to accommodate files, sockets and other external entities. Qt further provides data binding facilities to connect signals to slots, but unlike in our approach, these are uni-directional, and there is no mechanism for breaking propagation loops. When using Qt as a GUI backend from our framework, we provide an intermediate library that takes care of change detection and thread safety, shielding users from such details. Our notification system is similar to the observer design pattern [14] frequently encountered in object-oriented programming. This pattern has specific support in recent versions of Javascript in the form of Object.observe() [36]. The observer pattern enables detecting changes to objects, but it is necessary to install change handlers. This leads to issues of inversion of control common in event-driven programming [12, p. 36–37], and the scheme is further inherently uni-directional, unlike our bi-directional relations. Facebook’s React25 has similar goals to the observer pattern but is more declarative. Unlike our approach, React only provides uni-directional data-binding. Like our approach, React uses change detection mechanisms to minimise data propagation, which in the case of Web sites produces minimal DOM migrations. React gathers change propagation responsibilities to a central dispatcher in an attempt to maximise throughput. In contrast, our solution opts for a 23 http://people.inf.elte.hu/divip/LGtk/LGtk.html 24 https://github.com/Netflix/RxJava

26 http://angularjs.org/

25 http://facebook.github.io/react/

27 http://emberjs.com

57

Acknowledgements

[20] George T Heineman. An instance-oriented approach to constructing product lines from layers. Technical Report, WPI CS Tech Report 0506, 2005. [21] Kenneth Hoste. An Introduction to Gtk2Hs, a Haskell GUI Library. In Shae Erisson, editor, The Monad.Reader Issue 1. 2005. [22] Graham Hutton and Diana Fulger. Reasoning About Effects: Seeing the Wood Through the Trees. In Proceedings of the Symposium on Trends in Functional Programming, Nijmegen, The Netherlands, May 2008.

The authors would like to thank Paolo Capriotti, Robert Mitchelmore, Ambrus Kaposi, Graham Hutton, David McGillicuddy, Florent Balestrieri, Philip H¨olzenspies, Jennifer Hackett for their input during multiple discussions. We thank anonymous reviewers for helpful comments on earlier drafts of this manuscript.

References

[23] Roumen Kaiabachev, Walid Taha, and Angela Zhu. E-FRP with priorities. In Proceedings of the 7th ACM & IEEE international conference on Embedded software, EMSOFT ’07, pages 221–230. ACM, 2007. [24] Keera Studios. Keera Hails - Haskell on Rails. https://github. com/keera-studios/keera-hails. [25] Glenn E. Krasner and Stephen T. Pope. A Cookbook for Using the Model-View Controller User Interface Paradigm in Smalltalk-80. J. Object Oriented Program., 1(3):26–49, August 1988. [26] Glenn E Krasner, Stephen T Pope, et al. A Description of the ModelView-Controller User Interface paradigm in the Smalltalk-80 System. 1988. [27] Hai Liu, Neal Glew, Leaf Petersen, and Todd A Anderson. The intel labs haskell research compiler. In Proceedings of the 2013 ACM SIGPLAN symposium on Haskell, pages 105–116. ACM, 2013. [28] Conor McBride and Ross Paterson. Applicative programming with effects. Journal of functional programming, 18(01):1–13, 2008. [29] Sean McDirmid and Wilson C Hsieh. Superglue: Component programming with object-oriented signals. In ECOOP 2006–ObjectOriented Programming, pages 206–229. Springer, 2006. [30] Steffen Michels and Rinus Plasmeijer. Uniform data sources in a functional language. In Submitted for presentation at Symposium on Trends in Functional Programming, TFP, volume 12, 2012.

[1] Peter Achten and Rinus Plasmeijer. Interactive functional objects in Clean. In Implementation of Functional Languages, pages 304–321. Springer, 1998. [2] Peter Achten, Marko Van Eekelen, and Rinus Plasmeijer. Generic Graphical User Interfaces. In Implementation of Functional Languages, pages 152–167. Springer, 2005. [3] C. P. R. Baaij, J. Kuper, and L. Schubert. Soosim: Operating system and programming language exploration. In G. Lipari and T. Cucinotta, editors, Proceedings of the 3rd International Workshop on Analysis Tools and Methodologies for Embedded and Real-time System (WATERS 2012), Pisa, Italy, pages 63–68, Italy, 2012. Giuseppe Lipari. [4] John Backus. Can programming be liberated from the von neumann style? Commun. ACM, 21(8):613–641, 1978. [5] Magnus Carlsson and Thomas Hallgren. FUDGETS: A graphical user interface in a lazy functional language. (Section 6):321–330, 1993. [6] Antony Courtney and Conal Elliott. Genuinely Functional User Interfaces. In Proceedings of the 2001 Haskell Workshop, pages 41–69, 2001. [7] Evan Czaplicki. Elm: Concurrent FRP for Functional GUIs. PhD thesis, 2012. [8] L´aszl´o Domoszlai, Bas Lijnse, and Rinus Plasmeijer. Parametric lenses: change notification for bidirectional lenses. In Proceedings of the Symposium on Trends in Functional Programming, Soesterberg, The Netherlands, May 2014. Accepted for publication.

[31] Steffen Michels, Rinus Plasmeijer, and Peter Achten. iTask as a new paradigm for building GUI applications. In Implementation and Application of Functional Languages, pages 153–168. Springer, 2011.

[9] Jonathan Edwards. Coherent reaction. In Proceedings of the 24th ACM SIGPLAN conference companion on Object oriented programming systems languages and applications, pages 925–932. ACM, 2009.

[32] Brad A. Myers. Separating application code from toolkits: Eliminating the spaghetti of call-backs. In Proceedings of the 4th Annual ACM Symposium on User Interface Software and Technology, UIST ’91, pages 211–220, New York, NY, USA, 1991. ACM. [33] Brad A. Myers. Why Are Human-Computer Interfaces Difficult to Design and Implement? Technical report, 1993. [34] Henrik Nilsson, Antony Courtney, and John Peterson. Functional reactive programming, continued. In Proceedings of the 2002 ACM SIGPLAN workshop on Haskell, pages 51–64. ACM, 2002. [35] Rob Noble and Colin Runciman. Gadgets: Lazy functional components for graphical user interfaces. In Programming Languages: Implementations, Logics and Programs, pages 321–340. Springer, 1995. [36] Addy Osmani. Data-binding revolutions with Object.observe(). [37] Bryan O’Sullivan, John Goerzen, and Don Stewart. Real World Haskell. O’Reilly Media, Inc., 2008. [38] Gergely Patai. Eventless reactivity from scratch. Draft Proceedings of Implementation and Application of Functional Languages (IFL’09), pages 126–140, 2009. [39] Ivan Perez. 1st Year PhD Report. http://www.cs.nott.ac.uk/ ~ixp/, December 2014. [40] Simon L Peyton Jones and Philip Wadler. Imperative functional programming. In Proceedings of the 20th ACM SIGPLAN-SIGACT symposium on Principles of programming languages, pages 71–84. ACM, 1993.

[10] Conal Elliott and Paul Hudak. Functional reactive animation. In International Conference on Functional Programming, pages 163– 173, June 1997. [11] Conal M Elliott. Tangible functional programming. ACM SIGPLAN Notices, 42(9):59–70, 2007. [12] Mohamed Fayad and Douglas C. Schmidt. Object-oriented application frameworks. Commun. ACM, 40(10):32–38, October 1997. [13] J Nathan Foster, Michael B Greenwald, Jonathan T Moore, Benjamin C Pierce, and Alan Schmitt. Combinators for bi-directional tree transformations: a linguistic approach to the view update problem. ACM SIGPLAN Notices, 40(1):233–246, 2005. [14] Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides. Design Patterns: Elements of Reusable Object-oriented Software. Addison-Wesley Longman Publishing Co., Inc., Boston, MA, USA, 1995. [15] Jeremy Gibbons and Ralf Hinze. Just do it: Simple monadic equational reasoning. In ICFP, September 2011. [16] Seth Gilbert and Nancy Lynch. Brewer’s conjecture and the feasibility of consistent, available, partition-tolerant web services. SIGACT News, 33(2):51–59, June 2002. [17] S. Goderis. On the separation of user interface concerns: A Programmer’s Perspective on the Modularisation of User Interface Code. Asp / Vubpress / Upa, 2007.

[41] Benjamin C. Pierce. Combinators for bi-directional tree transformations: A linguistic approach to the view update problem, October 2004. Invited talk at New England Programming Languages Symposium.

[18] Tim Harris, Simon Marlow, Simon Peyton-Jones, and Maurice Herlihy. Composable memory transactions. In Proceedings of the Tenth ACM SIGPLAN Symposium on Principles and Practice of Parallel Programming, PPoPP ’05, pages 48–60, 2005.

[42] Werner Vogels. Eventually consistent. Commun. ACM, 52(1):40–44, January 2009.

[19] J. R. Heard. Beautiful code, compelling evidence. Technical report, 2008.

58

The Remote Monad Design Pattern Andy Gill Neil Sculthorpe ∗ Justin Dawson Andrew Farmer Mark Grebe Jeffrey Rosenbluth †

Aleksander Eskilson Ryan Scott James Stanton

Information and Telecommunication Technology Center, University of Kansas, USA [email protected]

Abstract

Definition. A remote monad is a monad that has its evaluation function in a remote location, outside the local runtime system.

Remote Procedure Calls are expensive. This paper demonstrates how to reduce the cost of calling remote procedures from Haskell by using the remote monad design pattern, which amortizes the cost of remote calls. This gives the Haskell community access to remote capabilities that are not directly supported, at a surprisingly inexpensive cost. We explore the remote monad design pattern through six models of remote execution patterns, using a simulated Internet of Things toaster as a running example. We consider the expressiveness and optimizations enabled by each remote execution model, and assess the feasibility of our approach. We then present a full-scale case study: a Haskell library that provides a Foreign Function Interface to the JavaScript Canvas API. Finally, we discuss existing instances of the remote monad design pattern found in Haskell libraries.

By factoring the RPC into sending invocation and service name, we can group together procedure calls, and amortize the cost of the remote call. To give an example, Blank Canvas, our library for remotely accessing the JavaScript HTML5 Canvas, has a send function, lineWidth and strokeStyle services, and our remote monad is called Canvas: send :: Device -> Canvas a -> IO a lineWidth :: Double -> Canvas () strokeStyle :: Text -> Canvas ()

If we wanted to change the (remote) line width, the lineWidth RPC can be invoked by combining send and lineWidth:

Categories and Subject Descriptors D.3.2 [Programming Languages]: Language Classifications—Applicative (functional) languages

send device (lineWidth 10)

Likewise, if we wanted to change the (remote) stroke color, the strokeStyle RPC can be invoked by combining send and strokeStyle:

Keywords Monads, Remote Procedure Call, FFI, Design Pattern.

1.

Introduction

send device (strokeStyle "red")

Remote Procedure Calls (RPCs) are expensive. This paper presents a way to make them considerably cheaper: the remote monad design pattern. Monads [34, 42] provide a general way of structuring composite computations. These monadic computations are first class: they can be passed to functions as arguments, returned from functions as results, and stored in data structures. But monadic computations are not typically executed remotely. This paper investigates the ways that monadic computations can be serialized for the purposes of being sent to remote locations for external execution. The idea is that, rather than directly call a remote procedure, we instead give the remote procedure call a service-specific monadic type, and invoke the remote procedure call using a monadic “send” function.

The key idea of this paper is that remote monadic commands can be locally combined before sending them to a remote server. For example: send device (lineWidth 10 >> strokeStyle "red")

The complication is that, in general, monadic commands can return a result, which may be used by subsequent commands. For example, if we add a monadic command that returns a Boolean, isPointInPath :: (Double,Double) -> Canvas Bool

we could use the result as follows:

∗ Now

at the Department of Computer Science, Swansea University, UK, [email protected] † Unaffiliated

send device $ do inside Remote ()

In the remote monad design pattern, we have a way of running our monad remotely, which we call send:

1. Control flow needs to transfer to the correct C function. Given the lowest level of the GHC runtime system is written in C, control-flow transfer is straightforward.

send :: Device -> Remote a -> IO a

The key step in an efficient implementation of the remote monad is the choice of packaging of commands. Ideally, the entire monadic computation argument of send would be transmitted in a single packet; but in general this is not possible. Towards understanding the choices, we are going to build several ways of interacting with our toaster:

2. The data structures that are arguments and results of calls to C need to be marshalled into the correct format. For example, C strings are not the same as Haskell strings. 3. The abstractions of the target C library may not be idiomatic Haskell abstractions. For example, many C++ APIs assume OO-style class inheritance. Foreign abstractions can be simulated in Haskell, but this raises an obfuscation barrier.

• As a preamble, in §3.1 we build a basic asynchronous RPC, and

in §3.2 we build a basic synchronous RPC.

Any time control flow leaves the Haskell eco-system, all three of these concerns need to be addressed. All three are supported by the Haskell FFI for C: There is a way of directly promoting a C function into Haskell-land; there is good support for marshalling Haskell data structures into C structures, as well as automatic memory-management support; and Haskell abstraction capabilities are used to build more Haskell-centric APIs on top of the FFI capability. However, what about functions that cannot be called directly, but must be invoked using a remote procedure call? All three identified FFI issues come into play:

• In §4 we build the simple version of a remote monad, called

a weak remote monad, where a monadic API is utilized to conceptually bundle commands for remote execution. • In §5 we build another remote monad, called a strong remote

monad, where a monadic API is utilized to actually bundle commands for more efficient remote execution. This is our principal model of the key idea of this paper. • In §6 we build a remote applicative functor, which exploits the

restrictions of the applicative functor operations (relative to the more expressive monadic bind) to allow for better bundling.

1. The control flow needs to use an RPC mechanism to establish foreign control-flow transfer. This can add considerable costs. A typical mechanism would be to establish a TCP/IP connection with a published remote service.

• Finally, in §7 we use a deep-embedding technique to add sup-

port for remote binding of non-local values, again improving the bundling of remote primitives.

2. The procedure arguments are sent to the remote location, typically over a TCP/IP channel, the remote command is executed, and the result is communicated back.

In each case, we implement the local behavior of the send function, as well as give a minimal implementation of remote behavior. In general, in the remote monad design pattern, the remote execution may be in any language. Here, for simplicity, we simulate remote execution from within Haskell, and use the built-in Show and Read classes to simulate serialization and deserialization. We highlight the dual execution nature of the remote monad by using distinct data structures locally and remotely. Using distinct data structures also allows us to side-step the orthogonal challenge of GADT deserialization.

3. The remote nature of the call raises issues with presenting remoteness to the API user. What does this remote function look like to a Haskell programmer? Are the costs reflected in the API design? In this paper we investigate a generalization of the remote procedure call, using monads as a descriptor of remote execution. Specifically, we make the following contributions:

3.1

• We document a DSL pattern for bundling RPCs: the remote

An Asynchronous Remote Command Call

This first model will be able to send single commands asynchronously to a remote device. This model is also known as the Command design pattern [19].

monad design pattern. This pattern cuts the cost of RPCs in many cases by amortizing the remote aspect, and is a design point between classical monadic APIs, and using deeply embedded DSLs for offline remote execution.

Definition. A remote command is a request to perform an action for remote effect, where there is no result value or temporal consequence.

• Toward understanding the remote monad, we give four com-

plete models of remote execution (§3.1, §3.2, §4, §5) and sketch two useful variations of the strongest model (§6, §7). We give a set of remote monad laws (§8), and observe that the pattern has been used in a weak form several times before (§11).

We use a deep embedding of the commands that we want to send asynchronously. For this simple example, we only consider a single command:

• We explore the design pattern in a real-world large-scale

data Command = Say String deriving Show

example, called Blank Canvas (§9). We quantify our experiences, and measure performance on some benchmarks, comparing native JavaScript and Blank Canvas, over two operating systems, and two web browsers (§10).

We derive a Show instance so that we can serialize Command, in preparation for transmitting it to a remote interpreter.

60

Local

GHCi

send

Network

When we execute a remote procedure, we either want to get a result back (e.g. the measured temperature), or know that a specific remote action has been completed (e.g. the toast is made). In this model, because we are interested in getting a reply, we represent remoteness using a function from String to IO String.

Remote

Command

device

send Say

data Device = Device { sync :: String -> IO String }

"Say \"Do you want some toast?\""

As with commands, we use a deep embedding of the procedures that we want to send. However, as we now expect to receive a result in reply, we use a GADT with a phantom type index denoting the expected result type:

()

Figure 1: Example of an Asynchronous Remote Command Call

data Procedure :: * -> * where Temperature :: Procedure Int Toast :: Int -> Procedure ()

We represent a remote device as a function from String to IO (), wrapped in a data structure called Device, which models the communication channel between local and remote execution.

As with commands, we provide serialization using the Show class:

data Device = Device { async :: String -> IO () }

deriving instance Show (Procedure a)

We call the internal function async because it represents asynchronous communication. For asynchronous commands, there is no need for a back-channel on which to return values. We can now define send, which serializes a Command and then transmits it to a remote device:

For deserialization, we provide an auxiliary function that uses the phantom type index of Procedure to determine which Read instance should be used to parse the reply from the remote device: readProcedureReply :: Procedure a -> String -> a readProcedureReply (Temperature {}) i = read i readProcedureReply (Toast {}) i = read i

send :: Device -> Command -> IO () send d m = async d (show m)

The two read functions are each reading different types, as constrained by the specific Procedure constructor. Now we can write our send command:

Our Haskell simulation of the remote device requires a representation of commands on the remote device, an execution function for those commands, and a deserialization function that reads those commands. As a convenience, we use the same constructor name for the remote command data type, so that deserialization can be handled by a derived Read instance.1

send :: Device -> Procedure a -> IO a send d m = do r IO () execRCommand (Say str) = putStrLn ("Remote: " ++ str)

Our device, which simulates the remote interpreter handle, can then be defined as: device :: Device device = Device (execRCommand . read)

data RProcedure = Temperature | Toast Int deriving Read

This completes our model. Now we can test it by sending a Say command, which prints remotely:

execRProcedure :: RProcedure -> IO String execRProcedure Temperature = do t send device (Say "Do you want some toast?") Remote: Do you want some toast?

In summary, this model is a direct implementation of asynchronous remote calls. Figure 1 shows the interactions in this example invocation using a sequence diagram. We take the liberty of expressing our construction of Command as a sequence process, and we give the serialized text sent to the remote device, using green for an asynchronous call. 3.2

device :: Device device = Device (execRProcedure . read)

A Synchronous Remote Call

This completes our model. Now we can test it by sending a Temperature procedure, which returns the temperature locally:

In this subsection we build a synchronous remote call: a version of send that can receive a reply to a remotely transmitted procedure. That is, we will define a model of a remote procedure call.

GHCi> send device Temperature 56

We have taken a procedure, transmitted it to a remote interpreter, executed it remotely, and returned with the result — thus we have a basic model of a typical Remote Procedure Call. Figure 2 shows the sequence diagram for this interaction. We use red to highlight the synchronous communication call.

Definition. A remote procedure is a request to perform an action for its remote effects, where there is a result value or temporal consequence. 1 This

code must be placed in a separate module to avoid a name clash.

61

Local

GHCi

send

Network

Local

Remote

Procedure

GHCi

device

send

Network

Remote

Remote

device

send

send

say

Temperature "Temperature"

()

"56"

temperature

"Say \"Do you want some toast?\""

"Temperature"

56

"56"

56 56 say

Figure 2: Example of a Synchronous Remote Procedure Call

()

"Say \"56F\""

56 send toast

4.

The Weak Remote Monad

"Toast 120" "()"

Thus far, our arguments to send have not been instances of Monad. We will now address this. Here we join the asynchronous and synchronous models into a monad called Remote. In this section, we implement an initial version of the remote monad that does not amortize the cost of communication. We call this a weak remote monad.

() ()

Figure 3: Example of a Weak Remote Monad

Definition. A weak remote monad is a remote monad that sends each of its remote calls individually to a remote interpreter.

sendProcedure :: Procedure a -> Remote a sendProcedure m = Remote $ do d Remote a -> IO a send d (Remote m) = runReaderT m d

Finally, the virtual remote Device is a combination of the synchronous and asynchronous Devices: device :: Device device = Device (execRProcedure . read) (execRCommand . read)

data Device = Device { sync :: String -> IO String , async :: String -> IO () }

Now we can call send with a monadic argument, and chains of primitives, connected using the monad, will be executed. We have achieved our original goal: a (weak) remote monad where the primitive commands and procedures are executed in a remote location.

A send-style function, customized for our weak Remote monad, provides remote execution for Command. Observe that each command invokes the remote procedure call immediately.

GHCi> t Remote () sendCommand m = Remote $ do d Remote () say txt = sendCommand (Say txt)

Figure 3 shows the sequence diagram for this example of the weak remote monad. For every primitive call invoked, there is a remote call to the device. Furthermore, the GHCi computation does not terminate until the toast is complete.

For procedures, we also provide a send-style function, again customized for our weak Remote monad. Again, each procedure invokes the remote procedure call immediately.

62

5.

The Strong Remote Monad

Local

We want to bundle monadic remote calls, and send them as packets of computations to be remotely executed. The strong remote monad does this. We have two classes of primitive remote calls: commands that do not require any specific result to be observed, and procedures that require a reply from the remote site.

GHCi

send

Network

Remote

Remote

device

send say () temperature

• For commands, which are asynchronous and do not send a

reply, we can queue up the command and commit to sending it later.

"Packet [Say \"Do you want some toast?\"] Temperature" "56"

56

• For procedures, which are synchronous, we need to transmit

say

them immediately. Thus we first send all outstanding queued commands, then send the procedure, and then await the procedure’s result.

() "[Say \"56F\"]" 56 send

At the end of executing a send, we flush the queue of any outstanding commands. This is the key idea behind a strong remote monad: package the sequence of monadic actions into a list of commands, which are (in all but the final case) terminated by procedures. This design assumes that the only primitive remote calls in our remote monad are commands and procedures, and thus there is no way of locally pausing or stalling the pipeline of commands. The queuing of commands is simply a bundling strategy for commands and procedures that would be executed immediately after each other anyway.

toast "Packet [] (Toast 120)" "()" () ()

Figure 4: Example of a Strong Remote Monad

sendProcedure :: Procedure a -> Remote a sendProcedure p = Remote $ do d IO a send d (Remote m) = do (r,cs) Remote () sendCommand cmd = Remote (modify (++ [cmd]))

execRPacket :: RPacket -> IO String execRPacket (Packet cs p) = do mapM_ execRCommand cs execRProcedure p

say :: String -> Remote () say txt = sendCommand (Say txt)

Figure 4 shows the sequence diagram for the strong remote monad, on the same example as used for the weak remote monad. As can been seen, Packet combines Commands, punctuated by Procedures.

Supporting procedures is more involved. We need to flush the queue of outstanding Commands, as well as to actually send a packet containing the Commands and procedure to the remote site:

63

6.

The Remote Applicative Functor

Local

Network

Remote

There is also a remote applicative functor. GHCi

Definition. A remote applicative functor is an applicative functor that has its evaluation function in a remote location, outside the local runtime system.

send

Remote

device

send say

As with monads, there are two classes: the weak remote applicative functor, and the strong remote applicative functor. Without a bind operator, applicative functors [30] are fundamentally better suited to remoteness than monads are: subsequent applicative computations cannot depend on the results of prior computations, which in our context allows for bundling of procedures. In fact (using our terminology) a strong remote applicative functor is currently used by Facebook to bundle database requests [29]. Any weak remote monad is, by definition, (at least) a weak remote applicative functor, with primitive calls transmitting individually. More interesting is the strong remote applicative functor, which we consider here. Exploiting the independence of subsequent calls from the results of prior calls, we are going to bundle all the Commands and Procedures together in a single packet, which we represent by a list of Prims (primitive remote calls).

() temperature 56∗ toast ()∗

Applicative Functor used to extract primitives. (56∗ , ()∗ , and 99∗ are unevaluated thunks at return time.)

temperature 99∗

"[Command (Say \"Good Morning\"), Procedure Temperature, Procedure (Toast 120), Procedure Temperature]" "[\"56\",\"()\",\"99\"]"

(Returned value used to tie the knot) (56,99)

Figure 5: Example of a Strong Remote Applicative Functor

data Prim :: * where Command :: Command -> Prim Procedure :: Show a => Procedure a -> Prim

We are able to transmit Procedures as a bundle because we cannot examine the result on individual Procedures until we have the entire applicative functor computation result — a key property of applicative functors. Our simulated remote device now needs to handle lists of intermingled commands and procedures:

deriving instance Show Prim

Our applicative functor is a wrapper around a monad transformer: newtype Remote a = Remote (WriterT [Prim] (State [String]) a)

data RPrim = Command RCommand | Procedure RProcedure deriving Read

This monad transformer is a combination of the writer monad, for accumulating queued primitive calls, and the state monad, for simultaneously parsing results. We will use lazy evaluation to “tiethe-knot” [2] between these two effects in send. We explicitly provide the instances for Applicative and Functor, but not Monad. We handle Commands using the underlying writer monad:

execRPrims :: [RPrim] -> IO [String] execRPrims [] = return [] execRPrims (Command c : ps) = do execRCommand c execRPrims ps execRPrims (Procedure p : ps) = do r Procedure a -> Remote a sendProcedure p = Remote $ do tell [Procedure p] ~(r:rs) (t1,t2) temperature) (toast 120 *> temperature) Remote: Good Morning Remote: Toasting... ...sleeping for 120 seconds... Remote: Done! GHCi> print (t1,t2) (56,99)

send :: Device -> Remote a -> IO a send d (Remote m) = do rec let ((a,ps),_) = runState (runWriterT m) r r Remote Id bindBindee e = do i * where Reply :: StringExpr -> Procedure String

Definition. A remote binding is the combination of a request to perform an action for its remote effects, where there is an interesting result value, and the remote interpreter binding this result value to a name remotely. The remote action is called a remote bindee.

reply :: StringExpr -> Remote String reply e = sendProcedure (Reply e)

This completes our set of transmittable primitives. The remaining definitions are almost identical to the strong remote monad model, except that the Remote monad now contains a numeric state that is used for fresh name generation, and the remote interpreter has an environment that can be updated. Using the above new machinery, and a suitable remote device, we can now run an example:

The result of a remote binding can be used immediately, without needing local interaction, because the result value resides remotely. Locally, we generate a new and unique Id, allowing the bindee to be bundled with commands. This is the trick — remote binders are a form of remote procedure calls, but because they return handles with known identifiers, we can transmit them to the remote site as remote commands. We want our Temperature procedure to now be a Bindee, so that we can directly use the result remotely. We constrain the type of an Bindee in the same way we constrained the return type of a procedure: by using a GADT.

GHCi> send device $ do t * where Temperature :: Bindee StringExpr

We now define the Command data type such that Say takes a StringExpr argument, and with a Bind command that will remotely bind an Id to a Bindee: data Command where Say :: StringExpr -> Command Bind :: Id -> Bindee StringExpr -> Command

newtype Object = Object Int

We have our utility functions say and temperature, and a function bindBindee that locally generates a name for the remotely bound value, before utilizing sendCommand.

Haskell can pass around the abstract but serializable Object, and the remote interpreter has an array of objects, indexed by an integer. However, a caveat with remotely bound objects is that, by creating a handle to a remote object, there is no (easy) way to know when you can garbage collect it.

say :: StringExpr -> Remote () say txt = sendCommand (Say txt)

65

8.

The Remote Monad Laws

8.2

A remote monad is a monad that has its evaluation function in a remote location, outside the local runtime system. In the remote monad design pattern, this achieved by providing:

Making networks robust is a hard problem. For example, UDP packets are not guaranteed to arrive, or arrive in order, so are unsuitable, without additional infrastructure, for use in a remote monad. The remote monad laws assume the network works. However, having all remote calls wrapped in a send allows for send to enforce a specific policy; for example, time-out after 3 seconds and raise an exception in the Local monad. Also, the Remote monad could have control structures for exception handling built in. The advantage is that by having a structured way of calling remote services, this gives hooks to include systematic recovery services as part of the remote service API.

• a remote monad — a set of primitive commands and procedures

that form a monad; and • a send function that facilitates remote execution of the primi-

tive commands and procedures. Towards a more systematic understanding of the remote monad, we propose the remote monad laws. Consider a send specialized to a specific destination or device d, with a remote monad Remote, and local monad Local . The send function is a natural transformation between Remote and Local : sendd

::

8.3

As part of a design pattern, the send function itself says nothing about how the individual commands and procedures within the Remote monad are bundled into packets. Instead, as we have seen, send functions can implement different bundling algorithms. We propose using the monad-transformer lift laws [22, 27], also known as the monad homomorphism laws, as our remote monad laws, because we are lifting the Remote computation to the remote site, by Local effect. = =

return a sendd m >>= (sendd . k)

9.

(1) (2)

= = =

pure a sendd m1 sendd m2 fmap f (sendd m)

(3) (4) (5)

Laws (1) and (3) state that a send has no effect for pure computations. Laws (2) and (4) state that packets of remote commands and procedures preserve the ordering of their effects, and can be split and joined into different sized packets without side effects. Law (5) is a reflection of the fact that send captures a natural transformation. 8.1

Extended Example: Blank Canvas

In our toaster models, the remote interpreter was also written in Haskell. This will not be the case in general. Blank Canvas is our Haskell library that provides the complete HTML5 Canvas API, using a strong remote monad. In this section, we describe how we used the remote monad design pattern to build this full scale API that remotely calls JavaScript. The HTML5 standard, implemented by all modern web browsers, supports a rich canvas element. This canvas element provides a low-level 2-dimensional interface to the in-browser widget. The API provides approximately 30 methods, most of which are void methods called for their effect, and almost 20 settable attributes. Table 1 gives a complete list of the base API. We name our remote monad Canvas, because it represents methods on the JavaScript Canvas. From examining Figure 1, all methods and all settable attributes can be transliterated into monadic Haskell functions in our remote monad. All methods and attributes are serializable values; there are no callbacks in this API. Classifying the API as remote commands, bindings and procedures is all about deciding if a specific data structure is local or remote:

Assuming these laws, the monad laws, and the laws relating functors and applicative functors to monads, the following morphism laws can be derived: sendd (pure a) sendd (m1 m2 ) sendd (fmap f m)

Threading

We have assumed a single-threaded local user and a single-threaded remote service, and the remote monad laws reflect this assumption. We could lift the local single-threaded assumption trivially, by using a lock on invocations of send, keeping the remote interactions single threaded. Alternatively, we could allow the local actions to be threaded, and have the thread usage reflect into the remote site. The interleaving semantics will certainly depend on the specific remote monad, and, as when running any side-effecting command on any multi-threaded system, interference patterns need to be considered and accounted for.

∀ a . Remote a → Local a

sendd (return a) sendd (m >>= k)

Networking

Infinite Remote Monads

From observation of the laws, it is straightforward to split any finite sequence of total Remote primitives into arbitrarily sized packets. It should be possible to lift the finiteness pre-condition as follows:

• isPointInPath, toDataURL, and measureText, are pro-

cedures, which return values to the Haskell runtime system, returning Bool, Text and TextMetrics respectively. (TextMetrics is simply a wrapper around a Double.)

• In the weak remote monad, each primitive invokes an individual

RPC. Therefore, it is completely reasonable to have an infinite stream of primitives in the Remote monad, in the same way that we can have infinite monadic computations in the IO monad. The hArduino library, discussed in §11.4, is an example of this.

• createLinearGradient,

createRadialGradient, and createPattern, are bindings, because CanvasGradient and CanvasPattern are subtypes of an image class, which can only be used remotely.

• In the strong remote monad, it would be possible to have an

infinite stream of primitives in the Remote monad, provided the packets are themselves finite. If necessary, this could be artificially manufactured by putting an upper bound on the number of consecutive commands before sending a packet.

• ImageData is a type that is local to Haskell. This is a de-

sign choice. ImageData is an array of RGB byte values, intended for pixel-level manipulations of images before calling putImageData to render to a canvas. Instead of reflecting an entire deep embedding of arithmetic and array operations, we make ImageData a wrapper around a Haskell byte vector, to be constructed Haskell-side, and provided as a serializable argument to putImageData. Thus, getImageData is a procedure.

We leave lifting the finiteness requirement of the remote applicative functor to future work, observing that an applicative functor will likely require Alternative, or something similar, to achieve an interesting infinite denotation.

• Everything else, including setting attributes, are commands.

66

Table 1: JavaScript API for HTML5 Canvas void void void void void void void void void TextMetrics void void void void void void void void void void void boolean

T RANSFORMATION save() restore() scale(float x,float y) rotate(float angle) translate(float x,float y) transform(float m11,float m12,float m21,float m22,float dx,float dy) setTransform(float m11,float m12,float m21,float m22,float dx,float dy)

F ONTS , COLORS , STYLES AND SHADOWS ( ATTRIBUTES ) globalAlpha float globalCompositeOperation string lineWidth float lineCap string lineJoin string miterLimit float strokeStyle any fillStyle any shadowOffsetX float shadowOffsetY float shadowBlur float shadowColor string strokeStyle any fillStyle any font string textAlign string textBaseline string

T EXT fillText(string text,float x,float y,[Optional] float maxWidth) strokeText(string text,float x,float y,[Optional] float maxWidth) measureText(string text) PATHS beginPath() fill() stroke() clip() moveTo(float x,float y) lineTo(float x,float y) quadraticCurveTo(float cpx,float cpy,float x,float y ) bezierCurveTo(float cp1x,float cp1y,float cp2x,float cp2y,float x,float y ) arcTo(float x1,float y1,float x2,float y2,float radius ) arc(float x,float y,float radius,float startAngle,float endAngle,boolean d) rect(float x,float y,float w,float h) isPointInPath(float x,float y)

void void void void CanvasGradient CanvasGradient CanvasPattern string ImageData ImageData void

send context $ do moveTo(50,50) lineTo(200,100) lineWidth 10 strokeStyle "red" stroke()

send context $ do isPointInPath(10,20)

I MAGES toDataURL([Optional] string type, [Variadic] any args) createImageData(float sw, float sh) getImageData(float sx, float sy, float sw, float sh) putImageData(ImageData imagedata, float dx, float dy, [Optional] . . . )

function IsPointInPath(x,y) { return function (u,c) { $.kc.reply(u,c.isPointInPath(x,y)); } }

For bindings, we allocate a global variable, and remember the unique number inside the proxy object. Haskell send context $ do grd IO a

and the inner send has the type updateWindow :: Window -> Update a -> Curses a

This package compiles commands into ANSI-style command sequences to be execute directly on the terminal. 11.2

Database Access

• In the Haxl DSL, Marlow [29] uses the properties of applicative

functors to issue database queries in parallel. The send function has the type: runHaxl :: Env u -> GenHaxl u a -> IO a

Haxl is a DSL with a weak remote monad, with a strong remote applicative functor. • mongoDB [23] is an API into the popular MongoDB NoSQL

database that uses a weak remote monad. The send function has the type:

Anecdotal Evidence

access :: MonadIO m => -> -> ->

Blank Canvas has now been used by the students in four separate instances of our functional programming class. Students find it easy to understand, given the analog between the IO monad and the remote Canvas monad, with students often choosing to use Blank Canvas for their end-of-semester project. To give two examples, one end-of-semester project was Omar Bari and Dain Vermaak’s Isometric Tile Game, that can be rotated in 3D in real-time; another project was Blankeroids, a playable asteroids clone, written by Mark Grebe, on top of Yampa [9] and yampa-canvas [39]. Both are shown here with the students’ permission.

11.3

Pipe AccessMode Database Action m a -> m a

Browser / Server

• The rather ingenious Haste.App library [10, 11] is an instance

of the remote monad design pattern. The Haste.App uses two Haskell compilers, a Haskell to JavaScript compiler for the local monad, and GHC for the remote monad evaluator, with the glue code between the two generated artifacts being automatically generated. The send function has the type: onServer :: Binary a => Remote (Server a) -> Client a

The Remote is a wrapper to help stage the difference between the client and server compilations. The Server monad is the remote monad. The remoteness here is running on the server; the main program runs on the client browser. • Sunroof is a compiler for a monadic JavaScript DSL [4, 20],

developed by the University of Kansas. The compiler, which is considerably more involved than Blank Canvas, could be used stand-alone, as a part of a strong remote monad. There are three separate send commands: In summary, Blank Canvas, using the remote monad, is a viable way for students to draw pictures and write games in Haskell. Rendering graphics uses many more commands than procedures, so it (retrospectively) turns out to be an ideal candidate for the remote monad. Given that we have access to whole new capabilities, we consider the overheads of using the remote monad reasonable.

11.

asyncJS :: SunroofEngine -> syncJS :: (...) => SunroofEngine -> -> rsyncJS :: (...) => SunroofEngine ->

JS JS IO JS

t () -> IO () t a (ResultOf a) t a -> IO a

Using what we have learned from studying the remote monad design pattern, these three send functions could be combined into a single send.

Related Work

11.4

Once the remote monad design pattern is understood, many instances of its use, or of related patterns, can be observed in the wild. In this section we discuss some of the existing instances, and present the type of the send analog, with the natural transformation highlighted in red. This list is not intended to be comprehensive, but rather to give a flavor of existing uses, or close uses, of the remote monad and related ideas.

Embedded Systems

• Levent Erk¨ok’s hArduino package [14] uses the weak remote

monad design pattern. The send-command withArduino is a one-shot operation, and does not have the ability to return values. Instead, the monad represents the whole computation to be executed. withArduino :: Bool -> FilePath -> Arduino () -> IO ()

68

• Ben Gamari’s bus-pirate package [18] allows access to the

Domain Specific Languages are Haskell’s forte. There are shallowly embedded DSLs [25, 26], with direct execution, and deeply embedded DSLs [12], with a generated structure representing the computation and a paired evaluator. There are also tagless-final DSLs [6], where there is no early commitment to a specific embedding, but instead class overloading in used to specify the API. Several Haskell DSLs, including Feldspar [37] and Obsidian [8], reify the structure of monadic code [40, 41] to generate external imperative code. Other DSLs, including CoPilot [38] and Ivory [24], use a shallow embedding of statements to capture the monadic structure. All of these DSLs could leverage the remote monad design pattern.

I2 C or SPI embedded device protocols via a serial port, using monadic primitives. The send command implements the remote monad directly. runBusPirate :: FilePath -> BusPirateM a -> IO (Either String a)

• The University of Kansas used an (as yet unpublished) strong

remote applicative functor in the design of λ-bridge, an interface between Haskell and an FPGA, built on top of the open Wishbone [35] bus protocol. send :: Board -> BusCmd a -> IO (Maybe a)

12.

The applicative functor structure allows multiple reads and writes to be combined into a bus transaction, and executed on an FPGA. 11.5

GUIs

• Heinrich Apfelmus’ threepenny-gui [1] uses a user-interface

element monad UI as a weak remote monad. runUI :: Window -> UI a -> IO a

11.6

GPGPUs

• The popular accelerate package [7] uses a remote array

design pattern to program a GPGPU. The send function takes an Acc, and returns a pure result. runIn :: Arrays a => Context -> Acc a -> a

This version of send does not use a local monad, but it can be considered to use the Identity monad. The send function can be purely functional specifically because the computations that are performed remotely are purely functional. Like other uses of the remote design pattern, Acc encodes the computation to be performed, in this case on the GPGPU. Acc is a DSL which provides array-based operators; such as zipWith and map. There is no applicative apply, or monadic bind, for Acc. 11.7

SMT solvers

• Levent Erk¨ok’s sbv package [15] provides access to SMT

data Transport (m :: * -> *) = forall a . (Show a) => Transport (m a)

solvers, and uses a weak remote monad as its lowest level API. runSymbolic’ :: SBVRunMode -> Symbolic a -> IO (a, Result)

11.8

By providing a Read instance for Transport, we can hide the phantom type index from the Read instance, while ensuring that we have a Show instance for the phantom type. We expect that any full-scale implementation would use similar techniques when the remote interpreter uses Haskell. A remote monad is a sweet spot between an RPC and a deeply embedded DSL, offering the possibility of rich FFIs for little effort and runtime cost. The remote monad design pattern allows a progression from weak, to strong, to deep embedding, giving a gentler pathway to implement deeply embedded DSLs. We have used the remote monad design pattern many times, and hope others will find the pattern as useful as we have.

Games

• Douglas Burke’s mcpi package [5] uses a weak remote monad

to allow Haskell users to interact with a Minecraft server. runMCPI :: MCPI a -> IO a

11.9

Conclusion and Future Work

This work was inspired by the question of what would an online evaluator of a deeply embedded DSL outside the Haskell heap look like. However, as we have seen, other DSL technologies can be used to generate the remote packets; the remote monad ideas are orthogonal to specific DSL implementation technologies. We thought we were writing a paper about deeply embedded DSLs and interesting GADT encodings. Instead, we discovered a design pattern and a small language for RPC APIs. We have built a number of remote monads and remote applicative functors. Aside from the examples already documented above, we have also reimplemented the Minecraft API found in mcpi, but with a strong remote monad, and a general JSON-RPC framework in Haskell. In particular, the JSON-RPC protocol supports multiple batched calls, as well as individual calls. Currently, the user needs to choose between the monadic and applicative functor API. We will use this prototype as a test-bench to explore the combination of simultaneously being a strong remote monad and a strong remote applicative functor. The recent push to add applicative functor do-notation as a GHC-extension is something that we can take advantage of here. Our simulated remote interpreters used regular data structures, rather than sharing the GADT used by send. This was for clarity, but also to avoid the complications of reifying GADTs. This is not a fundamental limitation: we have also implemented all our models using GADTs for the remote procedures, using a wrapper GADT called Transport to enable deserialization.

Other Related Works

This work is the marriage of Domain Specific Languages and Remote Procedure Calls. Both have a long and rich history. Sun Microsystems implemented the first widely-distributed RPC implementation [31, 32]. The ideas were based on Birrell and Nelson’s seminal work in this area [3]. All modern operating systems include RPC capabilities, building on these initial implementations. There is now an array of high-level libraries and frameworks for almost any modern language, offering RPC services such as D-Bus [28], which operates within a single machine, and Java’s Jini, now called Apache River (http://river.apache.org/). The Haskell cloud-computing effort [13] includes support for RPCs, and can be considered such a middleware solution. There are also many low-level protocols for executing RPCs, such as JSON-RPC (http://www.jsonrpc.org/).

Acknowledgments We would like to thank Heinrich Apfelmus, Conal Elliott, and Levent Erk¨ok for their useful discussions and insights, and the anonymous reviewers for their useful and constructive comments. This material is based upon work supported by the National Science Foundation under Grant No. 1117569 and Grant No. 1350901. Aleksander Eskilson, Ryan Scott and James Stanton were supported by the NSF REU initiative. The icons in the sequence diagrams were created by Tomoyuki Miyano, and made available online, at http:/iconarchive.com/.

69

References

[23] T. Hannan. Hackage package mongoDB-2.0.5, 2015.

[1] H. Apfelmus. Hackage package threepenny-gui-0.6.0.2, 2015. [2] R. Bird. Using circular programs to eliminate multiple traversals of data. Acta Informatica, 21(3):239–250, 1984. [3] A. D. Birrell and B. J. Nelson. Implementing remote procedure calls. Transactions on Computer Systems, 2(1):39–59, 1984. [4] J. Bracker and A. Gill. Sunroof: A monadic DSL for generating JavaScript. In International Symposium on Practical Aspects of Declarative Languages, volume 8324 of LNCS, pages 65–80. Springer, 2014. [5] D. Burke. Hackage package mcpi-0.0.1.2, 2014. [6] J. Carette, O. Kiselyov, and C. Shan. Finally tagless, partially evaluated: Tagless staged interpreters for simpler typed languages. Journal of Functional Programming, 19(05):509–543, 2009. [7] M. M. T. Chakravarty, R. Clifton-Everest, G. Keller, S. Lee, B. Lever, T. L. McDonell, R. Newtown, and S. Seefried. Hackage package accelerate-0.15.1.0, 2015. [8] K. Claessen, M. Sheeran, and B. J. Svensson. Expressive array constructs in an embedded GPU kernel programming language. In Workshop on Declarative Aspects and Applications of Multicore Programming, pages 21–30. ACM, 2012. [9] A. Courtney, H. Nilsson, and J. Peterson. The Yampa arcade. In Haskell Workshop, pages 7–18. ACM, 2003. [10] A. Ekblad. Hackage package haste-compiler-0.4.4.4, 2015. [11] A. Ekblad and K. Claessen. A seamless, client-centric programming model for type safe web applications. In Haskell Symposium, pages 79–89. ACM, 2014. [12] C. Elliott, S. Finne, and O. de Moor. Compiling embedded languages. Journal of Functional Programming, 13(3):455–481, 2003. [13] J. Epstein, A. P. Black, and S. Peyton Jones. Towards Haskell in the cloud. In Haskell Symposium, pages 118–129, 2011. [14] L. Erkok. Hackage package hArduino-0.9, 2014. [15] L. Erkok. Hackage package sbv-4.4, 2015. [16] L. Erk¨ok and J. Launchbury. Recursive monadic bindings. In International Conference on Functional Programming, pages 174–185. ACM, 2000. [17] L. Erk¨ok and J. Launchbury. A recursive do for Haskell. In Haskell Workshop, pages 29–37. ACM, 2002. [18] B. Gamari. Hackage package bus-pirate-0.6.2, 2015. [19] E. Gamma, R. Helm, R. Johnson, and J. Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Addison Wesley, 1994. [20] A. Gill and J. Bracker. Hackage package sunroof-server-0.2.1, 2014. [21] A. Gill and A. Farmer. Hackage package kansas-comet-0.3.1, 2014. [22] A. Gill and R. Paterson. Hackage package transformers-0.4.3.0, 2015.

[24] P. C. Hickey, L. Pike, T. Elliott, J. Bielman, and J. Launchbury. Building embedded systems with embedded DSLs. In International Conference on Functional Programming, pages 3–9. ACM, 2014. [25] P. Hudak. Modular domain specific languages and tools. In International Conference on Software Reuse, pages 134–142. IEEE Press, 1998. [26] D. Leijen and E. Meijer. Domain specific embedded compilers. In Conference on Domain-Specific Languages, pages 109–122. ACM, 1999. [27] S. Liang, P. Hudak, and M. Jones. Monad transformers and modular interpreters. In Symposium on Principles of Programming Languages, pages 333–343. ACM, 1995. [28] R. Love. Get on the D-BUS. Linux Journal, 2005(130):3, 2005. [29] S. Marlow, L. Brandy, J. Coens, and J. Purdy. There is no fork: An abstraction for efficient, concurrent, and concise data access. In International Conference on Functional Programming, pages 325– 337. ACM, 2014. [30] C. McBride and R. Paterson. Applicative programming with effects. Journal of Functional Programming, 18:1–13, 2008. [31] S. Microsystems. RPC: Remote procedure call protocol specification. Technical report, RFC 1050, Apr. 1988. [32] S. Microsystems. RPC: Remote procedure call protocol specification: Version 2. Technical report, RFC 1057, June 1988. [33] J. Millikin. Hackage package ncurses-0.2.11, 2014. [34] E. Moggi. Computational lambda-calculus and monads. In Symposium on Logic in Computer Science, pages 14–23. IEEE Press, 1989. [35] OpenCores Organization. Wishbone B4: WISHBONE System-on-Chip (SoC) Interconnection Architecture for Portable IP Cores, 2010. URL http://opencores.org/opencores,wishbone. [36] B. O’Sullivan. Hackage package criterion-1.1.0.0, 2015. [37] A. Persson, E. Axelsson, and J. Svenningsson. Generic monadic constructs for embedded languages. In Symposium on Implementation and Application of Functional Languages, volume 7257 of LNCS, pages 85–99. Springer, 2012. [38] L. Pike, N. Wegmann, S. Niller, and A. Goodloe. A do-it-yourself high-assurance compiler. In International Conference on Functional Programming, pages 335–340. ACM, 2012. [39] N. Sculthorpe. Hackage package yampa-canvas-0.2, 2014. [40] N. Sculthorpe, J. Bracker, G. Giorgidze, and A. Gill. The constrainedmonad problem. In International Conference on Functional Programming, pages 287–298. ACM, 2013. [41] J. Svenningsson and B. J. Svensson. Simple and compositional reification of monadic embedded languages. In International Conference on Functional Programming, pages 299–304. ACM, 2013. [42] P. Wadler. Comprehending monads. In Conference on LISP and Functional Programming, pages 61–78. ACM, 1990.

70

Variations on Variants J. Garrett Morris The University of Edinburgh Edinburgh, UK [email protected]

Abstract

assuring that conceptually independent features are implemented independently, and simplifying refactoring of larger programs. This paper studies extensible variants, a language mechanism that supports modular programming. Extensible variants permit piecewise extension of algebraic data types with new cases, and support code reuse in constructing and deconstructing values of extended data types. We present two encodings of extensible variants, providing the same interface but using different extensions of the Haskell class system (instance chains and closed type families). Our goals in doing so are twofold. First, we evaluate their expressiveness, by comparing them with row typing, a canonical approach to extensible variants in functional languages. Second, we use them as test cases to compare the language mechanisms used in their definition. We find that we can implement the same functions in each encoding, and these functions are sufficient to express anything expressible with row types. However, our encodings introduce a need for explicit type annotations (or type signatures) in encoded terms where such annotations would not be necessary with row types. We sketch a mechanism that would eliminate the need for these type annotations. Finally, while our encoding using closed type families is as expressive as that using instance chains, a straightforward improvement of the latter escapes easy translation to the former.

Extensible variants improve the modularity and expressiveness of programming languages: they allow program functionality to be decomposed into independent blocks, and allow seamless extension of existing code with both new cases of existing data types and new operations over those data types. This paper considers three approaches to providing extensible variants in Haskell. Row typing is a long understood mechanism for typing extensible records and variants, but its adoption would require extension of Haskell’s core type system. Alternatively, we might hope to encode extensible variants in terms of existing mechanisms, such as type classes. We describe an encoding of extensible variants using instance chains, a proposed extension of the class system. Unlike many previous encodings of extensible variants, ours does not require the definition of a new type class for each function that consumes variants. Finally, we translate our encoding to use closed type families, an existing feature of GHC. Doing so demonstrates the interpretation of instances chains and functional dependencies in closed type families. One concern with encodings like ours is how completely they match the encoded system. We compare the expressiveness of our encodings with each other and with systems based on row types. We find that, while equivalent terms are typable in each system, both encodings require explicit type annotations to resolve ambiguities in typing not present in row type systems, and the type family implementation retains more constraints in principal types than does the instance chain implementation. We propose a general mechanism to guide the instantiation of ambiguous type variables, show that it eliminates the need for type annotations in our encodings, and discuss conditions under which it preserves coherence.

The expression problem. Wadler [18] proposed the expression problem as a benchmark for language expressiveness and modularity. The starting point is the definition by cases of a data type for arithmetic expressions, and an operation over that data type. For example, the data type might contain simple arithmetic expression, and the operation might be evaluation. The challenge is to extend the data type with new cases and new operations, reusing the original code (without modification), and preserving static type safety. This framing of the expression problem may seem artificial. However, similar problems arise regularly in domains such as compilation. For example, in implementing a Haskell compiler, we might want to desugar surface language constructs, like special syntax for tuples, into a core syntax with uniform notation for constructors. The type of such a pass much capture the effect of the pass (removing tuple syntax) and its requirements (the core syntax), but should not otherwise fix the AST. The encodings we present allow such typing; concretely, the pass would have the type

Categories and Subject Descriptors D.3.3 [Programming Languages]: Language Constructs and Features—Abstract data types Keywords extensible variants; row types; expression problem

1.

Introduction

Modularity is a central problem in programming language design, and good modularity support has many benefits. Good modularity support improves extensibility and code reuse, saving programmer effort and reducing the likelihood of bugs or infelicities in reimplemented functionality. It also provides for separation of concerns,

(Core < (e Tuple)) ⇒ Fix e → Fix (e Tuple)

where the < constraint requires that the result type include the Core cases, and the type operator denotes removing cases from a type.

Permission to make digital or hard copies of all or part of this work for personal or classroom use is granted without fee provided that copies are not made or distributed for profit or commercial advantage and that copies bear this notice and the full citation on the first page. Copyrights for components of this work owned by others than ACM must be honored. Abstracting with credit is permitted. To copy otherwise, or republish, to post on servers or to redistribute to lists, requires prior specific permission and/or a fee. Request permissions from [email protected]. Copyright is held by the owner/author(s). Publication rights licensed to ACM.

Implementing variants. Though definition of types by cases is standard in both functional and object-oriented languages, the expression problem is challenging in either paradigm. In many functional languages, adding new cases to an existing data type requires changing the definition of the data type, and thus the functions that use it. In many object-oriented languages, adding new operations requires changing the definition of the base class, and thus its subclasses.

Haskell’15, September 3-4, 2015, Vancouver, BC, Canada ACM. 978-1-4503-3808-0/15/09...$15.00 http://dx.doi.org/10.1145/2804302.2804320

71

There are at least two approaches to solving the expression problem in functional languages. The first approach, row typing [5, 14, 15, 19], relies on an extension to the type system specific to representing extensible records and variants. The second approach represents variants using generic binary coproduct and fixed point type constructors, and relies on overloading to generalize injection and branching operations from the binary to the general case [1, 17]. This paper develops a new encoding of extensible variants, based on the latter approach. Our approach differs from previous encodings in several ways. We permit the use of arbitrarily structured coproducts in both introduction and elimination of extensible variants, lifting technical restrictions present in many previous encodings. More significantly, we introduce a overloaded branching combinator, which can be seen as generalizing the categorical notion of the unique arrow from a coproduct. Unlike previous encodings, our approach does not require that elimination of an extensible variants be defined using top-level constructs (like type classes), and assures that elimination expressions cover all cases (unlike projection-based approaches to variant elimination). We give two implementations of our approach: one using instance chains [12], a proposed extension of the Haskell class system, and a somewhat more verbose implementation using closed type families [3], an existing feature of GHC.

2.

Rows and Variants

2.1

Row Typing and Qualified Types

Wand [19] introduced row types as a mechanism to type objects with inheritance. In his approach, the language of types is extended with rows, or sequences of labeled types `1 : τ1 , . . . , `n : τn . Records and variants are constructed from rows; a record of type Π(`1 : τ1 , . . . , `n : τn ) has fields `1 through `n with corresponding types, while a variant of type Σ(`1 : τ1 , . . . , `n : τn ) is given by one of the labels `i and a value of type τi . Wand introduced row variables ρ to permit polymorphism in row-typed operations. For example, the injection operator for a label ` would have the type α → Σ(ρ[` ← α]), where α ranges over types, ρ ranges over rows, and ρ[` ← α] denotes the result of adding (or replacing) label ` with type α to ρ. Wand provides a branching combinator of type (α → β) → β → Σ(ρ[` ← α]) → β, where the second argument is a default (or else) branch. Wand’s types do not track those labels not present in rows; thus, the type ρ[` ← τ ] may either add a new pair ` : τ to ρ or replace an existing pair ` : τ 0 . As a consequence, some programs in his calculus do not have principal types. Rémy [14, 15] proposed a variant of Wand’s system that associates labels with flags rather than with types directly; each flag φ is either pre(τ ), indicating that the label is present with type τ , or abs, indicating that the label is absent. For example, in Rémy’s calculus the injection function for label ` has type α → Σ(` : pre(α); ρ), indicating that label ` must be present in the result type, and the branching combinator for label `, case` , is given the type

Evaluating encodings. There is, of course, a cottage industry in encoding language features via increasingly cunning use of type classes. A critical question when evaluating any such encoding is how closely the encoding matches the original language feature. We examine how closely our encodings match approaches based on row types. While our system is sufficient to encoding arbitrary introduction and elimination of extensible variants, losing no expressiveness compared to row-based systems, the same is not true of the composition of introductions and eliminations. We identify a typing ambiguity that appears in all the encodings we know of, not just in ours, requiring the programmer to provide explicit type annotations not required by row type systems. Resolving this ambiguity requires the compiler to make seemingly arbitrary choices of type instantiation during type checking; we propose a new mechanism to guide this choice, and discuss the conditions under which the use of this mechanism does not cause incoherence in the resulting programs.

(α → γ) → (Σ(` : abs; ρ) → γ) → Σ(` : pre(α); ρ) → γ, where in each case ` : φ; ρ denotes the extension of row ρ with the pair ` : φ, and is defined only if ρ does not already contain some type labeled by `. Note the refinement compared to how branching is typed in Wand’s calculus: in the expression case` M N P we can assume that option ` is not present in the argument to N. Gaster and Jones [5] propose a variant of row typing that represents negative information using predicates on (row) types. As a consequence, their system captures the expressiveness of Rémy’s system but can use a simpler form of row types. For example, the injection operator in their system has type

Contributions. In summary, this paper contributes:

(ρ \ `) ⇒ α → Σ(` : α; ρ)

• A new approach to encoding extensible variants in Haskell,

and their branching operator has type

based on overloaded injection and branching combinators;

(ρ \ `) ⇒ (α → γ) → (Σ(ρ) → γ) → Σ(` : α; ρ) → γ,

• Implementations of this approach using instance chains and

closed type families; and,

where in each case the constraint ρ \ ` is satisfiable only if ρ does not contain a label `. Unlike Rémy’s approach, the system of Gaster and Jones does not need flags, and does not impose nonduplication constraints on the formation of rows. As it builds on Jones’s system of qualified types [6], Gaster and Jones’s system enjoys principal types, type inference, and easy integration with type classes and other features expressible with qualified types. Two properties of their type system are central to their principality and type inference results. First, like other row type systems, they consider types equivalent up to rearrangement of rows. Second, they show that, in addition to most general unifiers, they can compute most general inserters, or the most general substitutions for row variables than guarantee the inclusion of particular labeled types.

• A comparison of these systems with each other and with row

type systems, and a proposed language mechanism to address the expressiveness gap between them. To that end, we begin by describing existing approaches to providing extensible variants in functional languages, based on row types or overloaded injection functions (§2). We then describe our approach, and implement it using instance chains (§3). We show how our approach can be used to solve the expression problem, and show how it can give precise types to desugaring steps in programming language implementations. We compare our approach to systems built on row types (§4). We conclude that all the existing approaches to encoding extensible variants in Haskell suffer from typing ambiguities, requiring programmers to add type annotations not required by row type systems, and propose a simple mechanism to eliminate the need for such annotations. We then translate our implementation of extensible variants to use closed type families instead of instances chains (§5). This translation illustrates the similarities and differences between the two language mechanisms. We conclude by discussing related (§6) and future (§7) work.

2.2

Modular Interpreters and Data Types à la Carte

Wand originally introduced row types as a generalization of binary products and coproducts. An alternative approach to extensible variants is to use binary coproducts directly, but to generalize the injection and branching operators. Systems based on this approach differ from row-typing approaches in two ways. First, they tend

72

not to rely on labeling types. With the addition of suitable typelevel machinery for labels, however, they can be straightforwardly adapted to work on labeled types. Second, binary coproducts are not identified up to associativity and commutativity. Thus, a central concern for these systems is not introducing distinctions among equivalent (but rearranged) coproduct types. Liang et al. [10] gave an early example of this approach, as part of describing a modular approach to building language interpreters. They represent larger types as (right-nested) coproducts of smaller types; for example, a term language including arithmetic (TermA) and functional (TermF) terms would be described by OR TermA (OR TermF ()) (where OR is their coproduct type constructor). They define a type class SubType to simplify working with coproducts; SubType τ υ holds if υ is a right-nested coproduct and τ is one of its left-hand sides; it provides methods inj :: τ → υ to inject values of component types into the coproduct type and prj :: υ → Maybe τ to project values of component types from values of the coproduct type. For example, their system would provide functions inj :: TermA → OR TermA (OR TermF ()) prj :: OR TermA (OR TermF ()) → Maybe TermF Liang et al. define type classes for operations on variant types, such as interpretation, with instances for each term type and a generic instance for coproducts, wrapping the use of prj. Their approach does not directly address extensible variants: recursion is hard-wired into the term types. Swierstra [17] proposed another approach to extensible variants in Haskell, which he called “Data Types à la Carte”. He defines variants by combining binary coproducts with Sheard and Pasalic’s [16] approach to open recursion (or “two-level types”). Consequently, individual cases in his approach are functors, rather than ground types, in which the functor’s argument is used for recursive cases. Similarly, rather than defining coproducts of ground types, he defines coproducts of functors (written f ⊕ g). Finally, he uses a fixed point constructor Fix to construct types from functors. For example, in his system the types TermA (for arithmetic expressions) and TermF (for functional expressions) would be functors, and the combined expression type would be written Fix (TermA ⊕ TermF). Like Liang et al., Swierstra defines a class, called (:≺:), to generalize injection into (right-nested) coproducts. His (:≺:) class defines an injection function but not a projection function; he relies on type classes to implement functions that consume variants. Thus, his system provides functions like inj :: TermF e → (TermA ⊕ TermF) e Unlike the SubType class, (:≺:) is reflexive, and so can have inj :: TermF e → TermF e This avoids the need for “terminator” like () in the types of Liang et al. As a consequence, however, Swierstra’s instances are ambiguous for predicates of the form (f ⊕ g) :≺: h. Bahr [1] gives an extension of Swierstra’s approach and an implementation using closed type families. He follows Liang et al. in giving a version of the subtype class that provides both injection and projection operators; thus, his encoding does not require each elimination of extensible variants to be defined by a new type class. However, the projection-based approach does not guarantee that pattern matches are complete. Bahr finds an interesting solution to this problem. He defines his injection function with sufficient generality that he can use it to permute the structure of coproducts, and defines a split operator that rearranges its argument to surface desired cases. By then using the standard Haskell case construct on the results of split, Bahr can define extensible but complete branching. His approach to defining split introduces ambiguity, however, requiring the programmer to add explicit type signatures or proxy arguments.

1 2

data Fix e = In (e (Fix e)) data (f ⊕ g) e = Inl (f e) | Inr (g e)

3 4 5 6

(O) :: (f e → a) → (g e → a) → (f ⊕ g) e → a (f O g) (Inl x) = f x (f O g) (Inr x) = g x

Figure 1: Data types for variants and recursion.

1 2 3

data Const e = Const Int = Plus e e data Sum e data Product e = Times e e

4 5 6 7

type E1 = Fix (Const ⊕ Sum) type E1' = Fix (Sum ⊕ Const) type E2 = Fix ((Const ⊕ Sum) ⊕ Product)

Figure 2: Expression constructors and expression types

3.

Extensible Variants with Instance Chains

In this section, we describe another approach to encoding extensible variants. We begin from the same coproduct and fixed point constructors used by Swierstra [17]. However, our approach differs from his in two important ways. First, we define a more expressive inclusion class (