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:

  1. Making a new section of the program directory, which Pyret will use when evaluating the function body
  2. Matching the inputs in the call to the function’s parameters
  3. Adding names to the program directory with values corresponding to the function’s inputs
  4. Evaluating the function body
  5. 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)