The design recipe

Review: sum-of-squares implementations

Last time we saw two implementations of a function to sum the squares of numbers in a list:

import lists as L

fun sum-of-squares1(lst :: List<Number>) -> Number:
  cases (List) lst:
    | empty => 0
    | link(fst, rst) => (fst * fst) + sum-of-squares1(rst)
  end
end

fun sum-of-squares2(lst :: List<Number>) -> Number:
  L.sum(L.map(lam(x): x * x end, lst))
end

Which is better? See the lecture capture for discussion details.

The list function template

We’ve now seen a number of functions for processing lists. All of these functions share a common structure, which we call a template:

fun <function-name>(<arguments, including lst>) -> <return type>:
  cases (List) lst:
    | empty => <empty case>
    | link(fst, rst) <some processing on fst> <combined with> function-name(rst)
  end
end

The design recipe

Throughout the course, we’ve been informally discussing the sequence of steps we take when writing programs. As we start solving larger problems, it’s useful to formalize that sequence of steps. We call these steps the “design recipe.”

  1. Write the name, inputs, input types, and output type for the function.
  2. Write some examples of what the function should produce. The examples should cover all structural cases of the inputs (i.e., empty vs non-empty lists), as well as interesting scenarios within the problem.
  3. Identify the tasks that the problem requires. Label each task with some information on how you will handle it (i.e., use a built-in function like L.filter, write a new function). If you are writing a new function, note the inputs and output for the task, along with the types.
  4. For each task that requires you to write a function, start by copying the template for the main function input (i.e., the list template).
  5. Fill in the templates and bodies for all task functions.
  6. Combine the code for the tasks into code for the entire problem.

It can often be tempting to skip steps here (do we really need to write out examples?), but we strongly recommend not doing so.

Example: compute the average number of vowels in a list of words

fun average-vowels(lst :: List<string>) -> Number:
  ...
end
  • Define the set of vowels
  • Get a list of characters in a word [could use string-explode]
  • Count how many vowels are in a given word [could use L.filter, L.member, and L.length with the results from the previous two steps].
  • Sum the total number of vowels across all words in a list [could write a new function total-vowels that takes a list and returns a number].
  • Count the words in the list [could use L.length]
  • Compute the average number of vowels from the previous two values [using division].

Data organization and data types

Imagine that we’re doing a study on communication patterns between students at a particular higher education institution. We have data on text messages: the sender, recipient, day of week, and time sent for a number of messages (we don’t have the message contents, so as to preserve some modicum of privacy). How should we store these data?

We could imagine having a table:

sender :: String recipient :: String day :: String time :: ?
“4015551234” “8025551234” “Mon” ???

What should we use as the type of the time column? What would its values look like? We could imagine a number of possibilities:

  • String, such as "4:55"
  • Number, representing minutes or seconds since midnight
  • Multiple columns representing hours and minutes (and maybe seconds)

There are tradeoffs between using a single column and using multiple columns:

  • If we use multiple columns, we can access the components independently
  • If we use a single column, all of the “time” data are in one place
  • Others?

Pyret (like most languages) provides a feature we can use to resolve this tradeoff: custom data types! Let’s take a look at how we could define a time data type:

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)

A data type has several parts:

name
The name of the data type (TimeData)
constructor
a function that builds the data type (time)
components
the parts of the data (hour, minute)

We can write functions over data types:

fun appropriate-brunch-time(tm :: TimeData) -> Boolean:
  doc: "returns true if tm is an appropriate time to eat brunch"
  (tm.hour > 9) and (tm.hour < 13)
where:
  appropriate-brunch-time(time(0, 0)) is false
  appropriate-brunch-time(time(12, 0)) is true
  appropriate-brunch-time(time(8, 59)) is false
end

So now our table looks like:

sender :: String recipient :: String day :: String time :: TimeData
“4015551234” “8025551234” “Mon” time(2, 55)