Class summary:   Data Structure and Program Structure
1 Organizing Game States
2 Program structure follows data structure
2.1 Lists of Lists
2.2 A Concrete Example
2.3 Critical Things to Notice
3 Study Questions

Class summary: Data Structure and Program Structure

Copyright (c) 2017 Kathi Fisler

1 Organizing Game States

We reviewed your thoughts on this problem from the lecture prep: which of these two data organizations makes more sense for a game with an alien and a robot?

  data GameState1:

    | robot(pos :: Posn)

    | alien(pos :: Posn)

  end

  

  data GameState2:

    | state(robot-pos :: Posn, alien-pos :: Posn)

  end

The first one says that the game state (which gathers the data for all game components) will be EITHER a robot or an alien. The second one says that the game state will contain BOTH. For a game state, we want the BOTH version. In other contexts, EITHER will be more appropriate. We’ll continue to see examples of this decision as we progress through the course.

2 Program structure follows data structure

Understanding the connection between the structure or shape of your data and that of your code can help you get started on code. We first saw this idea with images, when we wrote programs for multi-stripe flags:

  above(rectangle(120, 30, "solid", "red"),

    above(rectangle(120, 30, "solid", "white"),

      rectangle(120, 30, "solid", "red")))

We have again seen this idea with lists. Recall that a list has the following datatype:

  data List:

    | empty

    | link(fst, rst :: List)

  end

We’ve talked about the list-program structure (or "template"). A function that processes a List will have the following shape:

  fun processList(myLst :: List):

    cases (List) myList:

      | empty => ...

      | link(fst, rst) => ... processList(rst) ...

    end

  end

Here, we see a direct relationship between the shape of the code and the shape of the list: both have separate cases for each of the empty and link data. Both reflect that the rst is a list (which gets processed by calling the function on the rest of the list).

Key Principle: The shape of your code will follow the shape of your data.

2.1 Lists of Lists

So what does this principle imply if we are working with a list of lists? Imagine that you had a list of lists describing a maze (like we have on the project), and we wanted to know how many walls we would have. What do the data and program template look like?

We could write the data block two ways: if we designed this by hand from scratch, we might write:

  # this is for the outer list

  data MazeTable:

    | empty

    | table-link(fst :: MazeRow, rst :: MazeTable)

  end

  

  # this is for the inner list

  data MazeRow:

    | empty

    | row-link(fst :: String, rst :: MazeRow)

  end

(Note: in lecture we wrote this with link instead of table-link and row-link. The names are changed here to avoid conflict with the built-in link in Pyret)

If instead we thought about having built-in lists, you might have expressed this as:

  data List-of-List-of-String:

    | empty

    | link(fst :: List[String], rst :: List-of-List-of-String)

  end

The second version is actually the same as the first due to the embedded list in the first position. The main difference is in how we’d write the example in each case. For example, here’s a simple grid written as list of lists:

  gridList = [list:

               [list: "x", "o"],

               [list: "y", "z"]]

If we expand the [list:] shorthand into links, this would look like:

  gridList = link(link("x", link("o", empty)),

               link(link("y", link("z", empty)),

                 empty))

Here’s the same grid written in the MazeTable and MazeRow definitions:

  gridMaze = table-link(row-link("x", row-link("o", empty)),

               table-link(row-link("y", row-link("z", empty)),

                 empty))

This second version separates the outer from the inner list through the names of the link functions.

If we look at the first (MazeTable) version, a code structure emerges. Specifically:

  # process the outer list

  fun processMazeTable(mt :: MazeTable):

    cases (MazeTable) mt:

      | empty => ...

      | table-link(mrow, rst) =>

           ... processMazeRow(mrow) ... processMazeTable(rst)

    end

  end

  

  # process the inner list

  fun processMazeRow(maze-row :: MazeRow):

    cases (MazeRow) mt:

      | empty => ...

      | row-link(tileString, rst) =>

           ... processMazeRow(rst)

    end

  end

What does this code DO? Nothing yet. This is just the template, the shape of a program to process grids. To write an actual program to process grids, you’d copy the template (both functions) and fill in the holes.

2.2 A Concrete Example

Let’s write a function that counts how many times "x" appears in the grid. We copy the template, rename the functions, and add some tests/examples:

  # process the outer list

  fun countXMazeTable(mt :: MazeTable):

    cases (MazeTable) mt:

      | empty => ...

      | table-link(mrow, rst) =>

           ... CountXMazeRow(mrow) ... countXMazeTable(rst)

    end

  where:

    # always test each case, so test the empty case

    processMazeTable(empty) is 0

    # test the sample maze we wrote earlier

    processMazeTable(gridMaze) is 1

  end

  

  # process the inner list

  fun countXMazeRow(maze-row :: MazeRow):

    cases (MazeRow) mt:

      | empty => ...

      | row-link(tileString, rst) =>

           ... countXMazeRow(rst)

    end

  where:

    # always test each case, so test the empty case

    processMazeRow(empty) is 0

    # these are the two individual rows from our grid example

    processMazeRow(row-link("x", row-link("o", empty))) is 1

    processMazeRow(row-link("y", row-link("z", empty))) is 0

  end

Now, we fill in the holes in the functions, looking at the examples for guidance:

  # process the outer list

  fun countXMazeTable(mt :: MazeTable):

    cases (MazeTable) mt:

      | empty => 0

      | table-link(mrow, rst) =>

           CountXMazeRow(mrow) + countXMazeTable(rst)

    end

  where:

    # always test each case, so test the empty case

    countXMazeTable(empty) is 0

    # test the sample maze we wrote earlier

    countXMazeTable(gridMaze) is 1

  end

  

  # process the inner list

  fun countXMazeRow(maze-row :: MazeRow):

    cases (MazeRow) mt:

      | empty => 0

      | row-link(tileString, rst) =>

        if tileString == "x":

           1 + countXMazeRow(rst)

        else:

           countXMazeRow(rst)

        end

    end

  where:

    # always test each case, so test the empty case

    countXMazeRow(empty) is 0

    # these are the two individual rows from our grid example

    countXMazeRow(row-link("x", row-link("o", empty))) is 1

    countXMazeRow(row-link("y", row-link("z", empty))) is 0

  end

2.3 Critical Things to Notice

Here are the criticial things to notice:

This is the essential code structure for processing a list of lists. You need two functions (one for outer, one for inner). You can use a built-in function like map or filter for the inner list, but that is still using two different functions that process lists. You need different functions because one function can process only one type of data as its main input.

Wait, does this mean we cannot process the table in just one recursive function? That’s exactly what it means. In some cases, if you can use a built-in function to process either the outer or the inner list, it might look like you have only one function. But all of the functions we’ve seen that process lists (map, filter, length, distinct) are also recursive functions – you just didn’t have to write the recursion yourself.

Carry this observation over to your work on the project.

3 Study Questions