BYU logo Computer Science

Control Flow

Legos and Language

When you’re building with Legos, each individual brick is not terribly useful on its own, but once we have enough bricks of different types, we can build some pretty cool stuff.

When you’re learning a new language, sometimes you run into situations where you struggle to express yourself because you don’t know enough words. But as your vocabulary and mastery of the grammar grow, your ability to express yourself becomes more fluent and comfortable.

Today we’ll be presenting more Lego bricks and vocabulary. Each idea by itself is simple, but as you learn to put them together, you’ll do some pretty cool stuff.

Do you remember…Go Green

from byubit import Bit

bit = Bit.new_world(10,3)
while bit.front_clear():
    bit.move()
    bit.paint("green")
bit.draw()

png

The while body looks like this:

bit.move()
bit.paint("green")

That works fine. It paints all the squares but the first one.

We could also have written it this way:

bit.paint("green")
bit.move()
bit = Bit.new_world(10,3)
while bit.front_clear():
    bit.paint("green")
    bit.move()
bit.draw()

png

That works fine too!

It paints all the squares except the last one.

Loops will very often miss one square - you can decide whether you want to miss the first square or the last square.

This is OK. Don’t go crazy trying to get all the squares in one loop. Just add a line before or after the loop.

bit = Bit.new_world(10,3)

# Paint the current square
bit.paint("green")

# Paint the rest
while bit.front_clear():
    bit.move()
    bit.paint("green")
bit.draw()

png

Looping Intuition

The decision and the action.

Does this make sense to everyone?

Lines before, in, and after a loop

bit = Bit.new_world(10,3)

# Paint the current square
bit.paint("green")

# Paint the rest
while bit.front_clear():
    bit.move()
    bit.paint("green")
bit.draw()

There are lines before the loop. They run once.

The lines in the loop run many (or no) times. It’s flexible.

The lines after the loop run once.

How does the computer know which lines belong to the loop and which lines don’t?

bit = Bit.new_world(10,3)

# Paint the current square
bit.paint("green")

# Paint the rest
while bit.front_clear():
    bit.move()
    bit.paint("green")

bit.paint("red")
bit.draw()

png

What happens if we indent bit.paint("red")?

No Go

bit = Bit.new_world(1,1)  # Not a very big space to live in...
while bit.front_clear():
    bit.move()
    bit.paint("green")

bit.draw()

png

How many times does the loop body run?

bit = Bit.new_world(5,3)
while bit.front_clear():
    bit.move()
    bit.paint("green")

bit.draw()

png

How manuy times does the loop body run?

Same loop code, different scenarios -> Generality

Generality

When you pull into the gas station, you have to find the pump specific for your car’s make and model.

🚗

When you bake a pie, you have to put the pie in the pie-baking oven, not the break-baking oven.

🥧

NO!

Generality is the concept that I can build one thing that will work for many use-cases.

It doesn’t have to work for ALL use-cases, but it probably isn’t useful if it only works in one case.

Usually, we like generalized code. One script works for many scenarios.

When you try to make your code cover too many scenarios…that’s silly.

Usually you’ll have a good feel for the kinds of cases you are trying to solve at the same time, and which cases need a new solution. Like gas pump vs EV charging station.

while True

Recall that a while loop needs an expression that evaluates to True or False.

If the condition is True, then the loop body will run.

If the condition is False, the loop body won’t run.

After each execution of the loop, we test the condition again.

What happens if the condition never changes?

Is that good or bad?

bit = Bit.new_world(3,3)
while True:
    bit.draw()
    bit.move()
    bit.paint("green")
bit.draw()

png

png

png

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

    MoveOutOfBoundsException                  Traceback (most recent call last)

    /tmp/ipykernel_849/3921399024.py in <module>
          2 while True:
          3     bit.draw()
    ----> 4     bit.move()
          5     bit.paint("green")
          6 bit.draw()


    /data/teach/cs110/lectures/Lecture2-Bit-control-flow/byubit.py in move(self)
        178         next_pos = self._get_next_pos()
        179         if not self._pos_in_bounds(next_pos):
    --> 180             raise MoveOutOfBoundsException(f"Bit tried to move to {next_pos}, but that is out of bounds")
        181         elif self._get_color_at(next_pos) == BLACK:
        182             raise MoveBlockedByBlackException(next_pos)


    MoveOutOfBoundsException: Bit tried to move to [3 0], but that is out of bounds

Spin!

