More Memory

A simple memory example

Let’s start with a simpler dataclass than the TodoItems we saw last time:

@dataclass
class Posn:
    x: int
    y: int

char1 = Posn(0, 0)
char2 = char1
rock = Posn(1, 1)

In our old model, we’d end up with a program dictionary like this:

name value
char1 Posn(x=0, y=0)
char2 Posn(0, 0)
rock Posn(1, 1)

In our new model, we have both a dictionary and memory:

Dictionary name value
  char1 loc 1
  char2 loc 1
  rock loc 2
Memory location value
  loc 1 Posn(x=0, y=0)
  loc 2 Posn(x=1, y=1)

The dictionary, as in our previous model, tracks the names we have defined. Memory, meanwhile, tracks the actual data we have.

Let’s see a few examples of how statements execute in this new programming model.

Lookups

> print(char2.y)

In order to evaluate this statement, we’ll first look up char2 in the dictionary. It’s pointing at loc 1, so we’ll look at that location in memory. We’re looking at the y field, so we look up that field in the value at loc 2 and find 0.

Updates (1)

> char1.x = 1

We first look up char1 in the dictionary and find loc 1. We then change the x field of the Posn at loc 1 to 1.

Updates (2)

> char2 = Posn(0, 0)

We’re building a new Posn, so we’ll need to add it to memory at loc 3. Then, we change the dictionary entry of char2 to point at this new location. We’re not changing the contents of char2–we’re changing what the name points to!

Update rules

  • We add to memory when a data constructor is used
  • We update memory when a field of existing data is reassigned
  • We add to the dictionary when a name is used for the first time (this includes parameters and internal variables when a function is called)
  • We update the dictionary when a name that is already in the dictionary is reassigned to a different value

Atomic values

Some values are atomic–they don’t have components, and can’t be modified in place. These values include numbers, booleans, and strings. When variables are bound to these values, we record them directly in the program dictionary.

> a = 2
> b = 3
> char3 = Posn(a, b)
Dictionary name value
  char1 loc 1
  char2 loc 3
  rock loc 2
  a 2
  b 3
  char3 loc 4
Memory location value
  loc 1 Posn(x=1, y=0)
  loc 2 Posn(x=1, y=1)
  loc 3 Posn(x=0, y=0)
  loc 4 Posn(x=2, y=3)

The move function

Let’s write a function to change a Posn:

def move(p:  Posn, dx: int, dy: int):
  """adds dx to p.x and dy to p.y"""

How would we write this function? Here are two possibilities:

def move(p:  Posn, dx: int, dy: int):
  """adds dx to p.x and dy to p.y"""
  p.x = p.x + dx
  p.y = p.y + dy
def move_wrong(p:  Posn, dx: int, dy: int):
  """adds dx to p.x and dy to p.y"""
  p = Posn(p.x + dx, p.y + dy)

As its name suggests, move_wrong is the wrong way to do what we’re trying to do. To see why, let’s step through the program dictionary as this program executes. (See lecture capture for details).

> car = Posn(0, 0)
> move_wrong(car, 5, 10)
> print(car.x)
0

In-class exercise

As practice with these concepts, write out the program dictionary and memory after we execute the following code.

car = Posn(0, 0)
me = car
move(car, 5, 5)

Lists and memory

Are lists atomic values?

Based on what we’ve already learned, they must not be–we can modify them in place! So, what happens when we create a list–say, a list of Posn’s?

> trees = [Posn(1, 3), Posn(3, 6)]
Dictionary name value
  trees loc 1
Memory location value
  loc 1 [loc 2, loc 3]
  loc 2 Posn(x=1, y=3)
  loc 3 Posn(x=3, y=6)