BYU logo Computer Science

To start this guide, download this zip file.

Practice with functions

We are going to practice writing functions by drawing some smiles. Our bit world starts like this:

large empty world

and we want to draw four smiles:

four smiles

Planning

Before you write any code, you should plan what you want to do. A good way to do this is with pencil and paper. Find a friend and draw out how you think you would solve this problem.

Tip: Start by drawing out just one smile. Remember, you can draw one smile with a function and then draw the rest by calling that function.

work with a friend to solve this problem

What did you draw? Maybe you drew something like this:

smiles sketch

or something like this:

smiles sketch

There is not necessarily a right way or a wrong way to do this. There may be some ways that are easier and some that are harder, in the sense that they take more work to write the associated program. One rule of thumb is to try to move in one direction only (left to right or top to bottom), as this will generally reduce the amount of code you have to write. But we could write code to match either of these drawings!

Starting to code

Download the file linked above and store it in your bit folder. Look at the starter code in smiles.py:

from byubit import Bit


@Bit.worlds('smiles')
def run(bit):
    pass


if __name__ == '__main__':
    run(Bit.new_bit)

What you want to do is translate your sketch into a set of functions that do each part. We are going to work off the first drawing:

smiles sketch

Your first instinct will normally be to start writing the first piece, labeled (1) in the diagram - move right. But if you keep doing this, you will end up with a very long run() function. Instead, think at the highest level and work your way down to smaller pieces.

The first step is to draw a smile, so let’s write a function for that:

from byubit import Bit


def draw_smile(bit):
    pass


@Bit.worlds('smiles')
def run(bit):
    draw_smile(bit)


if __name__ == '__main__':
    run(Bit.new_bit)

We are not ready to write the code there yet, so we wrote pass temporarily. Using pass makes the code valid, even though it does nothing.

Decomposition

The next step is to take each of the four steps from our drawing and turn them into functions:

def draw_smile(bit):
    move_into_place(bit)
    left_side(bit)
    bottom(bit)
    right_side(bit)

This is called decomposition in computer science — taking a larger problem and breaking it down into smaller pieces.

Decomposition is one of the most fundamental skills you will learn in this class. It is part of a broader concept referred to as “computational thinking.” You will find that this skill will help you with problem solving in many disciplines.

As you type this into PyCharm, you will notice that PyCharm will underline these functions with red squiggly lines:

PyCharm showing undefined functions

If you hover over one of them, a tip will pop up telling you what is wrong:

PyCharm showing tip for undefined function

PyCharm is telling you the function move_into_place() is not defined yet. You can’t call a function that you haven’t written. But this is easy to fix! Just click the blue solution, Create function 'move_into_place' and PyCharm will write an empty function for you:

PyCharm writing an empty function

Click on all of the rest of these, and now you have a complete program again:

from byubit import Bit


def move_into_place(bit):
    pass


def left_side(bit):
    pass


def bottom(bit):
    pass


def right_side(bit):
    pass


def draw_smile(bit):
    move_into_place(bit)
    left_side(bit)
    bottom(bit)
    right_side(bit)


@Bit.worlds('smiles')
def run(bit):
    draw_smile(bit)


if __name__ == '__main__':
    run(Bit.new_bit)

Filling in the functions

This program doesn’t do any drawing yet. But it has fantastic decomposition! Having the problem decomposed is an important step in programming. Filling in the details of each function is relatively easy.

Keep in mind our sketch:

smiles sketch

Let’s fill in just one function, the first one that is called:

def move_into_place(bit):
    """ Gets into position to draw a smile by moving to the
        top left eye and then turning right.
    """
    bit.move()
    bit.turn_right()

Notice we put bit.turn_right() here so we can get ready for the next piece.

Ok, that one is really easy, so let’s do one more:

def left_side(bit):
    """ Paints the top left eye and the  left corner of the smile.
        Ends up facing to the right on the bottom left of the smile.
    """
    bit.paint('blue')
    bit.move()
    bit.move()
    bit.paint('blue')
    bit.move()
    bit.turn_left()

That was actually pretty easy too!

Now, you may be tempted to keep writing code here. But it is a good idea to stop and see if these two functions are working before we fill in any more. Because all the other functions are using pass they are valid and will run. So you can run your code to see how you are doing so far:

smiles first run

Hey, that’s pretty good! You can see that your first two functions are working as expected.

Filling in more functions

Let’s fill in a couple more. Here’s our sketch:

smiles sketch

We can now do the bottom of the smile:

def bottom(bit):
    """ Paints the bottom part of the smile. Ends up facing right,
        in the bottom right corner of the smile.
    """
    bit.move()
    bit.paint('blue')
    bit.move()
    bit.paint('blue')
    bit.move()
    bit.paint('blue')
    bit.move()
    bit.turn_left()

and the right side of the smile:

def right_side(bit):
    bit.move()
    bit.paint('blue')
    bit.move()
    bit.move()
    bit.paint('blue')
    bit.turn_right()

Run your code to see how well those are working:

second run of smiles program

Look at that, we’ve got our first smile completed!

The rest of the smiles

Now that we can draw a single smile, we can go back to our main function:

@Bit.worlds('smiles')
def run(bit):
    draw_smile(bit)

We need to put in code for the rest of the smiles. Let’s try this:

@Bit.worlds('smiles')
def run(bit):
    draw_smile(bit)
    draw_smile(bit)
    draw_smile(bit)
    draw_smile(bit)

If we run this, we get a lot of errors:

third run of smiles program

Our smiles are too close together! This means we are missing some glue code. Notice how when we draw a single smile, we start off one square to the left. But when we finish a smile, we are two squares to the left of the next smile.

We can fix this by adding an extra move in between the smiles:

@Bit.worlds('smiles')
def run(bit):
    draw_smile(bit)
    bit.move()
    draw_smile(bit)
    bit.move()
    draw_smile(bit)
    bit.move()
    draw_smile(bit)

Now when we run this we get the correct final world:

fourth run of smiles program

We are very happy indeed! :-)

Decomposition + write small functions = happier life

When you decompose a bigger problem into smaller steps, you end up with small functions. Each function should have a single, clear purpose. When you write code, you should live by this philosophy:

Write small functions that do one thing well.

If you also write one function at at time, and then test it, you will be more successful. It is not fun to write a very large amount of code, have a problem in that code, and not know where that problem is.

Good decomposition will make your programs easier to write and make your programs easier to understand.

Remember, when you write code, you are writing not just for yourself now, but for anyone who might read your code in the future, including your future self.

xkcd comic about the future self