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:

  1. Add entries to the program dictionary binding parameter names to function arguments
  2. Evaluate the body of the function using this new dictionary
  3. Remove the bindings we added in step 1

This new model has some advantages over our substitution model:

  1. Reasoning about large data types is easier
  2. It’s much more similar to how Pyret actually works
  3. 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: …