Haskell/Variables and functions

All the examples in this chapter can be typed into a Haskell source file and evaluated by loading that file into GHC or Hugs. Remember not to type the prompts at the beginning of input. If there are prompts at the beginning, then you can type it in an environment like GHCi. If not, then you should put it in a file and run it.

VariablesEdit

We've already seen how to use the GHCi program as a calculator. Of course, this is only practical for very short calculations. For longer calculations and for writing Haskell programs, we need to keep track of intermediate results.

Intermediate results can be stored in variables, to which we refer by their name. A variable contains a value, which is substituted for the variable name when you use a variable. For instance, consider the following calculation

ghci> 3.1416 * 5^2
78.53999999999999

This is the area of a circle with radius 5, according to the formula A = \pi r^2. It is very cumbersome to type in the digits of \pi \approx 3.1416, or even to remember them at all. In fact, an important aspect of programming, if not the whole point, is to delegate mindless repetition and rote memorization to a machine. In this case, Haskell has already defined a variable named pi that stores over a dozen digits of \pi for us.

ghci> pi
3.141592653589793
ghci> pi * 5^2
78.53981633974483

Notice that the variable pi and its value, 3.141592653589793, can be used interchangeably in calculations.

Haskell source filesEdit

Now, we want to define our own variables to help us in our calculations. This is done in a Haskell source file, which contains Haskell code.

Create a new file called Varfun.hs in your favourite text editor [1] (the file extension .hs stands for "Haskell") and type/paste in the following definition:

r = 5.0

Make sure that there are no spaces at the beginning of the line because Haskell is a whitespace sensitive language (more about indentation later).

Now, open GHCi, move to the directory (folder) where you saved your file with the :cd YourDirectoryHere command, and use the :load YourFileHere (or :l YourFileHere) command:

Prelude> :cd c:\myDirectory
Prelude> :load Varfun.hs
Compiling Main             ( Varfun.hs, interpreted )
Ok, modules loaded: Main.
*Main>

Loading a Haskell source file will make all its definitions available in the GHCi prompt. Source files generally include modules (units of storage for code) to organize them or indicate where the program should start running (the Main module) when you use many files. In this case, because you did not indicate any Main module, it created one for you.

If GHCi gives an error like Could not find module 'Varfun.hs', you probably are in the wrong directory.

Now you can use the newly defined variable r in your calculations.

*Main> r
5.0
*Main> pi * r^2
78.53981633974483

So, to calculate the area of a circle of radius 5, we simply define r = 5.0 and then type in the well-known formula \pi r^2 for the area of a circle. There is no need to write the numbers out every time; that's very convenient!

Since this was so much fun, let's add another definition: Change the contents of the source file to

r = 5.0
area = pi * r ^ 2

Save the file and type the :reload (or :r) command in GHCi to load the new contents (note that this is a continuation of the last session):

*Main> :reload
Compiling Main             ( Varfun.hs, interpreted )
Ok, modules loaded: Main.
*Main>

Now we have two variables r and area available

*Main> area
78.53981633974483
*Main> area / r
15.707963267948966

Note

It is also possible to define variables directly at the GHCi prompt, without a source file. Skipping the details, the syntax for doing so uses the let keyword (a word with a special meaning) and looks like:

Prelude> let area = pi * 5 ^ 2

Although we will occasionally do such definitions for expediency in the examples, it will quickly become obvious, as we move into slightly more complex tasks, that this practice is not really convenient. That is why we are emphasizing the use of source files from the very start.


Note

To experienced programmers: GHC can also be used as a compiler (that is, you could use GHC to convert your Haskell files into a stand-alone program that can be run without running the interpreter). How to do so will be explained much later.


CommentsEdit

Before we continue, it is good to understand that it is possible to include text in a program without having it treated as code. This is achieved by use of comments. In Haskell, a comment can be started with -- and continues until the end of the line:

x = 5     -- The variable x is 5.
y = 6     -- The variable y is 6.
-- z = 7

In this case, x and y are defined, but z is not. Comments can also go anywhere using the alternative syntax {- ... -}:

x = {- Do this just because you can. -} 5

Comments are generally used for explaining parts of a program that may be somewhat confusing to readers otherwise.

Be careful about overusing comments. Too many comments make programs harder to read, and comments must be carefully updated whenever corresponding code is changed or else the comments may become outdated and incorrect.

Variables in imperative languagesEdit

If you are already familiar with imperative programming languages like C, you will notice that variables in Haskell are quite different from variables as you know them. We now explain why and how.

If you have no programming experience, you might like to skip this section and continue reading with Functions.


Unlike in imperative languages, variables in Haskell do not vary. Once defined, their value never changes; they are immutable. For instance, the following code does not work:

