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.)
Variables
Previously, we saw how to do simple arithmetic operations like addition and subtraction. Pop quiz: what is the area of a circle whose radius is 5 cm? No, don't worry, you haven't stumbled through the Geometry wikibook by mistake. The area of our circle is where is our radius (5cm) and , for the sake of simplicity, is . So let's try this out in GHCi:
___ ___ _ / _ \ /\ /\/ __(_) / /_\// /_/ / / | | GHC Interactive, version 6.4.1, for Haskell 98. / /_\\/ __ / /___| | http://www.haskell.org/ghc/ \____/\/ /_/\____/|_| Type :? for help. Loading package base-1.0 ... linking ... done. Prelude>
So let's see, we want to multiply pi (3.14) times our radius squared, so that would be
Prelude> 3.14 * 5^2 78.5
Great! Well, now since we have these wonderful, powerful computers to help us calculate things, there really isn't any need to round pi down to 2 decimal places. Let's do the same thing again, but with a slightly longer value for pi
Prelude> 3.14159265358979323846264338327950 * (5 ^ 2) 78.53981633974483
Much better, so now how about giving me the circumference of that circle (hint: )?
Prelude> 2 * 3.14159265358979323846264338327950 * 5 31.41592653589793
Or how about the area of a different circle with radius 25 (hint: )?
Prelude> 3.14159265358979323846264338327950 * (25 ^ 2) 1963.4954084936207
What we're hoping here is that sooner or later, you are starting to get sick of typing (or copy-and-pasting) all this text into your interpreter (some of you might even have noticed the up-arrow and Emacs-style key bindings to zip around the command line). Well, the whole point of programming, we would argue, is to avoid doing stupid, boring, repetitious work like typing the first 20 digits of pi in a million times. What we really need is a means of remembering the value of pi:
Prelude> let pi = 3.14159265358979323846264338327950
Here you are literally telling Haskell to: "let pi be equal to 3.14159...". This introduces the new variable pi, which is now defined as being the number 3.14159265358979323846264338327950. This will be very handy because it means that we can call that value back up by just typing pi again:
Prelude> pi 3.141592653589793
Don't worry about all those missing digits; they're just skipped when displaying the value. All the digits will be used in any future calculations.
Having variables takes some of the tedium out of things. What is the area of a circle having a radius of 5 cm? How about a radius of 25cm?
Prelude> pi * 5^2 78.53981633974483 Prelude> pi * 25^2 1963.4954084936207
Types
Following the previous example, you might be tempted to try storing a value for that radius. Let's see what happens:
Prelude> let r = 25
Prelude> 2 * pi * r
<interactive>:1:9:
Couldn't match `Double' against `Integer'
Expected type: Double
Inferred type: Integer
In the second argument of `(*)', namely `r'
In the definition of `it': it = (2 * pi) * r
Whoops! You've just run into a programming concept known as types. Types are a feature of many programming languages which are designed to catch some of your programming errors early on so that you find out about them before it's too late. We'll discuss types in more detail later on in the [[../Type basics|Type basics chapter]], but for now it's useful to think in terms of plugs and connectors. For example, many of the plugs on the back of your computer are designed to have different shapes and sizes for a purpose. This is partly so that you don't inadvertently plug the wrong bits of your computer together and blow something up. Types serve a similar purpose, but in this particular example, well, types aren't so helpful.
The tricky bit here is that numbers like 25 can either be interpreted as being Double or Integer (among other types)... but for lack of other information, Haskell has "guessed" that its type must be Integer (which cannot be multiplied with a Double). To work around this, we simply insist that it is to be treated as a Double
Prelude> let r = 25 :: Double Prelude> 2 * pi * r 157.07963267948966
Note that Haskell only has this "guessing" behaviour in contexts where it does not have enough information to infer the type of something. As we will see below, most of the time, the surrounding context gives us all of the information that is needed to determine, say, if a number is to be treated as an Integer or not.
Variables within variables
Variables can contain much more than just simple values such as 3.14. Indeed, they can contain any Haskell expression whatsoever. So, if we wanted to keep around, say the area of a circle with radius of 5, we could write something like this:
Prelude> let area = pi * 5^2
What's interesting about this is that we've stored a complicated chunk of Haskell (an arithmetic expression containing a variable) into yet another variable.
We can use variables to store any arbitrary Haskell code, so let's use this to get our acts together.
Prelude> let r = 25.0 Prelude> let area2 = pi * r ^ 2 Prelude> area2 1963.4954084936207
So far so good.
Prelude> let r = 2.0 Prelude> area2 1963.4954084936207
Template:Side note
Wait a second, why didn't this work? That is, why is it that we get the same value for area as we did back when r was 25? The reason this is the case is that variables in Haskell do not change[1]. What actually happens when you defined r the second time is that 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 your are refering to. Programming has something similar to context, called scope. We won't explain 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. Scope, however, does not solve the current problem. What we want to do is define a generic area function that always gives you the area of a circle. What we could do is just define it a second time:
Prelude> let area3 = pi * r ^ 2 Prelude> area3 12.566370614359172
But we are programmers, and programmers loathe repetition. Is there a better way?
Functions
What we are really trying to accomplish with our generic area is to define a function. Defining functions in Haskell is dead-simple. It is exactly like defining a variable, except with a little extra stuff on the left hand side. For instance, below is our definition of pi, followed by our definition of area:
Prelude> let pi = 3.14159265358979323846264338327950 Prelude> let area r = pi * r ^ 2
To calculate the area of our two circles, we simply pass it a different value:
Prelude> area 5 78.53981633974483 Prelude> area 25 1963.4954084936207
Functions allow us to make a great leap forward in the reusability of our code. But let's slow down for a moment, or rather, back up to dissect things. See the r in our definition area r = ...? This is what we call a parameter. A parameter is what we use to provide input to the function. It's something that the function depends on, like here the area depends on the radius. When Haskell is interpreting the function, the value of its parameter must come from the outside. In the case of area, the value of r is 5 when you say area 5, but it is 25 if you say area 25.
Scope and parameters
- Warning: this section contains spoilers to the previous exercise
We hope you have completed the very short exercise (I would say thought experiment) above. 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 the value 0. This is just a consequence of what we wrote above, namely the value of a parameter is strictly what you pass in when you call the function. And that is directly a consequence of our old friend scope. Informally, 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.
Multiple parameters
Another thing you might want to know about functions is that they can accept more than one parameter. Say for instance, you want to calculate the area of a rectangle. This is quite simple to express:
Prelude> let areaRect l w = l * w Prelude> areaRect 5 10 50
Or say you want to calculate the area of a triangle :
Prelude> let areaTriangle b h = (b * h) / 2 Prelude> areaTriangle 3 9 13.5
Passing parameters in is pretty straightforward: you just give them in the same order that they are defined. So, whereas areaTriangle 3 9 gives us the area of a triangle with base 3 and height 9, areaTriangle 9 3 gives us the area with the base 9 and height 3.
Functions within functions
To further cut down the amount of repetition it is possible to call functions from within other functions. A simple example showing how this can be used is to create a function to compute the area of a Square. We can think of a square as a special case of a rectangle (the area is still the width multiplied by the length); however, we also know that the width and length are the same, so why should we need to type it in twice?
Prelude> let areaRect l w = l * w Prelude> let areaSquare s = areaRect s s Prelude> areaSquare 5 25
Summary
- Variables store values. In fact, they store any arbitrary Haskell expression.
- Variables do not change.
- Functions help you write reusable code.
- Functions can accept more than one parameter.
Notes
- ↑ For readers with prior programming experience: Variables don't change? I only get constants? Shock! Horror! No... trust us, as we hope to show you in the rest of this book, you can go a very long way without changing a single variable! In fact, this non-changing of variables makes life easier because it makes programs so much more predictable.