11.4 Cyclic Data
11.4.1 Creating Cyclic Data
Earlier [Aliasing], we introduced the idea of aliased bank accounts, where multiple customers can operate the same account. Sometimes, a bank wants to keep track of all the customers who have access to a given account. For instance, when the account balance runs low, it would want to notify all the customers who have access to it.
Python | Pyret |
|
|
Account
refers to
Customer
(specifically, a list of them) and in turn
Customer
refers to Account
. This could get interesting.Python | |
| |
Pyret | |
|
Account
requires a list of its
Customer
s. We need to write
Python | |
| |
Pyret | |
|
_____
? It needs to refer to the very customer
account that we are presently creating.Python | |
| |
Pyret | |
|
_____
—Python | |
| |
Pyret | |
|
acct1
. To do so, they must evaluate that
account-creation expression. In doing so, they look up the name
elena
. However, elena
has not yet been bound in the
directory. Therefore, they produce an error.Observe that we can’t just reverse the order of these two bindings. If
we try that, we end up with the same problem: we try to create a
customer that refers to acct1
. But we haven’t yet defined
acct1
, producing the same error.
The problem is we are trying to create cyclic data. The two data refer to one another. We could already sense that this might happen from the data definitions, and now we must confront it. The problem is that we have to create some value first, and we can’t produce either one correctly since each one depends on the other.
As you might guess, we have to compromise. We have to construct one of the data first, and when we do so, it might not be entirely accurate. Then we create the other, and then modify the first one to have the correct contents.
Customer
is mutable (nor needs
to be), whereas the list of account owners is (and should be, because
the set of customers can grow). Therefore, we can write:
Python | |
| |
Pyret | |
|
Python | |
| |
Pyret | |
|
Directory
acct1
→ 1001elena
→ 1002
Heap
1001:
Account(8404, 500, [])
1002:
Customer("Elena", 1001)
Python | Pyret |
|
|
elena
is bound in the
dictionary. Furthermore, it is bound to something useful: Elena’s
customer information. So now the values are properly set up: Elena’s
customer information refers to the account, and the account refers to
Elena’s customer information:
Directory
acct1
→ 1001elena
→ 1002
Heap
1001:
Account(8404, 500, [1002])
1002:
Customer("Elena", 1001)
Python | |
| |
Pyret | |
|
acct1
is inaccurate because it does
not reflect the new owner. We can modify it in a similar way:
Python | |
| |
Pyret | |
|
Directory
acct1
→ 1001elena
→ 1002jorge
→ 1003
Heap
1001:
Account(8404, 500, [1002, 1003])
1002:
Customer("Elena", 1001)
1003:
Customer("Jorge", 1001)
Do Now!
We wrote slightly different code when adding Jorge’s account than when adding Elena’s account. Is one better than the other?
The code for Elena’s addition ignored whatever owners there previously were. That is, it would only work correctly in a setting where there were no other owners. The code for Jorge’s addition takes into account all the previous owners. Therefore, Elena’s code was perfectly fine for illustrating the simple first case, but Jorge’s code is more general in that it will work in all settings (including when the prior list of owners is empty).
Exercise
Write a function that takes care of adding a customer to an account.
11.4.2 Testing Cyclic Data
acct1
from earlier:
Python | |
| |
Pyret | |
|
…
.This leaves us with two choices:
You have two options: write tests in terms of the names of data, or write tests on the components of the data.
Here’s an example that illustrates both. After setting up the account, we might want to check that the owner of the new account is the new customer:
assert(new_acct.owner is new_cust)
Here, rather than write out the Customer
explicitly, we use the name
of the existing item in the directory. This doesn’t require you to write
ellipses. We also focused on just the owner
component, as a part of
the Account
value that we expected to change.
11.4.3 Cycles in Practice
Cyclic data show up in many settings in real programs. Whenever two data are interrelated, and we have good reason to want to get from either one to the other, they have the potential to have references to each other, which can lead to cycles. Sometimes the connection can be to provide updates, as above; other times it can simply be for navigational convenience.
Consider the Document Object Model (DOM), which is the data structure that represents every Web page in a Web browser. Programmers usually think of the DOM hierarchically, as a tree, because every note refers to all the nodes that constitute it: e.g., a page has references to each of its paragraphs, a list has references to each list item, and so forth. However, every one of these elements also has a reference to its parent. This way, a program can conveniently traverse “downward” or “upward”.
Programming with cyclic data introduces complications. If we traverse the data naïvely, we would go into an infinite loop. Rather, we have to keep track of the data we have previously visited, and make sure we don’t visit them again (see The Size of a DAG). Indeed, cyclic data are graphs [Graphs], so issues in processing graphs become relevant here.
acct1
Do Now!
Try it out in both Python and Pyret!
Different programming languages handle this problem in different ways. Some languages will go into an infinite loop trying to print cyclic data. Both Python and Pyret handle this more intelligently. Determining even whether a datum is cyclic is an interesting question, which we take up in Detecting Cycles.