bit = Bit.new_world(3,3)
while True:
    bit.left()
    ---------------------------------------------------------------------------

    Exception                                 Traceback (most recent call last)

    /tmp/ipykernel_849/2297832775.py in <module>
          1 bit = Bit.new_world(3,3)
          2 while True:
    ----> 3     bit.left()


    /data/teach/cs110/lectures/Lecture2-Bit-control-flow/byubit.py in left(self)
        188         """Turn the bit to the left"""
        189         self.orientation = self._next_orientation(1)
    --> 190         self._step()
        191
        192     def right(self):


    /data/teach/cs110/lectures/Lecture2-Bit-control-flow/byubit.py in _step(self)
        116         self._step_count += 1
        117         if self._step_count > MAX_STEP_COUNT:
    --> 118             raise Exception("Bit has done too many things. Is he stuck in an infinite loop?")
        119
        120     def save(self, filename: str):


    Exception: Bit has done too many things. Is he stuck in an infinite loop?

A Note on Calling Functions

Python requires () for a function call to happen. But it is technically valid syntax to leave the () off, but the meaning becomes different.

bit.move()

You: “Hey computer, move the bit.”

Computer: “OK”

bit.move

You: “Hey computer, did you know bit has an action called move?”

Computer: “Yes. I was aware of that.”

