How functions evaluate; introduction to tables
How functions evaluate
How does Pyret evaluate the following program?
BASE-COST = 0.25 fun pen-cost(num-pens :: Number, message :: String) -> Number: doc: "cost of ordering pens with slogans" num-pens * (BASE-COST + (string-length(message) * 0.02)) end fun add-shipping(subtotal :: Number) -> Number: doc: "add shipping amount to subtotal" if subtotal <= 50: subtotal + 10 else if (subtotal > 50) and (subtotal <= 100): subtotal + 20 else: subtotal + 30 end end fun total-cost(num-pens :: Number, slogan :: String) -> Number: doc: "compute the total cost of pens, including shipping" add-shipping(pen-cost(num-pens, slogan)) end total-cost(5, "wow")
Pyret evaluates function calls by:
- Making a new section of the program directory, which Pyret will use when evaluating the function body
- Matching the inputs in the call to the function’s parameters
- Adding names to the program directory with values corresponding to the function’s inputs
- Evaluating the function body
- Removing the new section from the program dictionary
We can combine this with what we learned earlier about the order in which Pyret evaluates expressions (top-down, innermost-outermost, left-right) to see how Pyret evaluates a program. See the lecture capture for an example of doing this for the program above.
Note that because the first thing Pyret does when evaluating a function call is add the function’s parameter names to the program directory, the names we choose for parameters don’t matter–at least to Pyret! You should still provide descriptive names to function parameters for readability.
Introduction to tables
So far we have seen programs that manipulate numbers, strings, and images. We’ve also seen Booleans, which are used to represent the answers to yes/no questions. Here are some data that can be represented with what we’ve seen so far:
- A picture of a dog (Image)
- The population of Azerbaijan (Number)
- The mass of Saturn (Number)
- The complete text of the Baghavad Gita (String)
- Whether or not I ate breakfast this morning (Boolean)
There’s a lot we can do with what we’ve seen so far! We can make images; we can compute costs; soon we’ll even see how to make animations! There are still data, however, that we’d have trouble representing. What if we wanted to write a program to look up the population of any town in Rhode Island (as measured in either 2000 or 2010, the last two census years)? Here’s a first attempt at such a program:
fun population(municipality :: String, year :: Number): if municipality == "Providence": if year == 2000: 173618 else if year == 2010: 178042 else: raise("bad year") end else if municipality == "Cranston": if year == 2000: 79269 else if year == 2010: 80387 else: raise("bad year") end else: raise("bad municipality") end end
raise("message")
stops Pyret from evaluating from the program, and displays an
error message to the user. It’s useful when dealing with unexpected function
inputs.
This program does some of what we want it to–we can use it to get population data. However, it has some problems:
- Rhode Island has 39 municipalities. This function handles 2. How would we scale it up?
- What if we want to do something else, like tracking population growth or density? Would we repeat these data in another function?
We can instead represent the same information (as well as some additional data) as a table. Tables are used for tabular data, like you might find in a spreadsheet:
include tables include shared-gdrive("cs111-2020.arr", "1imMXJxpNWFCUaawtzIJzPhbDuaLHtuDX") municipalities = table: name, city, population-2000, population-2010 row: "Providence", true, 173618, 178042 row: "Cranston", true, 79269, 80387 row: "Coventry", false, 33668, 35014 row: "Warwick", true, 85808, 82672 row: "North Providence", false, 32411, 32078 end
Some municipalities are technically “cities,” while others are “towns.” In New England, as opposed to other places in the country, this is mostly just a matter of what a municipality chooses to call itself: see this informative Wikipedia article. This table has 5 of Rhode Island’s municipalities, but it should be clear how to extend it to include the rest (and soon we’ll see how to instead load the information from an external source).
Now that we have the data in Pyret, we can write programs to answer
questions. For these table operations, we’re using our own library instead of
Pyret’s built-in table operations (that’s what that include shared-gdrive(...)
line is doing above). This library is documented here; it’s linked from the
“Software” page on the course website, and we’ll make sure to link to it from
labs, homeworks, and projects where you’re working with tables.
The first thing we might want to do is to get individual rows out of the table:
> municipalities.row-n(0) > municipalities.row-n(1)
The Row
values these operations return store single rows of data. These data
can be accessed by column name:
> municipalities.row-n(0)["name"]
We can write a function that takes a row:
fun population-decreased(r :: Row) -> Boolean: doc: "returns true if the municipality's population went down between '00 and '10" r["population-2010"] < r["population-2000"] end
What happens when we call our function on a couple of the rows from our table?
One thing we might want to do is to get a version of the table that only has cities where the population has decreased. We could try writing something like this:
fun filter-population-decreased(t :: Table): if population-decreased(t.row-n(0)): ... # keep row 0 if population-decreased(t.row-n(1): ... # keep row 0 else: ... end else: ... # don't keep row 0 end end
We might be able to make something like that work, but luckily, Pyret gives us an easier way to write it, which we’ll talk about next time. We can sort the data by 2010 population:
> sort-by(municipalities, "population-2010", false)
To find the least populous municipalities, we can change the last argument:
> sort-by(municipalities, "population-2010", true)