Today: function-call, decomposition, call-the-helper, Python style

Homework Strategy Note

At the start, your homework code is not working. You work and iterate, and eventually it's working. Ideally you understand why every line is in there. Not like, it's working but you're not sure exactly how! The later exam problems will greatly resemble the homework problems, so there's an extra incentive to feel confident about your homework solutions.

Recall - Make a Drawing

Following the advice from lecture-2, we'll make little drawings of the squares and colors to work out the code - the code problems are "detail oriented" as we say. On the homework problems, don't just stab at the Run button, make a drawing.


Check Front, then Move

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.

Tricky Case: Double Move

> 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.


alt: double move output

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()
        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.

Aside: Do Not Run-All To Debug 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.

Double Move Solution

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')

More Practice: Falling Water

The Falling Water problem in the puzzle section also demonstrates this issue for practice.

> falling-water


Decomposition - Program and Functions

Program Made of Functions

Our big picture - program made up of functions

alt: program is made of functions, each with def

Decomposition Strategy - Divide and Conquer

"Divide and Conquer" - a classic strategy, works very well with computer code

Web Browser - 21 Million Lines Of Code

Decompose Browser into Functions


Call a Function in Python - 2 Ways

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.

1. Call by noun.verb

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 Code

Look at a def again to see what it does. Here's what def for a bit problem might look like...

def go_west(bit):
    bit.left()
    bit.paint('blue')
    ...

The def establishes that this function name, go_west, 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.

2. Call Function By Name

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. We will explain that in detail later.

Function Call Picture

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.

Foreshadow Picture - Functions Calling Functions

High level - we have a program made of functions, each marked by a def and a name. Functions inside of the program will call each other, using their names. Python does not care what order the defs are in the program. Usually we put the simpler functions at the top, and the more complex ones below.

alt: a line in function 'b', calls the 'a' function


Decomposition Strategy - Bottom Up

For decomposition, we separate out a helper function for each sub-problems (aka divide and conquer).

Here we'll follow the bottom-up strategy where we write the helper function first.

1. Write the code for a helper function, get it working correctly.

2. Later functions can call that helper function when confronted with that sub-problem. A win!

Decomposition Example 1 - Fill Example

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.

> Fill Example

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:
alt: world without blue, bit at upper left

Program After:
alt: world filled blue, bit at lower left

Step 1. - Helper Function fill_row_blue()

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)
bit at left facing south

fill_row_blue After (post):
row filled with blue, bit back at start position

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.

Function Pre/Post Conditions

Pre/Post - Why Do I Care?

fill_row_blue() Pre/Post

Now comes the magic step for today - calling the helper function.

Challenge Step-2: Write fill_world_blue()

Aside: Milestone Strategy

To build a big program, don't write the whole thing and then try running it. Have a partially functional "milestone", get that working and debugged. Then work on a next milestone, and eventually the whole thing is done. We'll do this on every project, and it's the quickest way to get systems working.

2 Row Milestone

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.

1. How can I get the 1st row filled?

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.

2. Where is Bit Now?

Where is bit after the call? Look at the post condition.

3. How can I get the 2nd line filled?

Move bit down to row 2. Call helper again. Use the pre/post to work with the helper.

4. Loop To Solve All Rows But The Top

Put in a while loop to move bit down the left edge until hitting the bottom. Call the helper for each moved-to row. This solves all the rows but the top row.

...
while bit.front_clear():
    bit.move()
    fill_row_blue(bit)

5. Solve The Whole Thing

Put in one call to the helper to solve the top row, then the loop solves all the lower rows.

fill_world_blue() Solution

def fill_world_blue(filename):
    bit = Bit(filename)  # provided
    fill_row_blue(bit)
    while bit.front_clear():
        bit.move()
        fill_row_blue(bit)

Helper Functions - Strategy


Decomposition Example - Fancy Paint

> Fancy

Bit is moving past some lone blocks. We want each block to get a fancy paint job like this:

Fancy before and after
alt: fancy before
alt: fancy after

1. Helper paint_fancy(bit)

Helper function, paint around one block.

Pre: facing block

Post: painting done, on starter square, facing away from block

Fancy before and after
alt: fancy one before
alt: fancy one after

Helper Function paint_one(bit) Code

This code would be easy enough to write, but we're providing it to focus on the decomposition step.

Notice also the tripe-quote """Pydoc""" at start of the function. This is a Python convention to document the pre/post 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()

1. Start code of paint_all()

First step, write the standard while-loop to move bit forward to the side of the world.

    while bit.front_clear():
        bit.move()

2. How to detect a block to draw around?

What is an if-test that is True for a block above bit?

alt: test for block

A: A good start is bit.left_clear() - however, it has exactly the opposite T/F of what we want. Use "not" to invert it to make the T/F we want.

3. How to fancy paint the block?

Q: How to fancy paint the block?

A: Call the helper function - today's theme. Is bit facing the correct direction for the call? No, need to face the block before calling (i.e. matching the pre of the function).

4. What do do after the call?

Q: What way is bit facing after calling the helper?

A: Down - the post of the helper function 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 - turn bit to face that way. With bit's direction matching what the while-loop had before, the while loop will resume properly.

5. Run and see

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.

6. There's More - 2nd Row of blocks

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.

alt: fancy input with above and below blocks

7. How to Detect Below?

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 a block below on the (2) drawing above. 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 — very common to forget to update after a paste.

8. How to paint the lower block?

Q: How to paint the lower block

A: Call the helper. Need to account for pre/post as before.

paint_all() Solution

Here's the working code. The paint_one(bit) lines are the key lines for understanding decomposition.

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()
        if not bit.left_clear():
            bit.left()
            paint_one(bit)
            bit.left()
        if not bit.right_clear():
            bit.right()
            paint_one(bit)
            bit.right()

Fancy Paint Observations

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. This is how there's only a single bit.paint('red') and yet there's so much red in the output.

(optional) Extra Decomposition Example - Cover

> Cover Example

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 at start

cover_square() After (post):
cover after all 4 sides down

Cover Helper - cover_side()

Code for this is provided.

cover_side() Before: on top of first square, facing direction to go
alt: cover_side before

cover_side() After - move until clear to the right, painting every square green
alt: after one cover_side

1. Run cover_side()

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)

2. Challenge: write code for 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:


Some other points to clean up, if we have time.

Helpers At Top - Convention

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.

paint_one() Helper Pydoc Triple Quote

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 "Pydoc" for each function. The description is essentially a summary of the pre/post in words, see the """ section in here:

def paint_one(bit):
    """
    Begin facing square. Fancy paint
    around the square. End on start square,
    facing away from square.
    """
    ...

Coding Style 1.0 - Tactics

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.

See our Python Guide Style section. We'll pick out a few things today (for hw1), re-visit it for the rest later.