To save content items to your account,
please confirm that you agree to abide by our usage policies.
If this is the first time you use this feature, you will be asked to authorise Cambridge Core to connect with your account.
Find out more about saving content to .
To save content items to your Kindle, first ensure no-reply@cambridge.org
is added to your Approved Personal Document E-mail List under your Personal Document Settings
on the Manage Your Content and Devices page of your Amazon account. Then enter the ‘name’ part
of your Kindle email address below.
Find out more about saving to your Kindle.
Note you can select to save to either the @free.kindle.com or @kindle.com variations.
‘@free.kindle.com’ emails are free but can only be saved to your device when it is connected to wi-fi.
‘@kindle.com’ emails can be delivered even when you are not connected to wi-fi, but note that service fees apply.
Chapter 10 considered functional programming with data involving names and name abstractions. The new language features introduced in that chapter were motivated by the theory of nominal sets introduced in Part One of the book. We encouraged the reader to think of the type and function expressions of the FreshML functional programming language as describing nominal sets and finitely supported functions between them. However, just as for conventional functional programming, the sets-and-functions viewpoint is too naive, because of the facilities these languages provide for making recursive definitions. Giving a compositional semantics for such language features requires solving fixed-point equations both at the level of types (for recursively defined data types) and the level of expressions of some type (for recursively defined functions). As is well known, solutions to these fixed-point equations cannot always be found within the world of sets and totally defined functions; and this led the founders of denotational semantics to construct mathematical models of partially defined objects, functions, functionals, etc., based on a fascinating mixture of partial order, topology and computation theory that has come to be known as domain theory. For an introduction to domain theory we refer the reader to Abramsky and Jung (1994).
In this chapter we consider merging domain theory with the concepts from nominal sets – names, permutations, support and freshness. As a result we gain new forms of domain, in particular domains of name abstractions.
A very useful feature of functional programming languages such as OCaml (http://caml.inria.fr/ocaml) or Haskell (http://www.haskell.org) is the facility for programmers to declare their own algebraic data types and to specify functions on that data using pattern-matching. This makes them especially useful for metaprogramming, that is, writing programs that manipulate programs, or more generally, expressions in formal languages. In this context the functional programming language is often called the meta-level language, while the language whose expressions appear as data in the functional programs is called the object-level language. We already noted at the beginning of Chapter 8 that object-level languages often involve name binding operations. In this case we may well want meta-level programs to operate not on object-level parse trees, but on their α-equivalence classes. OCaml or Haskell programmers have to deal with this issue on a case-by-case basis, according to the nature of the object-level language being implemented, using a selfimposed discipline. For example, they might work out some ‘nameless’ representation of α-equivalence classes for their object-level language, in the style of de Bruijn (1972). When designing extensions of OCaml or Haskell that deal more systematically with this issue, three desirable properties come to mind:
• Expressivity. Informal algorithms for manipulating syntactic data very often make explicit use of the names of bound entities; when representing α-equivalence classes of object-level expressions as meta-level data, one would still like programmers to have access to object-level bound names.
This book has its origins in my interest in semantics and logics for locality in programming languages. By locality, I mean the various mechanisms that exist for making local declarations, restricting a resource to a specific scope, or hiding information from the environment. Although mathematics and logic are involved in understanding these things, this is a distinctively computer science topic. I was introduced to it by Matthew Hennessy and Alley Stoughton when we all arrived at the University of Sussex in the second half of the 1980s. At the time I was interested in applying category theory and logic to computer science and they were interested in the properties of the mixture of local mutable state and higher-order functions that occurs in the ML family of languages (Milner et al., 1997).
Around that time Moggi introduced the use of category-theoretic monads to structure different notions of computational effect (Moggi, 1991). That is now an important technique in denotational semantics; and thanks to the work of Wadler (1992) and others, monads are the accepted way of ‘tackling the awkward squad’ (Peyton Jones, 2001) of side-effects within functional programming. One of Moggi's monads models the computational effect of dynamically allocating fresh names. It is less well known than some of the other monads he uses, because it needs categories of functors and is only mentioned in (Moggi, 1989), rather than (Moggi, 1991).
For building large systems, it is essential to build them from components and use module abstraction, that is, abstraction of both data and control in modules or components, to separate what functionalities are provided by a component from how the functionalities are implemented inside the component. Module abstraction is best supported as abstract data types. An abstract data type is an interface for a certain set of operations on a certain kind of data, that is, the “what”, shielding users from having to know how the data are represented and how the operations are implemented, that is, the “how”. It is a fundamental concept in modern high-level programming languages, particularly object-oriented languages.
Unfortunately, clear and modular implementations of the modules or components result in poor performance when nontrivial query operations are frequently performed and values of query parameters are gradually updated. At the same time, efficient implementations that incrementally maintain the query results with respect to updates to parameter values are much more difficult to develop and to understand, because the code grows significantly and is no longer clear or modular. Because the definitions and uses of queries and updates can cross multiple components, transforming clear implementations of the queries into efficient incremental implementations requires incrementalization across module abstraction.
We have shown a systematic method that succeeds in designing efficient implementations for many problems in many application domains starting with clear specifications of these problems using high-level language constructs. The method is systematic by being based on the language constructs used in the specifications and being guided by the cost considerations taken from the application domains.
The method, even though consisting of Steps Iterate, Incrementalize, and Implement in order, is driven by the middle step—Step Incrementalize. Because efficient computations on nontrivial input must proceed repeatedly on incremented input, Step Incrementalize aims to make the computation on each incremented input efficient by storing and reusing values computed on the previous input. Steps Iterate and Implement are enabling mechanisms: to maximize reuse by Step Incrementalize, Step Iterate determines a minimum input increment to take repeatedly; to support efficient access of the stored values by Step Incrementalize, Step Implement designs a combination of linked and indexed data structures to hold the values.
We first take a deeper look at incrementalization, showing how to systematically exploit the previous result, intermediate results, and auxiliary values for incremental computation. This will use simple examples specified using recursive functions, followed by several different sorting programs as additional examples. We then discuss abstractions in general, focusing on the importance of and principles for not only building up, but also breaking through, abstractions. The latter may sound surprising at first, but it is natural when query functions are incrementalized with respect to updates.
Many complex computational problems are most clearly and easily specified using logic rules. Logic rules state that if certain hypotheses hold then certain conclusions hold. These rules can be used to infer new facts from given facts. Example applications include queries in databases, analysis of computer programs, and reasoning about security policies. Datalog, which stands for Database logic, is an important rule-based language for specifying how new facts can be inferred from existing facts. Its fixed-point semantics allows the computation of the set of all facts that can be inferred from a given set of facts. It is sufficiently powerful for expressing many practical analysis problems.
While a Datalog program can be easily implemented using a logic programming system, such as a Prolog system, evaluated using various evaluation methods, such as well-established tabling methods, or rewritten using transformation methods for more efficient evaluation, such as using the well-known magic-sets method, these implementations are typically for fast prototyping. The running times of Datalog programs implemented using these methods can vary dramatically depending on the order of rules and the order of hypotheses in a rule. Even less is known about the space usage. Developing and implementing efficient algorithms specialized for any given set of rules and with time and space guarantees is a nontrivial, recurring task.
We first look at computational problems programmed using loops over numbers and arrays. A loop is a command for repeating a sequence of operations. A number is an integer or a real number with an internal representation in a computer. An array is an arrangement of computer memory elements in one or more dimensions. Problems involving arithmetic on numbers were the first problems for which computing devices were built. Problems involving operations on arrays were at the center of many subsequent larger computer applications. Because nontrivial computations involve performing operations iteratively, loops are a most commonly used, most important construct in programming solutions to problems.
Clear and straightforward problem solutions tend to have expensive computations in loops, where the values that these computations depend on are updated slightly in each iteration. To improve efficiency, the results of these computations can be stored and incrementally maintained with respect to updates to the values that they depend on. The transformation of programs to achieve this is called incrementalization.
We will use a small example and several variations to explain the basic ideas of incrementalization. We then describe two larger examples: one in hardware design, to show additional loop optimizations enabled by incrementalization, and the other in image processing, to show handling of nested loops and arrays in incrementalization. We discuss the need for higher-level languages at the end.