On this page:
5.1 Steps
5.1.1 Problem statement
5.1.2 Data definition
5.1.3 Function name
5.1.4 Parameter list
5.1.5 Function stub
5.1.6 Purpose statement
5.1.7 Examples and expected results
5.1.8 Function body
5.1.9 Testing
5.1.10 Review and revise
5.2 Exercises
8.2

5 Recipe for Intervals

When the domain information that needs to be represented in the program consists of one or more ranges of numbers, then the data definition is concerned with handling intervals. An example would be a taxation scheme in which there is no taxation for goods below 1000 €, moderate taxation between 1 k€ – 10 k€, and high taxation for goods above 10 k€. When dealing with intervals special care needs to be taken to treat the boundary cases correctly. In the example above, is a product of 1000 € still without taxation or does it fall in the next interval? Boundaries can be represented precisely in this way: [0, 1000), [1000, 10000), [10000, inf), where brackets [ and ] denote inclusive boundaries and parentheses ( and ) denote exclusive boundaries: 1000 does not belong to the interval [0, 1000), but 0 does.

Data definitions for intervals typically use symbols to name each interval as well as constants to define the boundaries of the intervals.

tax.pf serves as the example for intervals.t

5.1 Steps

5.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?

#<

A fictitious country has decided to introduce a three-stage sales tax. Cheap items below 1 k€ are not taxed. Goods of more than 10 k€ are taxed at 10%. Items in between are taxed at a rate of 5%. Give a data definition and define a function that computes the amount of tax for a given item price.

>#

5.1.2 Data definition

Define how the domain information should be represented in the program or, vice versa, how to interpret the data (e.g., a number) as real-world information (e.g., a tax rate). Name each interval as a symbol.

# enumeration of tax stages:

# :no-tax, :low-tax, :high-tax

A type definition is used to represent the currency. Domain knowledge is necessary to decide whether :Int is a suitable representation for the currency.

# :Int represents Euro

Capture the interval boundaries as constants.

 1000 :LOW-TAX-BOUNDARY!  # interpret.: price in Euro

10000 :HIGH-TAX-BOUNDARY! # interpret.: price in Euro

5.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.

sales-tax:

5.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.

sales-tax: (price :Int -> :Int)

5.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.

sales-tax: (price :Int -> :Int) {

    0

} fun

5.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.

# Returns the amount of tax for the given price.

5.1.7 Examples and expected results

Write down examples with expected results in the test function. The test examples should cover corner cases (e.g., boundary values) and a typical case of each category (e.g. a value from the interior of an interval). Boolean functions should test positive and negative examples. You may already define constants for use in the implementation. Check that the codeis parsed without errors. (Some tests will fail for the stub.)

Examples:

For a price of 0 €  expect a sales tax of 0 €.

For a price of 537 € expect a sales tax of 0 €.

For a price of 1000 € expect a sales tax of 50 €.

For a price of 1282 € expect a sales tax of 64 €.

For a price of 10000 € expect a sales tax of 1000 €.

For a price of 12017 € expect a sales tax of 1202 €.

Corresponding test cases in test function:

sales-tax-test: {

    0 sales-tax, 0, test=

    537 sales-tax, 0, test=

    1000 sales-tax, 1000 0.05 * round, test=

    1282 sales-tax, 1282 0.05 * round, test=

    10000 sales-tax, 10000 0.10 * round, test=

    12017 sales-tax, 12017 0.10 * round, test=

} fun

 

sales-tax-test

The tax values are rounded and converted to integer numbers, which represent whole Euros.

Note that the test cases cover each of the boundary values of the intervals, and at least one value from the interior of each interval.

5.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 in a helper function. Moreover, a reusable subtask should be outsourced in a helper function (Don’t Repeat Yourself, DRY principle). It is often helpful to write a stub for the helper functions. This way you can already run the program.

# Returns the amount of tax for the given price.

sales-tax: (price :Int -> :Int) { # :Int represents whole Euro

    { 0 price <=  price 1000 <  and } { # :no-tax interval

        0

    }

    { 1000 price <=  price 10000 <  and } { # :low-tax interval

        price 0.05 * round

    }

    { price 10000 >= } { # :high-tax interval

        price 0.10 * round

    }

    { true } { # error: if this line is reached then price < 0

        "sales-tax, error: negative price" err

    }

} cond-fun

The implementation reflects the structure of the data. As specified in the enumeration there are three cases: :no-tax, :low-tax, and :high-tax. These three cases correspond to three intervals: [0, 1000), [1000, 10000), and [1000, inf). For each interval there is a condition that matches one of the intervals. When formulating the conditions care has to be taken to handle the boundary values correctly. In addition, there is an error case for negative prices. Since Euro is represented as integer number, a negative number could be provided, which could lead to an error.

5.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.

The given implementation satisfies all test examples:

tax.pf, line 36: Check passed.

tax.pf, line 37: Check passed.

tax.pf, line 38: Check passed.

tax.pf, line 39: Check passed.

tax.pf, line 40: Check passed.

tax.pf, line 41: Check passed.

All 6 tests passed!

5.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 above implementation should be improved. It still contains the raw interval boundaries and taxation rates. These should be stored as constants. Moreover, the conditions can be simplified because they are processed in sequence from the top.

0.05 LOW-TAX-RATE!

0.10 HIGH-TAX-RATE!

 

# Returns the amount of tax for the given price.

sales-tax: (price :Int -> :Int) { # :Int represents whole Euro

    { price 0 < } { # error

        "sales-tax, error: negative price" err

    }

    { price LOW-TAX-BOUNDARY < } {

        0

    }

    { price HIGH-TAX-BOUNDARY < } {

        price LOW-TAX-RATE * round

    }

    { true } {

        price HIGH-TAX-RATE * round

    }

} cond-fun

Domain knowledge might suggest to include a constant for the low-taxation category as well. There could be a revision of the taxation system, in which the first interval of [0, 1000) Euro becomes subject to a small taxation rate as well. Moreover, an additional interval for higher-priced items could be introduced, which would require adding a case. It is up to the programmer’s judgment to decide how far the generalization should go, because this can come at a cost. For example, introducing another constant for the first interval could increase the effort to read the code.

If we are satisfied with the function, we can write a reusable template, which can serve as a basis when implementing functions related to this taxation scheme in the future.

0.05 LOW-TAX-RATE!

0.10 HIGH-TAX-RATE!

 

fn-for-tax: (price :Int -> ...) { # :Int represents whole Euro

    { price 0 < } { # error

        "sales-tax, error: negative price" err

    }

    { price LOW-TAX-BOUNDARY < } {

        ...

    }

    { price HIGH-TAX-BOUNDARY < } {

        ...

    }

    { true } {

        ...

    }

} cond-fun

This template can be adapted for new functions that operate on this data type.

5.2 Exercises