Introduction to Programming and Problem-solving

CS 161




The Design Recipe

  1. Write down a purpose statement
  2. Write down a header
  3. Write some examples
  4. Turn examples into spec
  5. Take inventory
  6. Write the code
  7. Check the code
  8. Clean up




The Design Recipe — With Specifications

The design recipe is a step-by-step process that will help you design and organize your programs.   The idea of a design recipe was introduced by Felleissen et al. in their book How to Design Programs.   For students who don't like to be stuck looking at a blank screen, the design recipe tells you how to get started.  If you are uncertain how to express something in Grace, the design recipe will help by separating what you want to express from how to do it in Grace.

Many experienced designers use a process very much like the design recipe for designing programs that do important real-world jobs, like tracking your bank balance, or compressing music into MP3.  Still, we won’t pretend that this recipe captures the only way of designing a program: there are as many ways of designing programs as there are of ... well, of cooking eggs!  Nevertheless, in this course we will frequently require you to follow the design recipe, because this will help you to make progress; it will also help your instructors to see what you understand, and where you are having trouble.

One of the best features of the design recipe is that it supports automated checking that your code does what you state it should do.  Some teachers ask
their students to write down examples, print out results, and then manually check that the printout is what the examples say is expected.  This is repetitive, boring and error-prone.  It’s probably so boring that you would not bother to do it every time you made a change to your code.  Which may cause you to turn in methods with comments that don’t actually tell the reader what the method does.

Tasks that are repetitive, boring and error-prone are ideal candidates for automation — in other words, they are things that we should ask the computer to do for us.  That’s exactly what the minispec specification  framework for Grace does: it automates the process of checking that the code you write actually works the way that you say it does.  minispec is part of the beginningStudent dialect.

For the present, we will be expecting that your programs will be collections of Grace methods, and the design recipe therefore assumes that you will start by designing one or more methods.  The major steps in the design recipe are in the box in the left sidebar.

  1. Write down a purpose statement for the method that you are developing.

A purpose statement is a Grace comment that summarizes the purpose of the method in a single line.  It should be a short but precise answer to the question "what does this method compute?"   It should tell someone reading your program what the method does without them having to read the code.  Here is an example purpose statement:


    // Returns the Fahrenheit equivalent of a number representing a temperature in degrees Celsius.

 
It should be a phrase ending with a period, and should be written as a description of what the method does (so we say "Returns the ..." rather than "Return the")..


  1. Write down a header for the method.

The header tells Grace that you are defining a method, gives the method a name, says (implicitly) how many parameters the method will have, and gives the parameters names too.  Use the parameter names in the method comment.  For example:


dialect "beginningStudent"

