3

I have a specification where I am attempting to define an LRU Cache system and one of the problems I am running into is how I can go about deleting values from a structure key/value pairing (which is basically a dictionary or a hash-map in other languages).

Here is the specification itself so far (incomplete):

EXTENDS Integers, Sequences
VARIABLES capacity, currentSize, queue, dictionary

Init == /\ (capacity = 3 ) /\ (currentSize = 0)
        /\ (queue = <<>>) /\ (dictionary = [])


AddItem(Item, Value) == IF currentSize < capacity
            THEN /\ currentSize' = currentSize + 1
                 /\ queue' = Append(queue, Item)
                 /\ dictionary[item] = value
            ELSE /\ queue' = Append(Tail(queue), Item) 
                 /\ dictionary' = [x \in dictionary: x /= queue[3]]

GetItem(Item) == dictionary[item]

Next == \/ AddItem 
        \/ GetItem

I was referencing this documentation on the Learn TLA Plus website but there seems to be nothing on removing a key value pair from the list. So far the only thing I could think to do would be to filter out the value that matches the key and create a new dictionary object, but I would prefer a method with a little more direct access.

0 _
  • 10,524
  • 11
  • 77
  • 109
Q.H.
  • 1,406
  • 2
  • 17
  • 33
  • The guide is misleading in how it explains structures, raised an issue here: https://github.com/hwayne/learntla/issues/41 – Hovercouch Nov 05 '17 at 04:52

2 Answers2

11

Before I can answer, I have to ask another question: what do you mean by 'delete a value'? Remember, TLA+ isn't a programming language: it's a specification language. That means that it's built on having a very clear understanding of what you're doing. So let's talk about deletion.

The only two complex collections in TLA+ are sets and functions. A function maps some set of elements, its domain, to values. Structures and sequences are just syntactic sugar over functions: a structure's domain is its fixed keys, while a sequence's domain is 1..n, n \in Nat. Functions need to have a domain. If you want to 'delete' a key from a structure, you need 'delete' it from the structure's domain.

A corresponding action is taking the tail of a sequence. Lamport defines that on page 341 of Specifying Systems, which is included in the TLA+ Toolbox. Here's the definition (from the standard module Sequences.tla--slightly modified in the linked version):

Tail(seq) == [i \in 1 .. (Len(seq) - 1) |-> seq[i + 1]]

In other words, the tail of a sequence is defined by constructing the sequence offset by one, with the last element removed. Removing something from the domain of a function is 'done' by constructing an identical function, sans that one element in its domain. Which makes sense when you think about it: we can't mutate a mathematical function any more than we can mutate the number 7.

Then again, we need to construct new functions off existing ones enough that TLA+ adds some convenience syntax. For changing a single mapping in a function, we have EXCEPT. For adding a new mapping, the TLC module adds the @@ operator. Deleting a mapping is generally not something people do, so we'd have to define it ourselves. You'd have to do something like

dictionary' = [x \in DOMAIN dictionary \ {item} |-> dictionary[x]]

Note that your way of adding to a dictionary is wrong: dictionary[item] = value is an equality check, not an assignment. dictionary'[item] = value doesn't work either, because it doesn't completely specify dictionary. You'd have to do something like

dictionary' = [x \in {item} |-> value] @@ dictionary

(or use :>, also in TLC module)

At this point it might taste like we're going the wrong way, there's probably a simpler way to specify a changing dict. I'm guessing your spec doesn't depend on some implementation detail of your keys: you don't expect your cache to change behavior if you use strings instead of integers as keys. In that case, I'd specify an arbitrary set of keys and values, which lets us define the mutations like this:

CONSTANTS Keys, Values, NULL
VARIABLE dict \* [key \in Keys |-> NULL]

Add(dict, key, val) == [dict EXCEPT ![key] = val]
Del(dict, key, val) == [dict EXCEPT ![key] = NULL]

Where dict[key] = NULL represents that key not being in the dictionary.

This is generally one of the reasons I recommend PlusCal for beginners, because then you don't have to worry about how to mutate functions while learning the basics of specification. If you're writing a pluscal algorith, you can mutate the dict with dict[key] := val.

0 _
  • 10,524
  • 11
  • 77
  • 109
Hovercouch
  • 1,962
  • 12
  • 12
3

Adding to @Hovercouch's answer: "deleting" does not correspond to what a TLA+ specification means, unlike programming languages.

Declaring dictionary as a VARIABLE says that dictionary is an identifier of an operator that takes no arguments (nullary), and can change value over a behavior ("over time"). It doesn't say anything else.

Looking at step (pair of consecutive states in a behavior), the value of dictionary (in the first state), and of dictionary' (of dictionary in the second state) are unrelated [1, p.313]. Only through constraints expressed in the specification can we restrict their values.

If in a state dictionary = [x \in {"foo", "bar"} |-> x], then dictionary' can be anything (any value in ZF, i.e., any set). See [1, Sec. 6.5 on p.72, Sec. "Don't be..." on p.80, pp.139--140].

The value of the state predicate

dictionary["foo"] = "foo"

at such a state is TRUE, because indeed dictionary maps the value "foo" to the value "foo". In contrast, the state predicate:

dictionary["foo"] = "bar"

