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.”
- Write the name, inputs, input types, and output type for the function.
- 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.
- 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.
- 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).
- Fill in the templates and bodies for all task functions.
- 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) |