Datatypes with multiple constructors

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.

Some of the designs we came up with:

data Date:
  | date(year :: Number, month :: Number, day :: Number)
end

data Event:
  | event(date :: Date, time :: TimeData, duration :: Number, descr :: String)
end

calendar ::List<Event> = [list: ]
calendar = table: date: Date, ...

Are there any other designs that would work?

Adding TODO items

Many people keep a “to-do” list of tasks they need to do, along with deadlines and other information. Let’s say TODO items have the following data:

  • Task
  • Deadline
  • Urgency

How would we add TODO items to our calendar? We now need to store two types of calendar entries: appointments and TODO items.

We could put everything in one table:

date time duration description task urgency
10/23/2019 1pm 50 “CSCI 0111” “” “”
10/24/2019 8pm 0 “” “Use avocado” “high”

This isn’t great, though–we’re not using all of the fields for all of the entries, and it would be easy to write code that accidentally treats a TODO item like an appointment or vice versa.

We could use two tables:

date time duration description
10/23/2019 1pm 50 “CSCI 0111”
deadline task urgency
10/24/2019 “Use avocado” “high”

Now, though, we’ve lost the benefit of having a single calendar with all of our entries. For many tasks (for instance, displaying entries sorted by date) we want to combine these data sources.

Datatypes to the rescue

Luckily, we can define datatypes with multiple constructors!

data Event
  | appt(date :: Date, time :: Time, duration :: Number, descr :: String)
  | todo(deadline :: Date, task :: String, urgency :: String)
end

Note that we’re still defining one datatype (called Event) but that it has two constructors, each with different components!

So our calendar is now a List<Event>:

calendar = [list:
  appt(date(2019, 10, 23), time(5, 0),
    50, "CSCI 0111"),
  todo(date(2019, 10, 24),
    "Use avocado", "high")]

Let’s say we want to search our calendar events for one matching a particular string–for instance, I might want to find any and all avocado-related events. How can we write a function that processes both appointments and TODO items?

As it turns out, we’ve already seen the syntax to do this:

fun event-matches(event :: Event, term :: String) ->
  cases (Event) event:
    | appt(date, time, duration, descr) =>
      string-contains(descr, term)
    | todo(deadline, task, urg) =>
      string-contains(task, term)
  end
where:
  event-matches(
    appt(date(2019, 10, 25), time(5, 0), 50,
      "Cooking avocados"),
    "avocado") is true
  event-matches(
    appt(date(2019, 10, 25), time(8, 10), 180,
      "Baseball game"),
    "avocado") is false
  event-matches(
    todo(date(2019, 10, 25), "Use avocado", "high"),
    "avocado") is false
end

And we can use it to filter our calendar:

fun search-calendar(calendar :: List<Event>, term: String) -> List<Event>:
  L.filter(lam(e): event-matches(e, term) end, calendar)
end

Enumerating possibilities

Datatypes with multiple constructors are useful when a type has multiple variants. One way to use them is to enumerate a small set of possible values. For instance, we’ve been using strings to indicate TODO urgency, but we could instead define a datatype:

data Urgency:
  | high
  | medium
  | low
end

What advantages does this approach have? What about disadvantages?