r = 5
r = 2

The variables in functional programming languages are more related to variables in mathematics than changeable locations in a computer's memory. As in Haskell, you would definitely never see a variable used this way in a math classroom (at least not in the same problem). The compiler will give an error due to "multiple declarations of r". People more familiar with imperative programming, which involves explicitly telling the computer what to do, may be accustomed to read this as first setting r = 5 and then changing it to r = 2, but in functional programming languages, the program is in charge of figuring out what to do with memory.

Here's another example of a major difference from imperative languages

r = r + 1

Instead of "incrementing the variable r", this is actually a recursive definition of r in terms of itself (we will explain recursion in detail later on; just remember that this is radically different from imperative languages).

Because you don't need to worry about changing values, variables can be defined in any order. For example, the following fragments of code do exactly the same thing:

 y = x * 2
 x = 3
 x = 3
 y = x * 2

We can write things in any order that we want, there is no notion of "x being declared before y" or the other way around. This is also why you can't declare something more than once; it would be ambiguous otherwise. Of course, using y will still require a value for x, but this is unimportant until you need a specific numeric value.

By now, you might be wondering how you can actually do anything at all in Haskell where variables don't change. But trust us; as we hope to show you in the rest of this book, you can write every program under the sun without ever changing a single variable! In fact, variables that don't change make life so much easier because it makes programs much more predictable. It's a key feature of purely functional programming, which requires a very different approach from imperative programming and requires a different mindset.

FunctionsEdit

Now, suppose that we have multiple circles with different radii whose areas we want to calculate. For instance, to calculate the area of another circle with radius 3, we would have to include new variables r2 and area2[2] in our source file:

r  = 5
area  = pi*r^2
r2 = 3
area2 = pi*r2^2

Clearly, this is unsatisfactory because we are repeating the formula for the area of a circle verbatim. To eliminate this mindless repetition, we would prefer to write it down only once and then apply it to different radii. That's exactly what functions allow us to do.

A function takes an argument value (or parameter) and gives a result value, like a variable, that takes its place. (If you are already familiar with mathematical functions, they are essentially the same.) Defining functions in Haskell is simple: It is like defining a variable, except that we take note of the function argument that we put on the left hand side. For instance, the following is the definition of a function area which depends on a argument which we name r:

area r = pi * r^2

