2 Recipe for Atomic Data
Atomic data is data that cannot be broken down further. Examples are integer and floating point numbers, Boolean values, and strings. It could be argued that text strings are not atomic as they can be further subdivided into characters. Yet we treat strings as atomic here.
wages.pf serves as the example for atomic data.
2.1 Steps
2.1.1 Problem statement
Write down the problem statement as a comment. The problem statement should answer these questions: What information needs to be represented? What should the function (to be implemented) do with the data? What cases need to be considered?
#<
Design a function that computes weekly wages with overtime from the number
of hours worked. The hourly rate is 10 €/hour. Regular working time is 40
hours/week. Overtime is paid 150% of the normal rate of pay.
>#
2.1.2 Data definition
Write how the real-world information (also called domain information) should be represented in the program. Conversely, write how to interpret the data (e.g., a number) as real-world information (e.g., degrees Celsius). You may describe the interpretation in a comment.
# :Int represents hours worked
# :Int represents wage in cents
2.1.3 Function name
hours-to-wages:
2.1.4 Parameter list
hours-to-wages: (hours :Int -> :Int)
2.1.5 Function stub
hours-to-wages: (hours :Int -> :Int) {
0
} fun
2.1.6 Purpose statement
Write down a purpose statement (given as a comment). The purpose statement should describe what the function computes (not how it does that) and should mention the given inputs and produced result.
# Computes the wage in cents given the number of hours worked.
2.1.7 Examples and expected results
Write down examples with expected results in the test function. Define any constants that the function needs. Check that the code is parsed without errors. (Some tests will fail for the stub.)
Examples:
For 0 hours worked, expect 0 cents.
For 20 hours worked, expect 20 * 1000 cents.
For 39 hours worked, expect 39 * 1000 cents.
For 40 hours worked, expect 40 * 1000 cents.
For 41 hours worked, expect 40 * 1000 + 1 * 1500 cents.
For 45 hours worked, expect 40 * 1000 + 5 * 1500 cents.
Corresponding test cases in test function:
hours-to-wages-test: {
0 hours-to-wages 0 test=
20 hours-to-wages 20 1000 * test=
39 hours-to-wages 39 1000 * test=
40 hours-to-wages 40 1000 * test=
41 hours-to-wages 40 1000 * 1 1500 * + test=
45 hours-to-wages 40 1000 * 5 1500 * + test=
test-stats
} fun
hours-to-wages-test
The test= function takes two arguments: The actual result and the expected result. Each check will report the line number on which it appears. This helps to locate failed tests in the source code. In the web-based development environment, passed tests are marked with a checkmark and failed tests are marked with a cross.
Running the above tests on the stub produces:
wages.pf, line 6: Check passed.
wages.pf, line 7: Actual value 0 differs from expected value 20000.
wages.pf, line 8: Actual value 0 differs from expected value 39000.
wages.pf, line 9: Actual value 0 differs from expected value 40000.
wages.pf, line 10: Actual value 0 differs from expected value 41500.
wages.pf, line 11: Actual value 0 differs from expected value 47500.
5 of 6 tests failed.
2.1.8 Function body
Implement the function body. Put required helper functions on a wish list. These will be implemented later.
How to identify the need for a helper function: A function should perform one well-defined task. A change in task or data type should be outsourced to a helper function. Moreover, a reusable subtask should be outsourced in its own helper function. The repetitive work of reimplementing the subtask again and again can thus be avoided. This strategy has been termed the Don’t Repeat Yourself (DRY) principle. It is often helpful to initially just write a stub for the helper functions. This way you can already compile the program to check syntactic correctness.
The implementation of the hours-to-wages function does not need any helper functions. However, it needs an if-operator to decide whether the working time includes overtime.
# Computes the wage in cents given the number of hours worked.
hours-to-wages: (hours :Int -> :Int) {
hours 40 <= {
hours 1000 *
} {
40 1000 * hours 40 - 1500 * +
} if
} fun
2.1.9 Testing
Check that the function body satisfies the tests. Correct the function body (and the tests). Look for opportunities to simplify the structure of the code. This typically requires multiple iterations.
Running the tests on the implemented function produces:
wages.pf, line 14: Check passed.
wages.pf, line 15: Check passed.
wages.pf, line 16: Check passed.
wages.pf, line 17: Check passed.
wages.pf, line 18: Check passed.
wages.pf, line 19: Check passed.
All 6 tests passed!
2.1.10 Review and revise
Review and revise the function name, the parameter names, and the purpose statement. Improve them if necessary. A design is not complete until it has a purpose statement and tests. The purpose statement should describe what the function computes (not how it does the computation) and should mention the given inputs and produced result. The test examples should cover corner cases and a typical case.
The complete source file wages.pf now looks like this:
#<
Design a function that computes weekly wages with overtime
from the number of hours worked. The hourly rate is 10 €/hour.
Regular working time is 40 hours/week. Overtime is paid 150%
of the normal rate of pay.
>#
# Computes the wage in cents given the number of hours worked.
hours-to-wages: (hours :Int -> :Int) {
hours 40 <= {
hours 1000 *
} {
40 1000 * hours 40 - 1500 * +
} if
} fun
hours-to-wages-test: {
0 hours-to-wages 0 test=
20 hours-to-wages 20 1000 * test=
39 hours-to-wages 39 1000 * test=
40 hours-to-wages 40 1000 * test=
41 hours-to-wages 40 1000 * 1 1500 * + test=
45 hours-to-wages 40 1000 * 5 1500 * + test=
test-stats
} fun
hours-to-wages-test
2.2 Generalizing the Function
The example function as written above contains some concrete bits of information, such as the work hours per week, the regular hourly rate, and the hourly rate for overtime. The weekly work hours are repeated multiple times, which contradicts the Don’t Repeat Yourself (DRY) principle. Such information should be represented as constants or as parameters to the function. What is represented as a constant and what is represented as a parameter depends on the needs of the application domain.
One possibility is to extract all information as constants:
40 WEEKLY_HOURS! # regular work hours per week
1000 HOURLY_RATE_REGULAR! # in cents
1500 HOURLY_RATE_OVERTIME! # in cents
hours-to-wages: (hours :Int -> :Int) {
hours WEEKLY_HOURS <= {
hours HOURLY_RATE_REGULAR *
} {
WEEKLY_HOURS HOURLY_RATE_REGULAR *
hours WEEKLY_HOURS - HOURLY_RATE_OVERTIME * +
} if
} fun
Another possibility is to include all information as parameters:
hours-to-wages: (
weekly-hours :Int,
hourly-rate-regular :Int,
hourly-rate-overtime :Int,
hours-worked :Int
-> :Int)
{
hours-worked weekly-hours <= {
hours-worked hourly-rate-regular *
} {
weekly-hours hourly-rate-regular *
hours-worked weekly-hours - hourly-rate-overtime * +
} if
} fun
This makes the function more flexible, but also more difficult to use, because many arguments have to be provided. Typically only the hourly rate and hours worked change frequently – from worker to worker and for different weeks, respectively.
2.3 Exercises
Design a function has-overtime that computes whether or not the given number of work hours contains overtime.
Modify hours-to-wages such that the program outputs an error if hours is negative. The operator "message" err pushes an error object on the stack and stops execution.