method celsiusToFahrenheit(cTemperature:Number) -> Number { // Returns the Fahrenheit equivalent of `cTemperature`, in degrees Celsius. }



  1. Write some examples illustrating how your method might be used, and what the result should be if there is a result.  Put these examples in the method comment too.

Notice that for the method celsiusToFahrenheit we can easily write down what we expect the method to return, because it is a method that returns a value.

dialect "beginningStudent"
method celsiusToFahrenheit(cTemperature:Number) -> Number { // Returns the Fahrenheit equivalent of `cTemperature`, which is a number in degrees Celsius. // Examples: // expect celsiusToFahrenheit (0) to be 32.0
// expect celsiusToFahrenheit (100) to be 212.0
// expect celsiusToFahrenheit (-40) to be (-40.0) }

At this point you may well realize that you have been sloppy when you wrote your purpose statement, and have left out important details.  You may also find that the name that you chose doesn't look so good when you see it being used, in a method request.   If so, go back and change the name.

Specifying what a method should do

This is the fourth step in the design recipe:

  1. Turn the examples in your method comment into an executable specification.

Now we take those examples and turn them into a specification of what the method should do.  To write the specification, we use some more features of the "beginningStudent" dialect.  Each example from the comments becomes a specify statement that describes one case of the method's behaviour.  We group the individual specification cases into a description.  The following description captures what celsiusToFahrenheit should do.

dialect "beginningStudent"

method celsiusToFahrenheit(cTemperature:Number) -> Number {
// Returns the Fahrenheit equivalent of `cTemperature`, which is a number in degrees Celsius.

// Examples:
// celsiusToFahrenheit(0) should be 32.0
// celsiusToFahrenheit(100) should be 212.0
// celsiusToFahrenheit(-40) should be -40.0
}

describe "temperature conversion" with { specify "zero Celsius" by { expect (celsiusToFahrenheit 0) toBe 32.0 } specify "100 Celsius" by { expect (celsiusToFahrenheit 100) toBe 212.0 } specify "-40 Celsius" by { expect (celsiusToFahrenheit (-40)) toBe (-40.0) } }


Running your specification

The wonderful thing about a specification written like this is that Grace can run it: it can be used to check that your code actually does what you expect.  Click the run button!  That's right: you can try try out the specification before you have written any code!  Naturally, you will get some error messages , since you haven't actually written any code yet.  You should see some output like this:

temperature conversion: 3 run, 0 failed, 3 errors
Errors:
-40 Celsius: TypeError: result of method celsiusToFahrenheit(_) is not of type Number. It's missing methods %(_), &(_) ...
 
Here Grace is telling you that the method celsiusToFahrenheit(_) — which you specified should return a Number— did not do so.  This is because celsiusToFarenheit(_) finished and returned done — not surprising, since there is nothing in the method body except the comment.  It is good that we got these three errors: they tell us that the three specifications are being checked.

Now you are in a position to proceed with the next three steps of the design recipe:

  1. Take inventory: stop and think about what you know (the parameters), what the method has to do, and what existing Grace code you can call on to help you. 

  2. Write the code to implement the method!

  3. Check the code that you have written against the specification.

Remember, all three of these steps are in the same box because sometimes it will be obvious how to write the code once you have taken inventory, and sometimes it will be far from obvious.  Inside celsiusToFahrenheit, you know the value of the parameter, cTemperature. You also know that 0°C is 32°F and that the Fahrenheit degree is 5/9 of the size of the Celsius degree. So, after a moment's thought, you might realize that

(cTemperature * 5 / 9) + 32

expresses the Fahrenheit temperature, and quickly move to completing the method definition:

dialect "beginningStudent"

method celsiusToFahrenheit(cTemperature:Number) -> Number {
    // Returns the Fahrenheit equivalent of cTemperature, a number in degrees Celsius.

  return (cTemperature * 5 / 9) + 32 } describe "temperature conversion" with {
specify "zero Celsius" by {
expect (celsiusToFahrenheit 0) toBe 32.0
}
specify "100 Celsius" by {
expect (celsiusToFahrenheit 100) toBe 212.0
}
specify "-40 Celsius" by {
expect (celsiusToFahrenheit (-40)) toBe (-40.0)
}
}


Notice that we can take the examples out of the method body, because the same examples are now captured in the description.  Now we can ask grace to check the code against the description again:


3 run, 2 failed, 0 errors
Failures:
    -40 Celsius: ‹9.777777777777779› should be ‹-40›
    100 Celsius: ‹87.55555555555556› should be ‹212›


Notice that Grace tells you about the failures but not the successes; the the code followed the first case of the specification but Grace was silent.   Can you see the cause of the failures?  Often, the output from a specification that disagrees with the code will be sufficient to pinpoint your bug; sometimes, you will need to put in print statements.  In this case, it is not too hard; the Celsius degree is larger than the Fahrenheit degree, so we need more Fahrenheit than we had Celsius; the correct conversion factor is 9/5, not 5/9.   This also explains why our method worked for 0 Celsius.

Here is the improved version:


dialect "beginningStudent"

method celsiusToFahrenheit(cTemperature:Number) -> Number {
    //Returns the Fahrenheit equivalent of cTemperature, in degrees Celsius.

    return (cTemperature * 9/5) + 32
}

describe "temperature conversion" with {
specify "zero Celsius" by {
expect (celsiusToFahrenheit 0) toBe 32.0
}
specify "100 Celsius" by {
expect (celsiusToFahrenheit 100) toBe 212.0
}
specify "-40 Celsius" by {
expect (celsiusToFahrenheit (-40)) toBe (-40.0)
}
}


Let's run the specification again: this time we get:



3 run, 0 failed, 0 errors


To summarize steps 4, 5 and 6: taking inventory and writing your code may happen in sequence if the method is very simple, but most likely will alternate with checking against the specification.   When your checks reveal a problem, look at the resources that you have available to help you solve it.   When all of your checks pass, write some more specification cases.   Try and break your code!

  1. Clean up the code

The final step in the design recipe is to take a look at your code and clean it up.   Did you choose good names for the parameters?  Are those comments useful?  After every change to the code, check it again.   That's the only way to be confident that you didn't accidentally mess up.

The specifications that we have been writing here are in a dialect called minispec, which is part of the beginningStudent dialect.



 



Most recently modified by Andrew P. Black at 16:34 on Monday 10 April 2017