The syntax here is important to note: the function name comes first (in our example, that's "area"), followed by a space and then the argument ("r" in the example). Following the = sign, the function definition is a formula that uses the argument in context with other already defined terms.

Now, we can plug in different values for the argument in a call to the function. Load this definition into GHCi and try the following calls:

*Main> area 5
78.53981633974483
*Main> area 3
28.274333882308138
*Main> area 17
907.9202768874502

Thus, we can call this function with different radii to calculate the area of the corresponding circles.

You likely know functions from mathematics already. Our function here is defined mathematically as

A(r) = \pi \cdot r^2

In mathematics, the parameter is enclosed between parentheses, as in A(5) = 78.54 or A(3) = 28.27. While the Haskell code will still work with parentheses, they are normally omitted. As Haskell is a functional language, we will call a lot of functions, and whenever possible we want to minimize extra symbols.

Parentheses are still used for grouping expressions (any code that gives a value) to be evaluated together. Note how the following expressions are parsed differently:

area (5 + 3)    -- area (5 + 3)
area 5 + 3      -- (area 5) + 3

This shows that function calls take precedence over operators like + the same way multiplication is done before addition in mathematics.

EvaluationEdit

Let us try to understand what exactly happens when you enter an expression into GHCi. After you press the enter key, GHCi will evaluate the expression you have given. This means that it will replace each function with its definition and calculate the results until a single value is left. For example, the evaluation of area 5 proceeds as follows:

   area 5
=>    { replace the left-hand side  area r = ...  by the right-hand side  ... = pi * r^2 }
   pi * 5^2
=>    { replace  pi  by its numerical value }
   3.141592653589793 * 5^2
=>    { apply exponentiation (^) }
   3.141592653589793 * 25
=>    { apply multiplication (*) }
   78.53981633974483

As this shows, to apply or call a function means to replace the left-hand side of its definition by its right-hand side. As a last step, GHCi prints the final result on the screen.

Here are some more functions:

double x    = 2*x
quadruple x = double (double x)
square x    = x*x
half   x     = x / 2
Exercises
  • Explain how GHCi evaluates quadruple 5.
  • Define a function that subtracts 12 from half its argument.

Multiple parametersEdit

Of course, functions can also have more than one argument. For example, here is a function for calculating the area of a rectangle given its length and its width:

areaRect l w = l * w
*Main> areaRect 5 10
50

Another example that calculates the area of a triangle \left(A = \frac{bh}{2}\right):

areaTriangle b h = (b * h) / 2
*Main> areaTriangle 3 9
13.5

As you can see, multiple arguments are separated by spaces. That's also why you sometimes have to use parentheses to group expressions. For instance, to quadruple a value x, you can't write

quadruple x = double double x

because that would mean to apply a function named double to the two arguments double and x: functions can be arguments to other functions (and you will see why later). Instead, you have to put parentheses around the argument:

quadruple x = double (double x)

Arguments are always passed in the order given. For example:

subtract x y = x - y
*Main> subtract 10 5
5
*Main> subtract 5 10
-5

Here, subtract 10 5 evaluates to 10 - 5, but subtract 5 10 evaluates to 5 - 10 because the order changes.

Exercises
  • Write a function to calculate the volume of a box.
  • Approximately how many stones are the famous pyramids at Giza made up of? Use GHCi for your calculations.

Remark on combining functionsEdit

It goes without saying that you can use functions that you have already defined to define new functions, just like you can use the predefined functions like addition (+) or multiplication (*) (operators are defined as functions in Haskell). For example, to calculate the area of a square, we can reuse our function that calculates the area of a rectangle

areaRect l w = l * w
areaSquare s = areaRect s s
*Main> areaSquare 5
25

After all, a square is just a rectangle with equal sides.

This principle may seem innocent enough, but it is really powerful, in particular when we start to calculate with other objects instead of numbers.

Exercises
  • Write a function to calculate the volume of a cylinder. The volume of a cylinder is the area of the base, which is a circle (you already programmed this function in this chapter, so reuse it) multiplied by the height.

Local definitionsEdit

where clausesEdit

When defining a function, it is not uncommon to define intermediate results that are local to the function. For instance, consider Heron's formula A = \sqrt{s(s-a)(s-b)(s-c)} for calculating the area of a triangle with sides a, b, and c:

heron a b c = sqrt (s*(s-a)*(s-b)*(s-c))
    where
    s = (a+b+c) / 2

The variable s is half the perimeter of the triangle and it would be tedious to write it out four times in the argument of the square root function sqrt.

It would be wrong to just write the definitions in sequence

heron a b c = sqrt (s*(s-a)*(s-b)*(s-c))  -- s is not defined here
s = (a+b+c) / 2                           -- a, b, and c are not defined here

because the variables a, b, c are only available in the right-hand side of the function heron, but the definition of s as written here is not part of the right-hand side of heron. To make it part of the right-hand side, we have to use the where keyword.

Note that both the where and the local definitions are indented by 4 spaces, to distinguish them from subsequent definitions. Here is another example that shows a mix of local and top-level definitions:

areaTriangleTrig  a b c = c * height / 2   -- use trigonometry
    where
    cosa   = (b^2 + c^2 - a^2) / (2*b*c)
    sina   = sqrt (1 - cosa^2)
    height = b*sina
areaTriangleHeron a b c = result           -- use Heron's formula
    where
    result = sqrt (s*(s-a)*(s-b)*(s-c))
    s      = (a+b+c)/2

ScopeEdit

If you look closely at the previous example, you'll notice that we have used the variable names a, b, c twice, once for each of the area functions. How does that work?

Fortunately, the following fragment of code does not contain any unpleasant surprises:

Prelude> let r = 0
Prelude> let area r = pi * r ^ 2
Prelude> area 5
78.53981633974483

An "unpleasant surprise" here would have been getting 0 for the area because of the let r = 0 definition getting in the way. That does not happen because when you defined r the second time you are talking about a different r. This is something that happens in real life as well. How many people do you know that have the name John? What's interesting about people named John is that most of the time, you can talk about "John" to your friends, and depending on the context, your friends will know which John you are referring to. Programming has something similar to context, called scope.

We won't explain the technicalities behind scope (at least not now), just know that the value of a parameter is strictly what you pass in when you call the function, regardless of what the variable was called in the function's definition.

SummaryEdit

  1. Variables store values. In fact, they store any arbitrary Haskell expressions.
  2. Variables do not change.
  3. Functions help you write reusable code.
  4. Functions can accept more than one parameter.

We also learned that comments allow non-code text to be stored in a source file.


NotesEdit

  1. The Wikipedia article on text editors is a reasonable place to start if you need suggestions. Popular ones include Vim, Emacs and, on Windows, Notepad++ (not plain old Notepad). Proper programming text editors will provide syntax highlighting, a feature which colourises code in relevant ways so as to make it easier to read.
  2. As you can see, the names of variables may also contain numbers. Variables must begin with a lowercase letter, but for the rest, any string consisting of letter, numbers, underscore (_) or tick (') is allowed.


Last modified on 11 April 2014, at 03:53