Class summary: Introduction to Lists
Copyright (c) 2017 Kathi Fisler
1 Looking up values by keys
We want a function that takes the name of a person and returns the number of tickets they have ordered:
fun tickets-for(t :: Table, who :: String) -> Number: |
doc: "Extract tickcount value for order with given name" |
... |
where: |
tickets-for(event-data-clean, "Alvina") is 3 |
tickets-for(event-data-clean, "Ernie") is 0 |
end |
We filled in the body as follows:
fun tickets-for(t :: Table, who :: String) -> Number: |
doc: "Extract tickcount value for order with given name" |
matches = filter-by(t, lam(r): r["name"] == who end) |
matches.row-n(0)["tickcount"] |
where: |
tickets-for(event-data-clean, "Alvina") is 3 |
tickets-for(event-data-clean, "Ernie") is 0 |
end |
What happens if we try the following?
tickets-for(event-data-clean,"kathi") |
Our current code assumes that filter-by will return a non-empty table. We should instead check that we got a non-empty table, and raise an error if we did not:
fun tickets-for(t :: Table, who :: String) -> Number: |
doc: "Extract tickcount value for order with given name" |
matches = filter-by(t, lam(r): r["name"] == who end) |
if matches.length() > 0: |
matches.row-n(0)["tickcount"] |
else: |
raise("Tickets-for: table has no row with name " + who) |
end |
where: |
tickets-for(event-data-clean, "Alvina") is 3 |
tickets-for(event-data-clean, "Ernie") is 0 |
tickets-for(event-data-clean, "Kathi") raises "no row" |
end |
The where clause shows how to check whether a call to the function results in an error being raised – rather than write is in the example, we write raises. The string after raises needs to be a substring of the raised error for the test to pass.
2 Lists: Two Motivating Problems
Consider the following two questions:
Is every discount in the table from a valid set of discount codes?
What are the names of everyone with the student discount?
We have an idea of how to write the first one – a filter-by with a helper function that uses or to check the code against a collection of options:
fun check-discounts1(t :: Table) -> Table: |
doc: "filter out rows whose discount code is not valid" |
fun invalid-code(r :: Row) -> Boolean: |
not( |
(r["discount"] == "STUDENT") or |
(r["discount"] == "BIRTHDAY") or |
(r["discount"] == "") or |
(r["discount"] == "EARLYBIRD")) |
end |
filter-by(t, invalid-code) |
end |
There’s something unsatisfying about this solution, though: every time the set of codes changes, we have to change the function. It would be much nicer if the codes could be written independently of the function. Then, the sales department could change the codes without having to bother the programmers every time.
So the real question is how can we rewrite this function so that the set of valid codes is written down outside the function?
3 Lists: a new kind of data for sets
valid-discounts = [list: "STUDENT", "BIRTHDAY", "", "EARLYBIRD"] |
|
fun check-discounts(t :: Table) -> Table: |
doc: "filter out rows whose discount code is not valid" |
fun invalid-code(r :: Row) -> Boolean: |
not(L.member(valid-discounts, r["discount"])) |
end |
filter-by(t, invalid-code) |
where: |
check-discounts(event-data) |
is |
add-row( |
add-row( |
add-row(event-data.empty(), event-data.row-n(3)), |
event-data.row-n(4)), |
event-data.row-n(6)) |
end |
Here is a version written with anonymous functions/lambda.
fun check-discounts2(t :: Table) -> Table: |
doc: "filter out rows whose discount code is not valid" |
filter-by(t, lam(r): not(L.member(valid-discounts, r["discount"])) end) |
where: |
check-discounts2(event-data) |
is |
add-row( |
add-row( |
add-row(event-data.empty(), event-data.row-n(3)), |
event-data.row-n(4)), |
event-data.row-n(6)) |
end |
4 What are Lists?
Lists are one of the key data structures in programming. They feature:
An unbounded number of items
An order on items (first, second, third, ...)
As we will see, there are many built-in operations on lists.
5 Extracting Lists from Tables
Turning to the second question, how could we get a list of names of people with the "STUDENT" discount? (Perhaps we want to validate those names against data from a school).
We know how to filter the table down to only those rows that have "STUDENT" in the discount column. How do we get the names from those rows? We use a table operator called get-column that pulls out the values from a column as a list:
filter-by( |
event-data-clean, |
lam(r): r["discount"] == "STUDENT" end).get-column("name") |
Alternatively, using an intermediate name for the filtered table:
rows = |
filter-by( |
event-data-clean, |
lam(r): r["discount"] == "STUDENT" end) |
rows.get-column("name") |
We’ll do a lot more with lists over the next several classes.