Today: function-call, decomposition bottom-up, top-down, delegation power, style-1
One of our strategies is "don't do it in your head", but this does not come naturally to Stanford students! They've made it this far, they figure. So let me try this approach.
You type the code in and run it. If it works perfectly - fine you're done. Suppose it does not work right. What are you going to do?
A strategy - tempting! - is to randomly add "not" to tests, and extra bit.move() here and there in the hopes that one of these will work! This is not the skill to get your programs working in the future.
Make a little drawing of bit in this world to think about your code. You want to build a mental model of how it's going to work, and write code from that. You know why each line is in the code. Chilling reminder: the exams will look just like these homework problems.
Suppose you are trying to work out what the while-test should be. In this way, we freeze the action - we look at the world and the code that is running in it. With bit at one spot in the world, look at the while-test, think about what it needs to do from your sketch. This is the right skillset for untangling these code problems.
Juliette and I will always have a supply of blank paper in office hours, and we will inevitably hand you a couple pieces, and say something like "could you sketch out the world, and then look at your line-4, and tell me what it's going to do when bit gets to that corner."
Bit code cannot just do a move() any old time. The move needs to be preceded by a front_clear() check - in effect this is how bit looks where it is going. This is the issue with this next problem.
> double-move (while + two moves in loop)
The goal here is that bit paints the 2nd, 4th, etc. moved-to squares red, leaving the others white. This can be solved with two moves and one paint inside the loop, but it's a little tricky.
The code below is a good first try, but it generates a move error for certain world widths. Why? The first move is safe, but the second will make an error if the world is even width. Run with Case-1 and Case-2 to see this.
def double_move(filename):
bit = Bit(filename)
while bit.front_clear():
bit.move()
bit.move()
bit.paint('red')
Usually you run your code one case at a time, using the Run All option as a final check that all the cases work. In this case, Run All reveals that some cases have a problem with the above code.
To figure out what is wrong with your code and fix, run a single case so the lines hilight as it runs. Use Run-All to find a case with a problem, but don't debug with it.
The problem is the second move - it is not guarded by a front_clear() check, so depending on the world width, it will try move through a wall. The first move in the loop does not have this problem - think about the while-test just before it.
So we don't know if the second move is safe or not. The solution is to put it in an if-statement that checks if the front is clear, only doing the move if front_clear is true:
def double_move(filename):
bit = Bit(filename)
while bit.front_clear():
bit.move() # This move is fine
if bit.front_clear():
bit.move() # This move needs a check
bit.paint('red')
The Falling Water problem in the puzzle section also demonstrates this issue for practice.
Our big picture - program made up of functions
In the Wizard of EarthSea novels by Ursula Le Guin .. each thing in the world has its secret, true name. A magician calls a thing's true name, invoking that thing's power. Strangely, function calls work just like this - a function has a name and you call the function using its name, invoking its power.
We've seen def many times - defines a function. Recall that
a function has a name and body lines of code, like this "go_west" function:
def go_west(bit):
bit.left()
bit.paint('blue')
...
To "call" a function means to go run its code, and there are two common ways it is done in Python. Which way a function is called is set by its author when the function is defined.
There are two ways to call a function in Python...
For "object oriented" code, which is how bit is built, the function call is the noun.verb form, e.g. bit.left(). Here "left" is the name of the function. Your code calls bit functions like this, but your own functions will use the more common "by name" form below.
For a function with a regular def, like this:
def go_west(bit):
bit.left()
bit.paint('blue')
...
A call to that function is written simple with the function's name with parenthesis after it, like this:
...
go_west(bit)
...
The "bit" goes in the parenthesis for now. We will explain that in detail later.
Say the program is running through some lines, which we'll call the "caller" lines. Then the run gets to the following function call line; what happens?
# caller lines
bit.right()
go_west(bit) # what does this do?
bit.left()
...
What the go_west(bit) directs the computer to do: (a) go run the lines inside the "go_west" function, suspend running here in the caller. (b) When go_west() is done, return to this exact spot in the caller lines and continue running with the next line. In other words, the program runs one function at a time, and function-call jumps over to run that function.
Suppose the computer is running one function, and within there is a call to a foo() function - the computer goes to run the foo() code, then returns and continues in the caller function.
High level - we have a program made of functions. When the computer is running in one function, sometimes there will be a "call" to another function. For example when the computer is running in the "b" function below. It gets to a line that calls the "a" function. The computer jumps up to "a", runs it from top to bottom. When done, the computer resumes on the next line in the "b" function. This is a sketch of the action, we'll work some real examples to see it in action.
Our first examples - we'll work on the helper function first, then the function that calls it. This is the "bottom up" order. There's also a top-down example we'll do at the end where you write the helper last. These are both fine techniques.
This example demonstrates bit code combined with divide-and-conquer decomposition. The program is decomposed into two functions.
The whole program does this: bit starts at the upper left facing down. We want to fill the whole world with blue, like this
Program Before:
Program After:
First we'll decompose out a fill_row_blue() function that just does 1 row.
This is a "helper" function - solves a smaller sub-problem.
fill_row_blue() Before (pre)
fill_row_blue After (post):
We could have you write the code for this one, but we're providing it today to get to the next part.
Run the fill_row_blue a few times, see what it does. (Case-1 in the menu is the fill_row_blue test case).
def fill_row_blue(bit):
bit.left()
bit.paint('blue')
while bit.front_clear():
bit.move()
bit.paint('blue')
bit.right()
bit.right()
while bit.front_clear():
bit.move()
bit.left()
-Now comes the key, magic step for today. First, know what fill_row_blue() does exactly, pre/post.
As an experiment write code to just solve the first 2 rows, without a loop. This is a test "milestone", working out that some things work, but without solving the whole thing.
This can be done with 1 line of code. Think function-call.
Where is bit and with what facing after the code above? How can you know what the bit state will be?
Put in a while loop, solve all the rows except the top one.
Put in one call to solve the top row, then a loop solves all the lower rows.
def fill_world_blue(filename):
bit = Bit(filename) # provided
fill_row_blue(bit)
while bit.front_clear():
bit.move()
fill_row_blue(bit)
Bit starts next to a block of solid squares (these are not "clear" for moving). Bit goes around the 4 sides clockwise, painting everything green.
Cover Before (pre):
cover_side() Helper After (post):
cover_square() After (post):
cover_side(bit): Move bit forward until the right side is clear. Color every square green. (demonstrates not left-clear test).
Run this code with case-1, see what it does. Study the code to see how it works, think about its pre/post (make a little drawing).
cover_square(bit): Bit begins atop the upper left corner, facing right. Paint all 4 sides green. End to the left of the original position, facing up.
Add code to paint the top and right sides. Key ideas:
> Hurdles
Before
After
def solve_hurdles(filename):
"""
Solve the sequence of hurdles.
Start facing north at the first hurdle.
Finish facing north at the end.
(provided)
"""
while bit.front_clear():
solve_1_hurdle(bit)
Helper function - solve one hurdle. If we had this, solve_hurdles would be easy. Just doing one hurdle is not so intimidating. Divide and conquer!
What helper functions would be useful here? Make observation about the 4 moves that make up a hurdle.
Here is a sketch, working out that there are two sub-problem types
Here is the solve_1_hurdle() code - sketching that we have go_wall_right() and go_until_blocked() helpers. Can write this before we write the helpers. Think about pre/post for each.
def solve_1_hurdle(bit):
"""
Solve one hurdle, painting all squares green.
Start facing up at the hurdle's left edge.
End facing up at the start of the next hurdle.
"""
go_wall_right(bit)
bit.right()
bit.move()
go_wall_right(bit)
bit.right()
go_until_blocked(bit)
bit.left()
go_until_blocked(bit)
bit.left()
Now write the helpers for the A/B sub-problems. Here we see the importance of the pre/post to mesh the functions together. (For lecture demo, may just use prepared versions of these.)
def go_wall_right(bit):
"""
Move bit forward so long as there
is a wall to the right. Paint
all squares green. Leave bit with the
original facing.
"""
bit.paint('green')
while not bit.right_clear():
bit.move()
bit.paint('green')
def go_until_blocked(bit):
"""
Move bit forward until blocked.
Painting all squares green.
Leave bit with the original facing.
"""
bit.paint('green')
while bit.front_clear():
bit.move()
bit.paint('green')
With the helpers written, set solve_hurdles() to the code below, testing one hurdle but not the loop:
def solve_hurdles(filename):
"""
Solve the sequence of hurdles.
Start facing up at the left edge of a hurdle.
Finish facing up at the end.
(provided)
"""
bit = Bit(filename)
solve_1_hurdle(bit)
# Un-comment the loop when the helpers
# are done
# while bit.front_clear():
# solve_1_hurdle(bit)
When it can solve one hurdle, change it to the loop form to solve the whole thing:
bit = Bit(filename)
while bit.front_clear():
solve_1_hurdle(bit)
Try running it on a few worlds. Switch to a small font so you can see all the code at once, watch the run jump around. Go, Bit go!
Think about writing that line where you call a helper
...
go_until_blocked(bit)
...
You can try the above lecture examples yourself. Use the "Reset Code" button to set it back to the start state.
There's also more problems on the experimental server for practice, Holes and Beloved:
> Holes
> Beloved
Some other points to clean up, if we have time.
There is a convention to put the smallest, helper functions first in a file. The larger functions that call them down below. This is just a habit; Python code will work with the functions in any order. Placing the helpers first does have a kind of logic — looking at solve_1_hurdle(), the functions it uses are defined above it, not after.
At the top of each function is a description of what the function does within triple-quote marks - we'll start writing these from now on. This is a Python convention known as "Pydoc" for each function. The description is essentially a summary of the pre/post in words, see the """ section in here:
def go_wall_right(bit):
"""
Move bit forward so long as there
is a wall to the right. Paint
all squares green. Leave bit with the
original facing.
"""
bit.paint('green')
while not bit.right_clear():
bit.move()
bit.paint('green')
Previously we had separate testing for each helper which is ideal, and we will do that again in CS106A. In this case, we just run the whole thing and see if it works without the benefit of helper tests.
CS106A doe not just teach coding. It has always taught how to write clean code with good style. Your section leader will talk with you about the correctness of code, but also pointers for good style.
All the code we show you will follow PEP8, so just picking up the style tactic that way is the easiest.
Python Guide - click on the Style section, we'll pick out a few things today (for hw1), re-visit it for the rest later.