is FALSE, because "foo" # "bar". If at a step with that state as first one, we write (see also [1, p.82]):

(dictionary["foo"] = "foo")'

we are saying only that the expression dictionary["foo"] equals "foo" in the next state. We haven't said that dictionary is a function in that state---just that it is such a value that dictionary["foo"] happens to equal "foo". Perhaps dictionary isn't a function there.

What is a function?

When do we call a given value f a "function"? The answer is when that f happens to satisfy the formula IsAFunction(f), where the operator IsAFunction is defined as [1, p.303]:

IsAFunction(g) == g = [x \in DOMAIN g |-> g[x]]

Tuples ("arrays") are functions with domain 1..n for some n \in Nat. In TLA+, functions are simply values with the above property. Since any value in ZF is a set, functions are sets, in that we can still write y \in f when f is a function [1, above Sec. 4.5 on p.43, and Sec 3.3 on p.30].

For example, with the theorem prover TLAPS we can prove the following theorem and check the proof using Isabelle by running tlapm -v -C test.tla:

---- MODULE test ----
EXTENDS TLAPS

f == [x \in {} |-> x]
g == [x \in {1, 2} |-> 3]

THEOREM
    LET
        S == f \cup {g}
    IN
        (1 \in f) => (1 \in S)
    PROOF BY Zenon
    (* you can replace this with OBVIOUS *)
=====================

(The model checker TLC enumerates states, and because we don't know what elements are contained in a set that happens to be a function, TLC cannot evaluate the expression y \in f, raising an Error: Attempted to check if the value: ... is an element of the function .... The syntactic analyzer SANY confirms that the above module is well-formed.)

We can also write f[x] when f isn't a function or when x \notin DOMAIN f. The problem is that in those cases the value of f[x] is unspecified, so a specification shouldn't depend on what those values are.

Comments on the specification

The expression dictionary = [] is not part of TLA+. To write the function with empty domain (there is exactly one such function, see the axiom about function extensionality [1, p.303]):

dictionary = [x \in {} |-> TRUE]

I would declare capacity as a CONSTANT, because it is intended to remain unchanged over a behavior (unless of course it changes). Also, currentSize isn't decreased by the spec, but I assume that this hasn't been added yet.

Also worth noting that the dictionary will map each item to a unique value, whereas the queue can end up containing multiple copies of the same item. Not sure what the OP intends for that case.

EXTENDS Integers, Sequences


CONSTANT capacity
VARIABLES currentSize, queue, dictionary


Init == /\ (capacity = 3 ) /\ (currentSize = 0)
        /\ (queue = <<>>) /\ (dictionary = [x \in {} |-> TRUE])

AddItem(Item, Value) ==
    IF currentSize < capacity

        (* Overwrite to what value the dictionary maps
        the value. *)
        THEN /\ currentSize' = currentSize + 1
             /\ queue' = Append(queue, Item)
             /\ dictionary' =
                     [x \in DOMAIN dictionary |->
                         IF x = Item
                             THEN Value
                             ELSE dictionary[x]]

        (* Remove the leading item from the dictionary's domain,
        then add the given item (perhaps the same), and
        map it to the given value. *)
        ELSE /\ queue' = Append(Tail(queue), Item) 
             /\ LET
                   (* It can happen that OldDom = NewDom. *)
                   OldDom == DOMAIN queue
                   first == queue[1]
                   TailDom == OldDom \ { first }
                   NewDom == TailDom \cup {Item}
                IN
                   dictionary' =
                       [x \in NewDom |->
                           IF x = Item
                               THEN Value
                               ELSE dictionary[x]]

GetItem(Item) == dictionary[item]

Next == \/ AddItem 
        \/ GetItem

The expression

dictionary' = [x \in DOMAIN dictionary |->
               IF x = Item THEN Value ELSE dictionary[x]]

can be replaced by [1, p.49]

dictionary' = [dictionary EXCEPT ![Item] = Value]

The expression

dictionary' = [x \in NewDom |->
               IF x = Item THEN Value ELSE dictionary[x]]

cannot be replaced with EXCEPT.

References

[1] Leslie Lamport, "Specifying systems", Addison-Wesley, 2002

0 _
  • 10,524
  • 11
  • 77
  • 109
  • "Since any value in ZF is a set, functions are sets, in that we can still write y \in f when f is a function" Correct me if I'm wrong, but is TLC able to model-check that? I just tried it and it gave an error. – Hovercouch Nov 05 '17 at 17:06
  • 1
    Any value is a set, including values `f` that happen to satisfy `IsAFunction(f)`. The [set membership](https://en.wikipedia.org/wiki/Element_(mathematics)) operator `_ \in _` applies to any two arguments (which are sets--there isn't anything else than sets). So we can write `y \in f` (see p.43 and p.30). TLC cannot enumerate this because we don't know the elements of `f`. The theorem prover TLAPS can prove theorems that involve such formulas (I added an example above), and the syntactic analyzer SANY confirms that the example is syntactically well-formed. – 0 _ Nov 05 '17 at 17:57
  • I have quite a bit of reading to do. l thought implementing an LRU Cache would be a simple example to do a spec for but this shows I still have no idea what I'm doing. – Q.H. Nov 05 '17 at 21:35