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 aString
. 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 awhere
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 thespy
, 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 insidechoose-hat
, but in the latter, thecomparison
name is only used for thespy
expression. -
Now we can see what went wrong: our comparison is checking that
temp-in-F
is strictly greater than80
, 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 below50
, 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 callingchoose-hat
, and the other directly passes the result of callingchoose-hat
toadd-glasses
- Now, add a new function
choose-hat-or-visor
that takes not only atemp-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 fromchoose-hat
. The boolean operatorand
, writtenboolean1 and boolean2
, and evaluates totrue
if both inputs to it aretrue
, will probably be useful! (Pyret also hasor
, which won't be useful in this problem, a a functionnot
, i.e.,not(boolean1)
that evaluates tofalse
whenboolean1
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:
-
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!
-
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.
-
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! -
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.
-