|
|
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.
-
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")..
- 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.
}
|
- 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:
- 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:
- 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.
- Write the code to
implement the method!
- 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!
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.
|
|