Skip to main content

Day 5 - Conditionals, Tests, and spy

Skills: 1

Pre-reading: 3.4

Intro (15 mins)

Goal Learn about boolean values, and conditional behaviour in programs, and also how to write function examples with where and to observe outputs with spy.

  • Another common form of data, in addition to numbers, strings, and images, are "true/false" values. These values, called "booleans" (after an Irish mathematician who studied them, in obscurity), have only two possibilities. In Pyret, written as:

    true
    false
  • This form of data allows us to express options, or choices. For example, we might want to ask whether the temperature is above 80F/27C, and if so, wear a sun hat.

  • This requires two things -- first, to be able to compare numbers (a current temperature, in F for example, and 80) -- there are many operators on numbers (and strings, etc) that return booleans -- and second, to use the result of that comparison (a boolean) and do different things in your program depending on the result -- this is the if expression.

  • Let's design that function, "choose-hat".

  • Step 1 -- type annotation -- fun choose-hat(temp-in-F :: Number) -> String -- since the result should be the hat I'm wearing, I'll return a String. Step 2 -- doc string -- doc: "determines appropriate head gear, with above 80F a sun hat, below nothing". Now we want to write our function. But how exactly should it work? Our third step, which we haven't seen before, is to write down examples, in the form of tests cases, of how the function should behave. We can include these in a where block for the function, and end up with this partially written function:

    fun choose-hat(temp-in-F :: Number) -> String:
    doc: "determines appropriate head gear, with above 80F a sun hat, below nothing"

    where:
    choose-hat(50) is "no hat"
    choose-hat(85) is "sun hat"
    end
  • Is there another number we should check? Probably 80 -- it's a boundary condition (i.e., right on the edge), which are commoon sources of bugs. If we add a test for this:

    fun choose-hat(temp-in-F :: Number) -> String:
    doc: "determines appropriate head gear, with above 80F a sun hat, below nothing"

    where:
    choose-hat(50) is "no hat"
    choose-hat(85) is "sun hat"
    choose-hat(80) is "sun hat"
    end
  • Now, we can proceed to Step 4 of our design recipe, which is to write code:

    fun choose-hat(temp-in-F :: Number) -> String:
    doc: "determines appropriate head gear, with above 80F a sun hat, below nothing"
    if temp-in-F > 80:
    "sun hat"
    else:
    "no hat"
    end
    where:
    choose-hat(50) is "no hat"
    choose-hat(85) is "sun hat"
    choose-hat(80) is "sun hat"
    end
  • Now when we run Run, Pyret reports the results of running these tests. We can see one of our tests fails. What went wrong?

  • We can think about it to debug, but we can also use another tool that Pyret gives us to figure out, called spy. This allows us to see expressions as code is running. The simplest form is to give it the names of identifiers, i.e.:

    fun choose-hat(temp-in-F :: Number) -> String:
    doc: "determines appropriate head gear, with above 80F a sun hat, below nothing"
    spy:
    temp-in-F
    end
    if temp-in-F > 80:
    "sun hat"
    else:
    "no hat"
    end
    where:
    choose-hat(50) is "no hat"
    choose-hat(85) is "sun hat"
    choose-hat(80) is "sun hat"
    end
  • But probably we want to know the comparison. We can either define a new name for that:

    fun choose-hat(temp-in-F :: Number) -> String:
    doc: "determines appropriate head gear, with above 80F a sun hat, below nothing"
    comparison = temp-in-F > 80
    spy:
    temp-in-F,
    comparison
    end
    if comparison:
    "sun hat"
    else:
    "no hat"
    end
    where:
    choose-hat(50) is "no hat"
    choose-hat(85) is "sun hat"
    choose-hat(80) is "sun hat"
    end
  • Or, since that pattern is common, spy allows us to name expressions used just in the spy, as:

    fun choose-hat(temp-in-F :: Number) -> String:
    doc: "determines appropriate head gear, with above 80F a sun hat, below nothing"
    spy:
    temp-in-F,
    comparison: temp-in-F > 80
    end
    if temp-in-F > 80:
    "sun hat"
    else:
    "no hat"
    end
    where:
    choose-hat(50) is "no hat"
    choose-hat(85) is "sun hat"
    choose-hat(80) is "sun hat"
    end
  • The difference between the last two is that in the former, comparison is added to the program directory inside choose-hat, but in the latter, the comparison name is only used for the spy expression.

  • Now we can see what went wrong: our comparison is checking that temp-in-F is strictly greater than 80, but our test shows what we want it to be greater than or equal to 80.

Class Exercise (40 mins)

  • Add another possibility to choose-hat, that if the temperature is below 50, you choose a winter hat. Any time you are changing code, you should revisit each step of the design recipe, in order, and change as appropriate (type signature, doc string, tests, code). To have more than one comparison, use:
    if condition:
    ...
    else if condition:
    ...
    else if condition: # any number of times
    ...
    else
    ...
    end
  • Now imagine you got new sunglasses, and want to design a function add-glasses that takes an outfit (as a string) and always adds ", and glasses" to what you will wear.
  • Let's now design a new function, choose-outfit, that takes as input the temperature in fahrenheit, and uses the two functions you just wrote to compute a final outfit. There are at least two ways of doing this -- one defines a new local name with the result of calling choose-hat, and the other directly passes the result of calling choose-hat to add-glasses
  • Now, add a new function choose-hat-or-visor that takes not only a temp-in-F, but also an additional argument, has-visor, which is a boolean that indicates whether the person owns a visor. If they do, and the temperature is above 95F, they will wear that. Your function should be able to re-use logic from choose-hat. The boolean operator and, written boolean1 and boolean2, and evaluates to true if both inputs to it are true, will probably be useful! (Pyret also has or, which won't be useful in this problem, a a function not, i.e., not(boolean1) that evaluates to false when boolean1 is true and vice versa).

Wrap-up (10 mins)

Goal Motivate full design recipe

  • There is a useful order we should carry out writing new functions, and we can make this concrete as a Design Recipe for functions:

    1. Type Annotation - Once we have decided we need to write a function, the first thing we should think about, assuming we were not told it, is what type of inputs the function should take and what type of output it should produce. While we introduced type annotations as a way to get better error messages, and indeed they are, this use as part of a design process is indeed much more important: it helps us organize our thoughts!

    2. Doc String - After figuring out the types of data the function operates over, it is worth writing down what the function should do! This is in English, and it doesn't need to include how the function works (and usually shouldn't) -- the purpose of the explanation is so that someone can understand how to use the function, not how it works internally. While this serves as useful documentation, it is even more useful as part of a design process, since like type annotations, it helps us organize our thoughts -- in particular, it makes us think a little bit more concretely about how the code should work -- i.e., how is the output constructed from the input.

    3. Tests -- The third step is writing tests is a where block -- these examples serve as the most concrete step of our design process, as its the point where we think through concrete cases of inputs, and exactly the output they should produce. It may be, as we write them, that we realize we did not think of something in the earlier steps, which is fine -- we go back and fix those and then continue forward. In addition to forming a useful step in the design process, these tests will also be used to check that our function works as expected!

    4. Code -- The last step, which we should only do after the first three, is writing down the code. There is a strong tendency for students, especially beginners, to want to jump ahead -- but the clarity that comes from doing the earlier steps will make writing the code easier, to the point that it is faster to not skip ahead.