bit = Bit.new_world(5,3)
while bit.front_clear():
        bit.move
        bit.paint('blue')
    ---------------------------------------------------------------------------

    Exception                                 Traceback (most recent call last)

    /tmp/ipykernel_849/3648138265.py in <module>
          1 bit = Bit.new_world(5,3)
    ----> 2 while bit.front_clear():
          3         bit.move
          4         bit.paint('blue')


    /data/teach/cs110/lectures/Lecture2-Bit-control-flow/byubit.py in front_clear(self)
        208         Black squares are not clear.
        209         """
    --> 210         self._step()
        211         return self._space_is_clear(self._get_next_pos())
        212


    /data/teach/cs110/lectures/Lecture2-Bit-control-flow/byubit.py in _step(self)
        116         self._step_count += 1
        117         if self._step_count > MAX_STEP_COUNT:
    --> 118             raise Exception("Bit has done too many things. Is he stuck in an infinite loop?")
        119
        120     def save(self, filename: str):


    Exception: Bit has done too many things. Is he stuck in an infinite loop?

if

while loops are great when you want to do the same thing repeatedly.

What if you want to control whether it happens only once?

What if you want to paint a square green if it is blue, but you want to paint it blue if it is something else?

bit = Bit.load("blue-to-green.txt")
bit.draw()

png

bit = Bit.load("blue-to-green.txt")
bit.draw()

# Paint green if blue, paint blue for anything else
while bit.front_clear():
    bit.move()
    if bit.get_color() == "blue":
        bit.paint("green")
    else:
        bit.paint("blue")
bit.draw()

png

png

👏🏽

That was cool. Let’s dig in.

if syntax

if <condition>:
    Do this if <condition> is True
else:
    Do this if <condition> is False

Also valid:

if <condition>:
    Do this if <condition> is True

Again, indentation matters.

bit.get_color() returns the color of the square bit is currently sitting on.

bit = bit.new_world(1,1)
print(bit.get_color())

bit.paint("blue")
print(bit.get_color())

bit.erase()
print(bit.get_color())

None blue None

None has a special meaning in python. It’s a way of saying “nothing”. You’ll see it used more as we go along.

Revisiting expressions

print(2)

2

print(1 + 1)

2

print(1 + 2 + 3 - 4)

2

In these expressions, I’m essentially telling the computer:

“Take these symbols, do the work they represent, and bring the result back to me.”

print(1 + 2 + 3 - 4)

becomes

print(2)

In python, the name of the function (e.g. bit.get_color) represents some series of work, just like + represents the work of addition.

When you call a function (e.g. bit.get_color()), you’re telling the computer:

“Do the work that bit.get_color represents, then bring the result back to me.”

When a function has nothing to return, it returns None.

print(bit.left())

None

So, what does print return? How could we find out?

Comparing values

while bit.front_clear():
    bit.move()
    if bit.get_color() == "blue":
        bit.paint("green")
    else:
        bit.paint("blue")

When we want to compare two values—i.e. we want to ask “are these the same?”—we use ==.

Yes, there are two = in ==.

print("blue" == "blue")

True

print("blue" == "red")

False

print(None == "blue")

False

What about asking “are these different?”

Use !=

print("blue" != "red")

True

print("blue" != "blue")

False

== and != in action!

if bit.get_color() == "green":
    # Run this if the current color is green
...

if bit.get_color() == None:
    # Run this if there is no color at the current square

...

if bit.get_color() != "red":
    # Run this if the color isn't red (i.e. the color is None, "green", or "blue")

Our Bag of Bricks

We now have a growing bag of Lego bricks

  • move, left, right
  • paint
  • front_clear
  • get_color
  • ==
  • while
  • if

Let’s put these together to build something great.

Fix the tree

bit = Bit.load("tree-before.txt")
bit.draw()

png

bit = Bit.load("tree-after.txt")
bit.draw()

png

When solving problems like these, don’t try to do it all in your head!

Draw pictures.

Write it out.

Let’s break the process down.

Start with the beginning

image.png

What is the next goal—what does that look like?

image.png

What code can get Bit from the start to the first goal?

Q: What do we want to do?

A: Move Bit from the start to the red square.

“Move Bit until the square is red”

OR

“While the current square is not red, move the Bit”

How do we know what color the square is?

bit.get_color()

How do we know whether that color is not red?

bit.get_color() != "red"

How do we move the bit while the color is not red?

while bit.get_color() != "red":
    bit.move()

Let’s try what we have so far

from byubit import Bit
bit = Bit.load("tree-before.txt")
bit.draw()

# Fill it in

bit.draw()
GO TEAM! 💪🏻

What’s the next goal?

We just moved to the red square.

Now we want to move to the green square, but paint as we go.

bit = Bit.load("tree-before.txt")
bit.draw()

# Move to the red square
while bit.get_color() != "red":
    bit.move()
bit.draw()

# Move up to the green, painting red as we go


big.draw()

Oops!

What happened?

bit.draw()

png

That’s not quite what we expected.

Rather than ask: “Why didn’t this work”, let’s ask “What sequence of steps did Bit take to get here”?

Let’s look at the code:

# Move to the green square and paint
bit.left()
while bit.get_color() != 'green':
    bit.move()
    bit.paint('red')
bit.draw()

How does the paint red and while condition interact?

  • Am I on a green color?
    • No, I’m on a red color, so move and paint red
  • Am I on a green color?
    • No, I’m on a red color, so move and paint red
  • Am I on a green color?
    • No, I’m on a red color, so move and paint red
  • Am I on a green color?
    • No, I’m on a red color, so move and paint red
🤔

Solution

So, we’re cobbering the color of the square with red before we check whether it is green.

How do we fix it?

There are several ways we could address it.

Let’s say we only want to paint empty squares red. Then we’ll never clobber a colored square.

# Solution - code live
bit = Bit.load("tree-before.txt")
bit.draw()

# Move to the red square
while bit.get_color() != "red":
    bit.move()
bit.draw()

# Move to the green square and paint
bit.left()
while bit.get_color() != 'green':
    bit.move()
    if bit.get_color() == None:
        bit.paint('red')
bit.draw()

png

png

png

bit = Bit.load("tree-before.txt")
bit.draw()

# Move to the red square
while bit.get_color() != "red":
    bit.move()
bit.draw()

# Move to the green square and paint
# Something isn't working right...please fix it.
bit.left()
while bit.get_color() != 'green':
    bit.move()
    bit.paint('red')
bit.draw()

Other solution

We could add a bit.move() before our second while block and then switch the order of bit.move() and bit.paint() in the while block.

bit = Bit.load("tree-before.txt")
bit.draw()

# Move to the red square
while bit.get_color() != "red":
    bit.move()
bit.draw()

# Move to the green square and paint
# Something isn't working right...please fix it.
bit.left()
while bit.get_color() != 'green':
    bit.move()
    bit.paint('red')
bit.draw()

Either solution is good.

Each person will have an individual preference for which solution they prefer.

That’s OK!

Thinking, Drawing, Coding

Use drawings to help yourself understand the task at hand and the current state of your code.

This isn’t a “beginner’s crutch”—seasoned professionals do this too!

More bricks for the bag

Clear?

bit.left_clear()
bit.right_clear()

These are like bit.front_clear(), but checks on the side.

Blocked squares

Sometimes Bit will encounter blocked squares. They are colored black.

bit = Bit.load("blocked-squares.txt")
bit.draw()

print("Front clear: ")
print(bit.front_clear())

print("Left clear: ")
print(bit.left_clear())

print("Right clear: ")
print(bit.right_clear())

png

Front clear: False Left clear: True Right clear: False

bit = Bit.load('while-right-clear.txt')
bit.draw()

while bit.right_clear():
    bit.move()

bit.draw()

png

png

not

not in python changes a True to False and a False to True.

while not bit.right_clear():
    bit.move()
print(not True)

False

Bit Puzzles

https://byucs110.org/resources/bit-puzzles/

Reverse Coyote

Remember Wiley Coyote from the old cartoon Roadrunner?

He’s always running off of cliffs and suspends in the air for a moment before falling.

Sometimes he’s able to run back to the cliff before he falls. Let’s help him out.

bit = Bit.load("reverse-coyote-start.txt")
bit.draw()

png

Bit.load("reverse-coyote-finish.txt").draw()

png

What is the test for this? Under what conditions do we want Bit to move?

bit = Bit.load("reverse-coyote-start.txt")

# fill in the rest!

bit.compare(Bit.load("reverse-coyote-finish.txt"))
    Color at 0,2 does not match: None vs green
    Color at 1,2 does not match: None vs blue
    Color at 2,2` does not match: None vs blue
    Color at 3,2 does not match: None vs blue
    Location of Bit does not match: (0, 2) vs (3, 2)

png