Maps (or dictionaries) are ubiquitous data structures both generally and in the theory of programming languages in particular; we're going to need them in many places in the coming chapters. They also make a nice case study using ideas we've seen in previous chapters, including building data structures out of higher-order functions (from Basics and Poly) and the use of reflection to streamline proofs (from IndProp).
We'll define two flavors of maps: total maps, which include a
default
element to be returned when a key being looked up
doesn't exist, and partial maps, which return an option to
indicate success or failure. The latter is defined in terms of
the former, using None as the default element.
One small digression before we begin...
Unlike the chapters we have seen so far, this one does not Require Import the chapter before it (and, transitively, all the earlier chapters). Instead, in this chapter and from now, on we're going to import the definitions and theorems we need directly from Coq's standard library stuff. You should not notice much difference, though, because we've been careful to name our own definitions and theorems the same as their counterparts in the standard library, wherever they overlap.
Documentation for the standard library can be found at http://coq.inria.fr/library/.
The Search command is a good way to look for theorems involving objects of specific types. Take a minute now to experiment with it.
First, we need a type for the keys that we use to index into our maps. In Lists.v we introduced a fresh type id for a similar purpose; here and for the rest of Software Foundations we will use the string type from Coq's standard library.
To compare strings, we define the function eqb_string, which internally uses the function string_dec from Coq's string library.
(The function string_dec comes from Coq's string library.
If you check the result type of string_dec, you'll see that it
does not actually return a bool, but rather a type that looks
like {x = y} + {x <> y}, called a sumbool, which can be
thought of as an evidence-carrying boolean.
Formally, an
element of sumbool is either a proof that two things are equal
or a proof that they are unequal, together with a tag indicating
which. But for present purposes you can think of it as just a
fancy bool.)
Now we need a few basic properties of string equality...
The following useful property follows from an analogous lemma about strings:
Similarly:
This handy variant follows just by rewriting:
Our main job in this chapter will be to build a definition of partial maps that is similar in behavior to the one we saw in the Lists chapter, plus accompanying lemmas about its behavior.
This time around, though, we're going to use functions, rather
than lists of key-value pairs, to build maps. The advantage of
this representation is that it offers a more extensional view of
maps, where two maps that respond to queries in the same way will
be represented as literally the same thing (the very same function),
rather than just equivalent
data structures. This, in turn,
simplifies proofs that use maps.
We build partial maps in two steps. First, we define a type of total maps that return a default value when we look up a key that is not present in the map.
Intuitively, a total map over an element type A is just a function that can be used to look up strings, yielding As.
The function t_empty yields an empty total map, given a default element; this map always returns the default element when applied to any string.
More interesting is the update function, which (as before) takes a map m, a key x, and a value v and returns a new map that takes x to v and takes every other key to whatever m does.
This definition is a nice example of higher-order programming: t_update takes a function m and yields a new function fun x' => ... that behaves like the desired map.
For example, we can build a map taking strings to bools, where "foo" and "bar" are mapped to true and every other key is mapped to false, like this:
Next, let's introduce some new notations to facilitate working with maps.
First, we will use the following notation to create an empty total map with a default value.
We then introduce a convenient notation for extending an existing map with some bindings.
The examplemap above can now be defined as follows:
This completes the definition of total maps. Note that we don't need to define a find operation because it is just function application!
To use maps in later chapters, we'll need several fundamental facts about how they behave.
Even if you don't work the following exercises, make sure you thoroughly understand the statements of the lemmas!
(Some of the proofs require the functional extensionality axiom, which is discussed in the Logic chapter.)
First, the empty map returns its default element for all keys:
Next, if we update a map m at a key x with a new value v and then look up x in the map resulting from the update, we get back v:
On the other hand, if we update a map m at a key x1 and then look up a different key x2 in the resulting map, we get the same result that m would have given:
If we update a map m at a key x with a value v1 and then update again with the same key x and another value v2, the resulting map behaves the same (gives the same result when applied to any key) as the simpler map obtained by performing just the second update on m:
For the final two lemmas about total maps, it's convenient to use the reflection idioms introduced in chapter IndProp. We begin by proving a fundamental reflection lemma relating the equality proposition on ids with the boolean function eqb_id.
Use the proof of eqbP in chapter IndProp as a template to prove the following:
Now, given strings x1 and x2, we can use the tactic destruct (eqb_stringP x1 x2) to simultaneously perform case analysis on the result of eqb_string x1 x2 and generate hypotheses about the equality (in the sense of =) of x1 and x2.
With the example in chapter IndProp as a template, use eqb_stringP to prove the following theorem, which states that if we update a map to assign key x the same value as it already has in m, then the result is equal to m:
Use eqb_stringP to prove one final property of the update function: If we update a map m at two distinct keys, it doesn't matter in which order we do the updates.
Finally, we define partial maps on top of total maps. A partial map with elements of type A is simply a total map with elements of type option A and default element None.
We introduce a similar notation for partial maps:
We can also hide the last case when it is empty.
We now straightforwardly lift all of the basic lemmas about total maps to partial maps.