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.)
Variables[edit]
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 . It is very cumbersome to type in the digits of
, 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 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 files[edit]
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 favorite text editor (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 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.
Comments[edit]
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. However, do not use them too much, like in the examples I gave, or they will serve to make a program illegible.
Variables in imperative languages[edit]
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, 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
because it means to define one thing twice, which is silly. The compiler will complain something about "multiple declarations of r
". People more familiar with imperative programming (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 that's not how Haskell works. Variables in Haskell are more like abbreviations for long expressions, much like in a calculation on paper, than locations in a changing computer memory.
Another example that doesn't work this way is
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 does something very different from what you might think).
Because you don't need to worry about changing values, the order in which variables are defined is irrelevant. 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 probably look very incredulous and wonder 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, not a bug. If you think this is a good time to stop, recall that pure functional programming takes a very different approach from imperative programming and requires a different mindset.
Functions[edit]
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
[1] 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, followed by the argument after a space (parentheses are not used). We can fit this pattern into the function definition, without any other special syntax required, just like a variable.
Now, we can plug in different values for the argument in these three calls to the function; load this definition into GHCi and try the following:
*Main> area 5 78.53981633974483 *Main> area 3 28.274333882308138 *Main> area 17 907.9202768874502
As you can see, 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
In mathematics, the parameter is enclosed between parentheses, and we would use it like or
. In Haskell, these parentheses are omitted; the function name and the argument will be separated by just a space. Since Haskell is a functional language, we will call a lot of functions, so we had better use a very brief notation.
Parentheses are still used for grouping expressions (any code that gives a value) to be evaluated together, however. 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.
Evaluation[edit]
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 |
---|
|
Multiple parameters[edit]
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 right triangle :
areaRightTriangle b h = (b * h) / 2
*Main> areaRightTriangle 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 |
---|
|
Remark on combining functions[edit]
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 |
---|
|
Local definitions[edit]
where
clauses[edit]
When defining a function, it is not uncommon to define intermediate results that are local to the function. For instance, consider Heron's formula 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 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
Scope[edit]
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), but Haskell's lexical scope is the magic that lets us define two different r
and always get the right one back depending on context.
Thanks to scope, the value of a parameter is strictly what you pass in when you call the function. Informally, we could say the r
in let r = 0
is not the same r
as the one inside our defined function area
– the r
inside area
overrides the other r
. You can think of it as Haskell picking the most specific version of r
there is. If you have many friends all named John, you go with the one which just makes more sense and is specific to the context; similarly, what value of r
we get depends on the scope.
Summary[edit]
- Variables store values. In fact, they store any arbitrary Haskell expressions.
- Variables do not change.
- Functions help you write reusable code.
- Functions can accept more than one parameter.
We also learned that comments allow non-code text to be stored in a source file.
Notes[edit]
- ↑ 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.