1 Mutation-induced Graphs
2 Mutation and Testing
3 Mutation in Search
4 Summary (so far)

To Mutate or Not to Mutate

Kathi Fisler

1 Mutation-induced Graphs

Even though yesterday’s lab was nominally about testing, it also introduced a twist on where graph-like data comes from. The vending machine program contained a single class, but it defined an entire graph of vending machine instances, each arising from an interaction with the machine over time. (We will draw this graph out in lecture, but it’s harder to reproduce in these notes).

The thrust of this comment is that we don’t just generate graphs explicitly as we did for routes and cities. Computational processes often give rise to graphs. We write many programs over such graphs. For example, the owner of the vending machine might want to prove that the machine never dispenses items without sufficient balance in the machine, or that someone can eventually purchase an item if they continue to deposit money. These are questions over the graphs arising from computation (this is one of the kinds of problems I work with in my research).

This is more an interesting side note than something I expect you to be able to answer questions about (with respect to the final, for example). For those going on in computer science, though, realizing that processes yield graphs that can be processed through programs is an important concept for you to master.

2 Mutation and Testing

Our vending-machine lab also introduced some questions on testing in the face of mutation. When we made the vending machine a static in our test cases, our tests failed to be independent: changes in one test influenced the execution of another test. JUnit tries to mitigate this problem by creating a new instance of the test class on each test (which is why removing the static eliminated the problem). Using setUp and tearDown methods also help manage testing in the face of mutation.

One lesson of the lab (aside from the mechanics of JUnit) is that testing somehow got a bit more tedious once we started working with mutation-based classes. Testing is one of the first cases when we see the adverse effects of mutation, but there are others.}

3 Mutation in Search

Imagine that you are writing a program to choose moves in a game (such as chess or some other strategy-based game). Your program needs to try out a move, see how it will perform, then backup and try additional moves until you have enough data to choose a good move.

Let’s look at this problem in a bit more detail in the context of a simpler game: tic-tac-toe. Imagine that the game board so far looks like

-------------

|   |   |   |

-------------

| X | O |   |

-------------

| O | X |   |

-------------

If X goes next and chooses the upper-middle cell, X is guaranteed not to win the game. If X chooses the top-right cell, X could still win depending on what O does. We can imagine writing a move-search program that simply tries all the moves (including these two) to determine which makes the most sense.

The search defines a tree of partially-completed boards; at each level, we fill another cell in turn. Each branch terminates when one player has won the game or the board is filled with a tie/draw.

Of course, when we implement the search, we might not want to create a tree of so many boards (especially if we are considering a larger game such as chess). Instead, we might choose to create a single board structure that we modify on each selection:

  class Game

    Board b;

  

    searchMoves(Player forPlayer) {

      LinkedList<Cell> openCells = b.getOpenCells();

      for (Cell c : openCells) {

        this.tryMove(c, forPlayer);

      }

    }

  

    void tryMove(Cell cell, Player byPlayer) {

       b.set(cell, byPlayer);

       // continue search to fill in remaining boards

    }

This uses much less storage, but introduces another problem: once we reach the end of a branch and try to backtrack to try another cell (in the for-loop), the board is already filled with the moves we tried on the previous branch. If we don’t handle this carefully, our search from subsequent moves won’t yield the right answer.

"Carefully" here means that as we back out of each branch of the tree, we have to undo any moves that we may have made on the way down. This task is not insurmountable, but it does complicate the code and open up lots of room for error (if someone forgets to undo and edit or does so incorrectly). Isn’t there a better way to handle this?

Unless space really is so critical an issue, you really are better off generating new game boards as you search through the tree. Note that this is different from generating the entire tree of moves and searching over that. You can simply generate the boards on the current branch, leaving the garbage collector to remove those nodes as you finish processing each branch. If you have to backtrack or undo computation, creating new objects is usually preferable to mutating existing objects.

4 Summary (so far)

Where does this leave us? Are memory and mutation evil constructs to be avoided at all costs in programming? Of course not. But you have to know how and when to use them effectively. Tune in tomorrow.