Program Flow
Once you have learned how to create functions and manipulate standard IO in a programming language, you quickly begin to run-in to situations where you need your module to make decisions. Around this time you also discover the need to save large amounts of data and then cycle through that data one-at-a-time in order to pick-out items on which you must perform some sort of work. This really has nothing to do with computer programming per se, this is just how the civilized human world works. At the end of the day, a computer program is just a way to mechanically perform human activities. The need for artificial intelligence arises from the need to perform human activities. Human activities are long, complex, and usually involve large amounts of data (whether computerized or not). Just imagine the huge amount of data and diagnostic metrics which would be required to maintain a healthy large farm or a prosperous township during the bronze-age. If all we ever did was hunt and gather, there would be no need for computers. Indeed, computers (in the form of counting machines like the abacus) started popping-up around the same time as organized agriculture and thus civilization itself.
All counting machines and computers (whether primitive or advanced) utilize the concept of leverage in order to perform their feats of prodigy. The idea is that a symbol (like a bead in the tenths-position on an abacus) can represent a whole-other set of ideas and values. Then by merely manipulating the list of symbols, you can manipulate all of the other values and thus perform calculations which would be daunting or impossible to primitive man and woman-kind (more daunting for man-kind). This is called symbolic leveraging and it's the foundation of civilization as we know it.
The programming paradigm that represents this is sometimes called program-flow or workflow. The tools we use to control the flow of work being performed in Haskell are: conditional statements, lists and list operations, recursion, counters, and list comprehensions. Ultimately, the mentality behind these sorts of programs can be boiled-down to this: Test every element of a list for some arbitrary condition. If the condition is True, perform a function on the element. If not, move on to the next element.
If we begin by examining conditional-statements, we can illustrate the process with a minimum amount of prerequisite knowledge and give you real-world skills which you can use to create all sorts of useful modules. Yes, conditional statements are based on boolean operations. However, most high-school graduates already know everything they need to know about booleans in order to create expert conditional statements. There's no point beating you over the head with abstruse knowledge which you will not be using anytime soon. Most of what you need to know about bools will be instantly apparent upon viewing the code.
Conditional statements lead directly to counting. Conditional statements are the fun and easy part of programming in Haskell. However, you will eventually find yourself needing to count the amount of items in a list which meet a specific boolean condition, or to look for an item in a specific index of a list (which also requires counting and a subsequent condition-test). The act of counting is where Haskell starts to get advanced. This is because Haskell does not have a feature which other programming languages have built empires upon: mutable-state variables. This is not a shortcoming either. The designers created Haskell that way on purpose. You're probably thinking: Wait, go back a sentence. What the hell is a mutable-state variable? Here's the long and the short of mutable-state variables:
When you write-down a number or value on a notepad, you can go back and erase it or add to it or subtract from it; whatever you want. Once you lift your pencil from the page, the data is saved exactly as you left it. Haskell doesn't allow this. When you give a variable some value in Haskell, Haskell takes that value and locks it away in a safe-deposit box. If you try to modify the value in any way, Haskell will force you to save the new value to yet another deposit box or risk losing it forever. In short, the original value can never be changed or mutated. The only thing you can do to it is to delete it and start over again. This is true for every data-type in Haskell. You can only ever do three things to a variable in Haskell:
- Create a new variable (Num, Char, List, etc.).
- Read the value of a variable you already created.
- Destroy a variable you previously created.
This makes counting extremely challenging. However, once you master counting in Haskell, you will have officially become a Functional Programmer. If I could send you a badge for that accomplishment, I would because it is mind-expanding and will probably change the way you think about computer programs. But right now I can't afford it. Anyways, don't worry too much about that. Counting Happens in the next Chapter . For now we get to concentrate on something far less challenging yet still pretty rewarding. Conditional Statements.
Conditional Statements
Bools are the main unit of conditional statements. As mentioned before, they are also the main unit of formal logic; which is a system of making decisions based on truth values. Haskell has a very rich feature-set to deal with truth values; and they come in the form of conditional statements known as 'guards'. So, in this text, conditional statements will be your intro into logic. Anyways, conditional statements are where things start to get really interesting in Computer Science. They allow you to begin designing artificial intelligence modules, instead of mere calculators.
Many of the functions you write in Haskell won't ever use Bools like True and False directly. Instead, they use conditional statements. Haskell will check whether a condition is True or False for you, without you even needing to see the words 'True' and 'False'. This is immensely convenient and makes your code both easier to read and write. In the 'Bools' section of the appendix, we will show you how to manipulate Bools discretely using logic statements like not-and and not-or etc. But for now, we will focus on how to create conditional statements because they are easier to understand, more powerful to use, and more fun to write than Bools.
There are different ways to create a conditional statement in Haskell. You can use 'if-then statements' like this: 'if x is higher than 100 then putStrLn ("Too high!") else function x.' However, 'if-then' statements are rather rigid and require special indentations and other syntax. It is much easier to create conditional statements using Haskell's dedicated tool for logic: 'Guards'.
Guards look a little funny, but they are really quite easy to understand. A guard in plain-English could be written like this :
'If this condition is True (list condition here), then do this: (insert activity here).'
In Haskell's guard syntax, we would write the above line like this:
| x = y = function x
| otherwise = x
Notice that there is the traditional 2-space indent. That's because guards are only-ever used inside of function definitions. Also, you will notice that the second line says 'otherwise'. This means that, if x is NOT equal to y, return x unchanged. 'otherwise' is required to compile the function. After all, Haskell needs to know what to do when the first-line is NOT True. Haskell calls this an 'exhaustive pattern'. An 'exhaustive pattern' is one which covers or exhausts all of the possibilities in a conditional statement. As a general rule of thumb, conditional statements need at least two lines: one line to cover what to do when the condition is met, and one line to cover what should be done otherwise.
- ' | ' is the same as 'If the following condition is True...'
- 'x = y' is the same as ' the condition is 'x = y'. Of course, you can make any logically-consistent condition you want.
- '=' means 'then do the following...'
- 'function x' is the activity you would like to have done. It doesn't have to be a function. However, any activity which requires a definition or statement of equality must be defined separately. As a result, the activity in question usually ends-up being a function of some sort.
There is one final ingredient to a guard, and that's called a 'where cluase'. A 'where clause' is not strictly necessary. However, it is necessary when the activity you wish-to-be done is so complicated that it needs further definition in its own right.
Consider the following scenario:
Imagine you purchased a new HDTV which you connect to the PC in your dorm-room. The sound it produces is wonderfully lush and dynamic in an almost 3-dimensional way. However, it tends to go from a whisper to a cacophony without warning; anytime there is an action scene. Your neighbor is ready to call the cops on you over the noise it creates. The problem is that whenever you turn it down for an action scene, you can't hear the dialogue during the normal scenes! Arggg! What to do? The bass-player in your band explains that you should create a 'software limiter' for your PC which will turn the volume down automatically, whenever the levels go above -5 dbv. That way, the dialogue will be louder and the action will be quiter.
This is a perfect job for a guard. All you need to do is setup a function which tests the input level. If the input level is greater than -5, the function will return an output of -5. If the function is less than -5, the output level will remain the same. We'll call that volume-level 'x'.This is such a simple function that you shouldn't even need a where clause. Here's what it would look like in Haskell.
limiter x
| x > -5 = -5
| otherwise = x
Notice that the first line does not have an equals-sign ('='). For some reason, functions that use guards don't use equals-signs before the definition. I suppose the Haskell folks just decided that guards have enough equals-signs in them as it is. This can be a little strange at first. It's another one of those Haskell idiosyncrasies you need to get used to.
Because Haskell is designed to emulate formal mathematical notation, we must always be aware of order-of-operations. This means we need to be aware of when and when not to use parens. 'limiter x' is designed to use negative numbers. However, if we input a negative number into Haskell WITHOUT parens, Haskell thinks you want it to perform a subtraction operation on x while simultaneously inserting x into the function 'limiter'. This creates a conflict in the order-of-operations. You can resolve this the same way you did in your high-school algebra class: put the negative-number inside parens, like this:
'Prelude> limiter (-7)'
In other words, 'If in doubt, use parens!'
Now, let's say you've used your limiter module for a few weeks now and you are starting to become dissatisfied with the results. There seems to be no urgency or suspense in the audio; and this is one of the things that made you choose that model of HDTV in the first place! Once the volume hits -5, it sort of falls flat. Once again, the bass-player comes to the rescue. He immediately diagnoses the problem and suggests that the solution here is to make a compressor instead of a limiter. An audio compressor takes every output and modifies it according to some middle-value. That is, it takes the quiet parts and makes them all louder, yet simultaneously takes the loud parts and makes them all quieter. In a way, it compresses the dynamics into a smaller-range while still preserving the roller coaster-like ups and downs which make a movie soundtrack so exciting.
To do this, we use guards in conjunction with 'where-clauses' as well as the Float data-type. The guards will be setup around an adjustable middle volume which we will call 'center'. When you run the program yourself, try inputting a center value of (-5). I also hate variable-names like 'x' because they tell us nothing about what value x is supposed to be. It only takes a second to find and apply a more descriptive name than 'x'. In this case, I will call the input value 'volumeLevel' because that is what we are modifying with this module; the volume level.Then we will use the 'where-clauses' to define mini-functions which raise and lower the input volume by some percentage. Since we are using a percentage to modify the volume, we need to output 'volumeLevel' in the form of a Float; which is the only way to achieve an accurate output value. Here's what our compressor looks like:
compressor volumeLevel center
| volumeLevel > center = cut volumeLevel
| volumeLevel < center = boost volumeLevel
where
boost volumeLevel = volumeLevel - (volumeLevel * 0.25)
cut volumeLevel = volumeLevel + (volumeLevel * 0.25)
Notice that 'where' lines up exactly with the guard-symbol (' | '). This is crucial syntax if you want your module to compile properly. Also, notice that there is no equals-sign after where. However, the following lines are indented two-spaces in from 'where'. Again, the two-space indent is a common pattern you will see in Haskell syntax. Make sure you check your first several functions to see that they maintain this unique spacing. If you haven't already, take a minute to try-out both functions on your own.
The next section will consist of exercises which concentrate on using guards to modify the previous modules you created in the last section. Once you master guards, you will have all of the required skills to start writing programs which are both fun and useful. Of course, some of you may not remember much about Bools. We'll review Boolean operations and offer a complete treatment of classical logic in the appendix at a later date. Simple Boolean operations like and (&&) and or (||) will be covered whenever it applies to an exercise. They are so simple to use, that they don't warrant a dedicated section for most students. What matters is that you get a good grasp of conditional statements and functions. After that, the bulk of the hard-stuff will be behind-you!