On this page:
2.1 Steps
2.1.1 Problem statement
2.1.2 Data definition
2.1.3 Function name
2.1.4 Parameter list
2.1.5 Function stub
2.1.6 Purpose statement
2.1.7 Examples and expected results
2.1.8 Function body
2.1.9 Testing
2.1.10 Review and revise
2.2 Generalizing the Function
2.3 Exercises
8.2

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.

Informal data definition (given as a comment):

# :Int represents hours worked

# :Int represents wage in cents

2.1.3 Function name

Conceive a descriptive function name. This should ideally be a short non-abbreviated name. You may revise the name and find a better name in the last step.

hours-to-wages:

2.1.4 Parameter list

Write down the function signature as a parameter list. The parameter names and types go left of the arrow (comma separated if you wish). The result type goes right of the arrow. The parameter names should ideally be descriptive, short, and non-abbreviated.

hours-to-wages: (hours :Int -> :Int)

2.1.5 Function stub

Write down the function stub, returning an arbitrary value from the range of the function. Check that the code is parsed without error.

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