Lecture notes: How functions evaluate, introduction to tables

How functions evaluate

How does Pyret evaluate the following program?

fun f(x :: Number):
  x + 3
end

fun g(y :: Number, z :: Number):
  y - z
end

f(f(g(32, 4)))

Pyret evaluates function calls by:

  1. Matching the inputs in the call to the function’s parameters
  2. Substituting these inputs for the matching parameters in the function’s body
  3. Evaluating the resulting expression.

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 to substitute the inputs for the function’s parameters, 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; in lab you’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-2018.arr", "1XxbD-eg5BAYuufv6mLmEllyg28IR7HeX")

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.

We can get the nth row of the data:

> municipalities.row-n(0)
> municipalities.row-n(1)
> municipalities.row-n(0)["name"]

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)

We can also find all of the cities:

fun is-city(r :: Row) -> Boolean:
  doc: "get the value in the row's city column"
  r["city"]
end

> filter-by(municipalities, is-city)

Note what we’re doing here: we’re passing a function to another function. How is is-city used?

We can find municipalities whose population went down:

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

> filter-by(municipalities, population-decreased)

And, we can combine all of these operations. How would we get the city with the smallest population?

> sort-by(filter-by(municipalities, is-city), "population-2010", true).row-n(0)