module Functions where
import Question
functions = do
h2 $ text "A programmable calculator"
p <#>During the first lecture, you were showed a method for converting
between different currencies. Here is a similar example:#>
<#>As you perhaps know, the Fahrenheit scale for temperature is still
used in some countries. 0° Celsius (C) corresponds to
32° Fahrenheit (F) and an increase of 5°C corresponds
to an increase of 9°F. How many degrees in Fahrenheit is
28°C?#> ?> do
prompts' [ ("32+28/5*9", "82.4") ]
p <#>The answer is 82.4°F. In what order are the operations
above performed?#>
p <#>We have seen how Hugs can be used for simple calculations. But
this is only the beginning! We can define a function that extends
the abilities of the system. As a first example we consider the
temperature converting problem above. If we wish to repeatedly
calculate how many °F a temperature given in °C
corresponds to, we can define a function that expresses this.
Defining a function is done in several steps:#>
let def = "celsiusToFahrenheit celsius = 32 + celsius/5*9"
ul $ do
li $ p <#>Give the function a name. The name chosen should be an indication
of what the function does. Here we decide on the name
celsiusToFahrenheit. (Note that names of functions must
begin with lower-case letters.)#>
li $ do
p <#>Decide what arguments (input) the function should require and what
result (output) to return. In this case it is easy:#>
ul $ do
li $ p <#>Input: A number, representing a temperature in °C.#>
li $ p <#>Output: A number, representing the corresponding temperature
in °F.#>
li $ p <#>Decide on names for the arguments to the function. Each name
stands for an arbitrary value of the argument. Since
the argument here is a temperature in °C, we decide
to use the name celsius for the first and
only argument.#>
<#>Describe how the result that we want can be computed using
the arguments. How can this be done for the current
example?#> .?> do
p <#>In this case the result is
32 + celsius/5*9.#>
-- TODO: Say something about the missing units?
li $ do
p <#>Write the function definition in Haskell. In this case#>
fixed def
p <#>Before we can use this definition we have to give it to Hugs.
However, we can't give a function definition directly to
Hugs. Instead we must work in two steps:#>
ul $ do
li $ do
p <#>Use a text editor (e.g. emacs) to write the
function definition in a file. Do it,
i.e. start emacs, type in the line#>
fixed def
p <#>and save this in a file with the name
Temperature.hs. (Haskell files usually have the
extension .hs.) Make sure the file is located in the
directory where Hugs was started. If you need to, quit Hugs,
check (with the Unix command ls) that there is a
file with the name Temperature.hs and make sure
(with the Unix command less) that this file has the
correct contents.#>
li $ do
p <#>Give Hugs the command to load this file:#>
prompts "" [ ("Prelude> :l Temperature.hs", "Main>") ]
p <#>Note that the complete name of the command is
:load, thus the symbol after the colon is the letter
l, and not the number 1.#>
p <#>We can now use the definition:#>
prompts "Main>" [ ("celsiusToFahrenheit 28", "82.4")
, ("celsiusToFahrenheit 0", "32.0")
]
p <#>Let's define some other functions. Write them in a new file.#>
ul $ do
<#>Write a function that calculates the area of a circle,
given the circle's radius.#> .?> do
ul $ do
li <#>Name: area.#>
li <#>Input: The radius of the circle.#>
li <#>Output: The area of the circle.#>
li <#>Argument names: radius.#>
<#>The area is pi times the square of the radius, i.e....#> .?>
do
fixed "pi * radius ^ 2"
p <#>Find out information about pi by using :i
pi.#>
<#>In the end the definition becomes...#> .?>
fixed "area radius = pi * radius ^ 2"
<#>Write a function that calulates the radius of a circle, given
the circle's area. (Note that if you write the new definition
in the same file as the old one, then you need to reload the
file in order to use the new definition. This is done by
:r (reload).)#> .?>
fixed "radius area = sqrt (area / pi)"
<#>Write a function that calculates the length of the hypothenuse
of a right-angled triangle, given the lengths of the two other
sides of the triangle.#> .?>
fixed "hypothenuse leg1 leg2 = sqrt (leg1 ^ 2 + leg2 ^ 2)"
-- <#>Write a function that tests whether radius (area r) is
-- equal to r. What happens when you try out your
-- definition?#> .?> do
-- fixed "prop_radiusArea r = radius (area r) == r"
-- p <#>Note that to reuse old definitions they have to be in the
-- same file. (Or you can use import statements, see
-- further down.)#>
<#>In the file Temperature.hs, define a function
fahrenheitToCelsius that converts from degrees
Fahrenheit to degrees Celsius.#> ?> do
fixed "fahrenheitToCelsius fahrenheit = ?"
<#>You were not given the full answer to the last exercise. Is your
new definition correct? There
are many ways to certify the correctness of a function. One is
to prove it. That gives reliable answers, but can take a lot of
time. Another way is to test the function. This method is never
completely reliable unless the function can be tested for all
possible inputs, but it is a cheap way to detect many errors.
Test your new function now.#> ?> do
<#>Of course there are many different ways of testing. One is to
manually test the function giving some different inputs. This,
however, takes a lot of time and effort. Those of you who remember
the lecture know that you can automate the testing.
Read up on how this works, and try it out yourselves.#> ?> do
<#>The first question that comes up is what it is that we want to test. We
want to know whether fahrenheitToCelsius is correct. What
is the meaning of correct in this case?#> ?> do
p <#>We want fahrenheitToCelsius to convert
from °F to °C. Assuming
celsiusToFahrenheit is correct, this means that we
want to show the following:#>
fixed "celsiusToFahrenheit (fahrenheitToCelsius fahrenheit) \
\== fahrenheit"
p <#>The above should be true for all possible inputs
fahrenheit, or at least those that correspond to
actual temperatures. (The function == compares two
values and determines whether they are equal or not.)#>
p <#>When studying functions in the mathematics course,
think about what the domain of definition of
fahrenheitToCelsius should
be. Today we will simply test the function for all possible numerical
inputs, regardless of whether they correspond to real temperatures or not.#>
p <#>Now we know what we want to test. Let's write the property below
in the file Temperature.hs:#>
fixed "prop_fahrenheitToCelsius fahrenheit =\n\
\ celsiusToFahrenheit (fahrenheitToCelsius fahrenheit) \
\== fahrenheit"
<#>In order to test this property, we must type
import QuickCheck at the top of the file. (This will
ensure that the testing library called
QuickCheck is "imported", i.e. becomes
available.) Test your program by typing quickCheck
prop_fahrenheitToCelsius. (Don't forget to reload the
file first.) What happens?#> ?> do
prompts "Main>"
[ ( "quickCheck prop_fahrenheitToCelsius"
, "ERROR - Unresolved overloading\n\
\*** Type : (Fractional a, Arbitrary a) => IO ()\n\
\*** Expression : quickCheck prop_fahrenheitToCelsius"
)
]
p <#>Hugs couldn't figure out what type
prop_fahrenheitToCelsius has, and complains.#>
<#>We must tell Hugs what kind of temperature we wish to use
when testing. "What do you mean, what kind of temperature?"
you may ask. Find out what the type of
fahrenheitToCelsius is.#> ?> do
p <#>To learn the type of an expression, use
:t (:type):#>
prompts "Main>"
[ ( ":t fahrenheitToCelsius"
, "fahrenheitToCelsius :: Fractional a => a -> a"
)
]
p <#>This type means that
fahrenheitToCelsius takes a fraction
(Fractional) as input and returns a fraction as
output.#>
p <#>There are many different types of fractions (more about this later), so
we have to state which one we mean. Let us choose
Ratio Integer first, a type for exact fractions. Add
the type signature#>
fixed "prop_fahrenheitToCelsius :: Ratio Integer -> Bool"
p <#>just before the definition of the property. This type
means that prop_fahrenheitToCelsius takes a
Ratio Integer as input and leaves a Boolean value
(Bool) as output. The function takes a
number as input and responds True or
False depending on whether
fahrenheitToCelsius is correct for the specified
number or not, so it seems reasonable, doesn't it?#>
<#>Add import Ratio to the beginning of the file,
so that you can use Ratio. Test the property again.
What happens now?#> ?> do
prompts "Main>"
[ ( "quickCheck prop_fahrenheitToCelsius"
, "ERROR - Cannot infer instance\n\
\*** Instance : Arbitrary (Ratio Integer)\n\
\*** Expression : quickCheck prop_fahrenheitToCelsius"
)
]
p <#>No one has defined how to generate random data for testing
("arbitrary" data) for the type Ratio
Integer.#>
<#>Save ArbitraryRatio.hs in
the same directory as Temperature.hs and type
import ArbitraryRatio in the beginning of
Temperature.hs in order to have access to the
definition needed. Test the property again. If you receive
an error message, there is something wrong with your
function. The error message lists an input that the
function couldn't handle correctly. Test
fahrenheitToCelsius for this value, and you will
hopefully get an idea of what you have done wrong. With all
errors taken care of, you will get the following
result:#>
prompts "Main>"
[ ( "quickCheck prop_fahrenheitToCelsius"
, "OK, passed 100 tests."
)
]
<#>If you didn't manage to define the function correctly, you may
now check out the correct solution.#> ?> do
fixed "fahrenheitToCelsius fahrenheit = \
\(fahrenheit - 32)*5/9"
p <#>Now change Ratio Integer to the type Double,
i.e. change the type signature to#>
fixed "prop_fahrenheitToCelsius :: Double -> Bool"
p <#>Why would one want to do that? Double is a
so-called floating point type, i.e. a
fractional type with limited precision; the numbers have a maximum
number of digits of precision:#>
prompts "Main>" [ ("sqrt 2 :: Double", "1.4142135623731") ]
p <#>You may be familiar with the square root of two (sqrt
2) having an infinite series of decimals, but we were given
a finite answer.#>
prompts "Main>"
[ ( "sqrt 2 :: Ratio Integer"
, "ERROR - Cannot infer instance\n\
\*** Instance : Floating (Ratio Integer)\n\
\*** Expression : sqrt 2"
)
]
p <#>Ratio Integer is not a floating point type, hence
the function sqrt won't work for such numbers. It
isn't possible to represent the square root of two exactly
with a rational number, even though one can come
arbitrarily close by choosing the denominator and
numerator large enough. This indicates the major
disadvantage with exact fractions: they may need a large
amount of space (large denominators and numerators), and
thus become inefficient to work with.#>
<#>Double is a type more widely used than Ratio
Integer, so someone has already defined how to
generate random testing data ("arbitrary" data) for the type. For
that reason, there is no need to import any more files. Test
your program again. What happens?#> ?> do
prompts "Main>"
[ ( "quickCheck prop_fahrenheitToCelsius"
, "Falsifiable, after 4 tests:\n\
\3.5"
)
]
p <#>(It may happen that your print-out looks a little different,
since the testing temperatures are randomly generated, but it
should have roughly the same look.)#>
p <#>The test didn't succeed. This is due to
Double not giving exact results:#>
prompts "Main>"
[ ( "celsiusToFahrenheit (fahrenheitToCelsius 3.5)"
, "3.5"
)
, ( "celsiusToFahrenheit (fahrenheitToCelsius 3.5) - 3.5"
, "-3.5527136788005e-15"
)
]
p <#>(-3.5527136788005e-15 stands for
-3.5527136788005 times 10 to the power of
-15.)#>
p <#>We get rounding-off errors. However, the results are often
good enough. Suppose we are satisfied with seven digits of
precision. Just like in the lecture, we can define a new equality
operator allowing small rounding-off errors:#>
fixed "infix 4 ~==\nx ~== y = abs (x-y) < 5e-8"
p <#>(Read the material from the lecture if you don't understand why
we use infix.)#>
<#>What type signature should we give for ~==?#> ?> do
p <#>~== takes two Doubles as input
and returns a Boolean value as output, so the type signature
should be:#>
fixed "(~==) :: Double -> Double -> Bool"
p <#>We write an arrow (->) after each
argument type. This function takes two arguments, so
we write two arrows.#>
p <#>(Since ~== is an operator, we have to put it
in brackets in the type signature. Operators are
functions that have non-letter symbols as names, and can
be used infix, i.e. between their
arguments, like for instance / in 4/3.)#>
<#>Test again, and it should work smoothly.#>