Today: function-call, decomposition, call-the-helper, Python style
The cartoon from the first day - code working is no, no, no, no, no, no, .. yes. From "Hyperbole and a Half" by Allie Broch.
It's a good idea to read the homework handout as you go through the problems. I know it's a lot of words, but often we put warnings or suggestions in there to help you avoid time-wasting areas.
The natural rest state of code is "not working". You start working, and fix, and run, and fix, and try to diagnose. Countless clicks of the Run button with bad results. Spend a lot of time in this phase. Finally you figure out the last piece at it works! Once it done, the solution can seem kind of clear, discounting the wandering path we took to get here.
Sure it's obvious once it's done, but many students spend a long path to get there. You are learning skills as you go through the long path. Some students feel like the other students are doing it with no work, which is not the case. Many students are putting in loads of work.
If we deleted your solution and you had to do it again, probably you would do it a lot faster. Once you are done with hw1, go back and look at those early problems. Now you could do them pretty easily. This gives you a feel for what you are learning.
Ideally, when it is done, you understand why every line is in there. This is a higher bar than just "it has a green checkmark", and not always achieved, but it's what we are aiming for.
When you finally solve that last issue and it works, you may feel a moment of real joy! It's earned!
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 which is the Python standard for superficial things like spacing and whatnot.
Read the introductory Python Style Basics section in the Python Guide for the basics everyone should know. We'll pick out a few style issues today in lecture and you should follow these on hw1. We'll revisit style ideas further, later in the quarter.
Indent 4 spaces — e.g. within a def, or within a loop. The tab key does this for you automatically — when editing Python code, it will put in 4 spaces for you, and the delete key will take out 4 spaces.
def foo(filename)
bit = Bit(filename)
while b.get_color() != 'red':
bit.move()
1 + 2 * 3 if x == 'red':
No space to the left of colon or comma. No space between a function name and its parenthesis. No space between parenthesis and their contents.
do_nums(1, 2, 3)
passThe word pass in Python is a placeholder that does nothing. Often pass in the code marks the place where you add code. Remove the pass when you put your code in, it's just a placeholder.
def - 2 Blank LinesIn code with multiple functions, leave 2 blank lines between defs.
'red'PEP8 gives the option of enclosing text with single or double quotes like 'red' or "hello", asking only that one convention or the other be followed. For CS106A, we prefer to use single quotes, saving wear and tear on your shift key!
bit.move() Any Old Time?The answer is no. If there is a wall or black square in front of bit, then the move will fail with a runtime error.
... bit.move()
bit.front_clear()Each move should be preceded by a check that bit.front_clear() returns True. In effect this is how bit looks one step ahead, checking that the way is clear. Checking that the front is clear is the issue with the next problem.
front_clear() -> TrueHere front_clear() returns True, so a move is ok.
front_clear() -> FalseHere front_clear() returns False, so a move would fail with an error.
> 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 blank. 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 in the loop 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() # possible error
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 work, and some cases expose a bug in this 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 then run that case by itself to debug it, so you can see the individual lines run.
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.
Essentially for the second move, if there is a clear square ahead, we want to move ahead and paint. Otherwise, no clear space, don't move. The solution is to add an if-statement that checks if the front is clear for the second move, and put the move-paint code is inside the if.
def double_move(filename):
bit = Bit(filename)
while bit.front_clear():
bit.move() # This move is safe
if bit.front_clear(): # If-check for 2nd move/paint
bit.move()
bit.paint('red')
The Falling Water problem in the puzzle section also demonstrates this issue for practice.
Sort of a joke, but not really. Many layers of CS technology are organized like ... instead of the programmer having to do something, we arrange things so they press a button, and the computer does it instead. Keyboard accelerators are on-brand for this sort of thinking!
A few keyboard tricks for editing code, these can save time, or at least they feel like they save time. On the Mac, "command" here refers to the "command" key. On windows it's either the control key or the windows key.
1. Select multiple lines, then tab, shift-tab to indent and un-indent.
2. Select multiple lines, then command-/ (command slash) comments and un-comments lines.
3. On the experimental server, command-enter .. hits the Run button. Saving valuable seconds, and smiting the keyboard is more fun.
4. Control-k "kills" a line of text, deleting it to the end of the line. Amazingly this works in gmail and many web forms and in a lot of editors - super handy! Hold down the ctrl key with one hand, and hammer away 'k' to get rid of lines of text when your draft is not working out.
See Keyboard accelerators chapter in the guide for a few more of these.
Functions are central in building computer systems, and are part of the key CS technique of "decomposition" - dividing a program into smaller functions.
Big picture view of a program — a program made up of functions
"Divide and Conquer" - a classic strategy, works better than you would think with computer code. Historically associated with Julius Caesar.
Surprising fact — writing a series of smaller functions takes less time than writing an equivalent giant function that does the whole problem. As if there is a "short function discount", where writing short functions is disproportionately easier.
It feels a little magic when it works — call the helper, and, poof! The problem's gone.
To "call" a function means to go run its code, and there are two ways it is done in Python. Which way a function is called is set by its author when the function is defined.
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 with this form now, and in future weeks we'll use many functions with the same noun.verb syntax...
bit.left() # turn bit lst.append(123) # append to list
def - Function Name and CodeLook at a def again to see what it does. Here's what def for a bit problem might look like.
Q: What is the name of the function being defined in this def?
def go_west(bit):
bit.left()
bit.paint('blue')
...
The name in this def is go_west, and that name refers to these indented lines of code. The def does not run the code. It establishes that this name refers to this code. If another part of the program wants to refer to this function, it uses the name go_west.
The second type of function call in Python is deceptively simple. You just type the function's name with parenthesis after it. Here is what a line of code calling the above go_west function looks like:
...
go_west(bit)
...
The word "bit" goes in the parenthesis for now. That's a parameter that we will explain in detail later.
Calling a function prompts the computer to go run the code in that function, and then comes back to where it was. Say for example the computer is running in a "caller" 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 where it left off.
The computer is only running one function at a time.
In the novel A Wizard of EarthSea by Ursula Le Guin .. each thing in the world has a secret, true name, given to it by the universe. 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. This book also features a wizardry school, written some 30 years before Harry Potter.
This example demonstrates bit code combined with divide-and-conquer decomposition. We'll write a helper function to solve a sub-part of the problem.
Utilizing decomposition, there will be a moment in the code where you put the right phrase at the right spot and, POOF!, somehow instantly the problem is solved. It feels a little magical.
Here is the task for the whole program: bit starts at the upper left facing down. We want to fill the whole world with blue, like this
What we have (before):
What we want (after):
fill_row_blue()First we'll decompose out a fill_row_blue() helper function that just solves 1 row.
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() helper a few times (Case-1) to see what it does.
Key question: What is the postcondition of a()? Does it match the precondition of b()?
e.g. Maybe a() leaves Bit one way , but b() requires Bit facing some other way, so I need to add in a little adjustment between the two function calls to mesh them together.
It's necessary to think about pre/post conditions as we mesh functions together. The documentation for a function is just a description of its pre and post conditions.
Typically we write down the pre/post for each function. For now, we are providing these function specifications, but later in the quarter we will have you write them.
Now comes the magic step for today - calling the helper function.
fill_world_blue()To build a big program, don't write the whole thing and run it only after it's all written. Instead, write a fraction of the code that gets it a partial "milestone" state, and run and debug that code until the milestone works. Then work on a next milestone, and eventually the whole thing is done. This is a good practice for building complex systems. Our assignment handouts will usually talk about building and testing each project in terms of milestones, to help cement this habit.
Fill the top row, how?
Not bit.left() It's natural to think about turning left and writing a loop at this point. In this case, there is a better way.
This can be done with 1 line of code. Think function-call.
A: Call the helper function - key example for this lecture
Code this up, click Run to see what it does, though it does not solve the whole problem.
Where is bit after the call? How to get to 2nd row? Look at the post condition of the helper.
Move bit down to row 2. Call helper again.
Comment out the calls to the helper temporarily. Write a while-front-clear loop to just move straight down the left side, stopping at the bottom row. This works simply.
Add a single call to the helper in the loop, solving each row just after bit moves into that row. In this way, when the loop halts at the bottom, the last row is solved. This does not solve the first row.
...
while bit.front_clear():
bit.move()
fill_row_blue(bit)
How to solve the top row? Put in one call to the helper to solve the top row. This is just like painting that one green square before the loop starts moving from lecture-2.
fill_world_blue() Solution
def fill_world_blue(filename):
bit = Bit(filename)
fill_row_blue(bit) # top row
while bit.front_clear():
bit.move()
fill_row_blue(bit) # moved-to row
> Fancy
Bit is moving past some lone blocks. We want each block to get a fancy paint job like this, where the afterwards the block has red to its left, green on top, and blue to its right.
Fancy - what we have and what we want
paint_one(bit)Helper function, paint around one block.
Pre: facing block
Post: painting done, back on the starting square, but facing away from block
The pre/post conditions are somewhat arbitrary, like Bit could end facing towards the block instead of away, and that could be made to work. Whatever the conditions are though, we need to be very clear about them, so the different parts of the program can agree about those conditions whenever there is a handoff from one function to another.
paint_one(bit) CodeThis code would be easy enough to write, but we're providing it in the starter code to focus on the decomposition step.
Notice also the triple-quoted """Docstring""" at start of the function. This is a Python convention to document the pre/post abstraction of a function.
def paint_one(bit):
"""
Begin facing square. Fancy paint
around the square. End on start square,
facing away from square.
"""
bit.left()
bit.move()
bit.right()
bit.move()
bit.paint('red')
bit.move()
bit.right()
bit.move()
bit.paint('green')
bit.move()
bit.right()
bit.move()
bit.paint('blue')
bit.move()
bit.right()
bit.move()
bit.left()
> Fancy
Recall what we want
First step, write the standard while-loop to move bit forward to the side of the world.
while bit.front_clear():
bit.move()
Write an if-test in the loop to detect the block. What test is True if a block is above bit? Make a little drawing to work it out.
A: A good start is bit.left_clear() - however, it has exactly the opposite T/F of what we want, so the correct if-test is
if not bit.left_clear():
Write the if-test, then worry about the code inside the if-statement to paint the block.
Q: How to fancy paint the block? i.e. the code that goes inside the if.
A: Call the helper function - today's theme. Is bit facing the correct direction for the call? No, need to turn left to face the block before calling, matching the pre of the function. (Work a little drawing for these details).
Q: What way is bit facing after calling the helper?
A: Down - the post of the paint_one() tells us this. We cannot resume the while-loop with bit facing down (you could run it and see). The while loop had bit facing the right side of the world - turn bit to face that way. With bit's direction matching what the while-loop had before, the while loop will resume properly. (Work a little drawing for these details.)
...
while bit.front_clear():
bit.move()
if not bit.left_clear():
bit.left()
paint_one(bit)
bit.left()
Put in the call to the helper - minding the pre and post adjustments before and after the call. Run it to see how it works.
Look at Case-3 - another row of blocks below. Actually we want bit to fancy paint blocks both above and below its horizontal track. This will not be much work, since we have the helper function.
Q: What is the if-test that is True for a block below as bit goes to the right.
A: Similar to previous test, just swapping left/right: if not bit.right_clear():. Add the if-statement for the lower block below the code for the top block.
Q: How to paint the lower block?
A: Call the helper again. Need to turn right first (see above drawing), needing to account for pre/post as before. It's fine to use copy/paste to re-use the code for the top block, but remember to then update the details in the pasted version — it's a common mistake to do a copy/paste but then forget to update a word. The paint_one() helper requires only that we face the block before calling it. The helper is elegantly direction-independent in this way.
Here's the working code. The paint_one(bit) lines are the key lines showing the power of function decomposition — Call the helper!
def paint_all(filename):
"""
Move bit forward until blocked.
For every moved-to square,
Fancy paint blocks which appear
to the left or right.
"""
bit = Bit(filename)
while bit.front_clear():
bit.move()
# Detect block above
if not bit.left_clear():
bit.left()
paint_one(bit) # Call the helper
bit.left()
# Block below
if not bit.right_clear():
bit.right()
paint_one(bit)
bit.right()
Important CS strategies to observe in the Fancy Paint example:
Decompose out a helper for a subproblem. It's big help later on the main problem.
Need to think about the pre/post before and after the call to the helper to mesh things together.
This problem is complex enough where the make-a-drawing technique is helpful to tame the details, meshing the parts together.
Q: look at the main output - how many bit.paint('red') lines are in this program?
A: Just one. Decomposition is about making a helper and then using it heavily, in effect making one copy of the code and then re-using it everywhere it applies. This is how there's only a single bit.paint('red') and yet there's so much red in the output.
Some other points to clean up, if we have time.
There is a convention to put the helper functions first in the program text. 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 — the paint_one() helper is first, and it is the simplest and does not depend on another function. Then the function that uses it is below.
At the top of each function is a description of what the function does within triple-quote marks. This is a Python convention known as a "Docstring" for each function. The description is essentially a summary of the pre/post abstraction in words. It does not describe every detail, but it gets the most important facts to call this function correctly.
def paint_one(bit):
"""
Begin facing square. Fancy paint
around the square. End on start square,
facing away from square.
"""
...
For now, the provided code includes the written Docstrings, but later we'll have an exercise where the problem statement directs the student to write it.
Extra practice - we are not doing this one in class.
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_square() Before (pre):
cover_square() After (post):
cover_side()Code for this is provided.
cover_side() Before: on top of first square, facing direction to go
cover_side() After - move until clear to the right, painting every square green
cover_side(bit) specification: Move bit forward until the right side is clear. Color every square green.
Run this code with case-1, to see what it does. (code provided)
cover_square()cover_square(bit) specification: Bit begins atop the upper left corner, facing right. Paint all 4 sides green. End one square to the left of the original position, facing up.
Add code to paint the top and right sides. Key ideas: