The design recipe
Data types review
Here’s the TimeData data type we defined last time.
data TimeData: | time(hour :: Number, minute :: Number) end
We can call time
to build times:
> noon = time(12, 0) > half-past-three = time(3, 30)
How would we write an earlier
function?
fun earlier(tm1 :: TimeData, tm2 :: TimeData) -> Boolean: ... where: earlier(time(0, 0), time(0, 1)) is true earlier(time(0, 1), time(1, 0)) is true earlier(time(1, 3), time(1, 2)) is false earlier(time(1, 0), time(0, 3)) is false end
We’ll need to use both hours and minutes:
fun earlier(tm1 :: TimeData, tm2 :: TimeData) -> Boolean: (tm1.hour < tm2.hour) or ((tm1.hour == tm2.hour) and (tm1.minute < tm2.minute)) where: earlier(time(0, 0), time(0, 1)) is true earlier(time(0, 1), time(1, 0)) is true earlier(time(1, 3), time(1, 2)) is false earlier(time(1, 0), time(0, 3)) is false end
The program dictionary
Way back when we first learned about functions, we talked about a model for how Pyret evaluates function calls: substitution. In the substitution model, when Pyret evaluates a call like
> earlier(time(1, 3), time(1, 2))
Pyret immediately substitutes the values into the body of earlier
:
(time(1, 3).hour == time(1, 2).hour) or ((time(1, 3).hour == time(1, 2).hour) and (time(1, 3).minute < time(1, 2).minute))
As illustrated above, data types make the substitution model less
appealing. Imagine if TimeData
had more components (seconds, dates, months,
years, etc.). Substituting the values of tm1
and tm2
in everywhere would
become quite painful.
Instead, we’re going to start using a slightly different model for function
evaluation, using the program dictionary. As we’ve talked about, the program
dictionary records the names and functions that have been defined at the top
level. So before our call to earlier
, the program dictionary might look like
this:
name | value |
---|---|
noon |
time(12, 0) |
half-past-three |
time(3, 30) |
earlier |
our function |
In this new model (which is much closer to how Pyret actually works!), when we evaluate a function call, Pyret:
- extends the program dictionary with new names binding the parameters to the arguments
- evaluates the function body with this new program dictionary
So, what happens when we call earlier(time(1, 3), time(1, 2))
? Pyret extends
the program dictionary so that it looks like this:
name | value |
---|---|
noon |
time(12, 0) |
half-past-three |
time(3, 30) |
earlier |
our function |
tm1 |
time(1, 3) |
tm2 |
time(1, 2) |
Pyret then evaluates the body of earlier
using this program dictionary. When
it sees tm1
, it looks up its value and finds time(1, 3)
.
What’s missing here? What happens if we do this?
> earlier(time(1, 3), time(1, 2)) false > tm1
As we’ve seen, Pyret will throw an error. Why does it do that, if we’ve added
tm1
(and tm2
) to the program dictionary? Pyret must remove the new entries
after a function call ends.
So, we have our new model for function call evaluation:
- Add entries to the program dictionary binding parameter names to function arguments
- Evaluate the body of the function using this new dictionary
- Remove the bindings we added in step 1
This new model has some advantages over our substitution model:
- Reasoning about large data types is easier
- It’s much more similar to how Pyret actually works
- It’s flexible enough to work for Python as well, as we’ll see!
Exercise: Designing a calendar
Let’s build a calendar! Say we want to represent a collection of calendar appointments, each of which has:
- Date
- Start time
- Duration
- Description
Design a data structure using lists, tables, and/or new datatypes, to represent our calendar. Any time you’re using a list or a table, be specific about the type of its contents or its columns; if you need a new datatype, specify its components.
Designs:
List<Event> data Event:
event(date :: Date, …) |
end
table: date: …, time: …