Modules and program structure
So far in this tutorial, we have seen quite a bit of Haskell code in an informal way. In this final section, we make explicit some of what we have been doing. In fact, Haskell's syntax is extremely intuitive and straightforward. The simplest rule is usually to "write what you mean."
The examples in this tutorial have used the standard Haskell format. In the standard format, comments are indicated with a double dash to their left. All comments in the examples are end-of-line comments, which means that everything following a double dash on a line is a comment. You may also create multi-line comments by enclosing blocks in the pair "{-" and "-}". Standard Haskell files should be named with the .hs extension.
Literate scripting is an alternative format for Haskell source files. In files named with the .lhs extension, all program lines begin with the greater than character. Everything that is not a program line is a comment. This style places an emphasis on program description over program implementation. It looks something like:
Factorial by primitive recursion on decreasing num > fac1 :: Int -> Int > fac1 n = if n==1 then 1 else (n * fac1 (n-1)) Make an "adder" from an Int > mkAdder n = addN where addN m = n+m > add7 = mkAdder 7 |
Sometimes in Haskell programs, function definitions will span multiple lines and consist of multiple elements. The rule for blocks of elements at the same conceptual level is that they should be indented the same amount. Elements that belong to a higher level element should be indented more. As soon as an outdent occurs, further lines are promoted back up a conceptual level. In practice, it is obvious, and Haskell will almost always complain on errors.
-- Is a function monotonic over Ints up to n?
isMonotonic f n
= mapping == qsort mapping -- Is range list the same sorted?
where -- "where" clause is indented below "="
mapping = map f range -- "where" definition remain at least as
range = [0..n] -- indented (more would be OK)
-- Iterate a function application n times
iter n f x
| n == 0 = x -- Guards are indented below func name
| otherwise = f (iter (n-1) f x)
|
I find that two spaces is a nice looking indentation for a subelement, but you have a lot of freedom in formatting for readability (just don't outdent within the same level).
Operator and function precedence
Operators in Haskell fall into multiple levels of precedence. Most of these are the same as you would expect from other programming languages. Multiplication takes precedence over addition, and so on (so "2*3+4" is 10, not 14). Haskell's standard documentation can provide the details.
There is, however, one "gotcha" in Haskell precedence where it is easy to make a mistake. Functions take precedence over operators. The result is that the expression "f g 5" means "apply g (and 5) as arguments to f" not "apply the result of (g 5) to f." Most of the time, this sort of error will produce a compiler error message, since, for example, f will require an Int as an argument rather than another function. However, sometimes the situation can be worse than this, and you can write something valid but wrong:
double n = n*2 res1 = double 5^2 -- 'res1' is 100, i.e. (5*2)^2 res2 = double (5^2) -- 'res2' is 50, i.e. (5^2)*2 res3 = double double 5 -- Causes a compile-time error res4 = double (double 5) -- 'res4 is 20, i.e. (5*2)*2 |
As with other languages, parentheses are extremely useful in disambiguating expressions where you have some doubt about precedence (or just want to document the intention explicitly). Notice, by the way, that parentheses are not used around function arguments in Haskell; but there is no harm in pretending they are, which just creates an extra expression grouping (as in res2 above).
You might think there is a conflict between two points in this tutorial. On the one hand, we have said that names are defined as expressions only once in a program; on the other hand, many of the examples use the same variable names repeatedly. Both points are true, but need to be refined.
Every name is defined exactly once within a given scope. Every function definition defines its own scope, and some constructs within definitions define their own narrower scopes. Fortunately, the "offside rule" that defines subelements also precisely defines variable scoping. A variable (a name, really) can only occur once with a given indentation block. Let's see an example, much like previous ones:
x x y -- 'x' as arg is in different scope than func name
| y==1 = y*x*z -- 'y' from arg scope, but 'x' from 'where' scope
| otherwise = x*x -- 'x' comes from 'where' scope
where
x = 12 -- define 'x' within the guards
z = 5 -- define 'z' within the guards
n1 = x 1 2 -- 'n1' is 144 ('x' is the function name)
n2 = x 33 1 -- 'n2' is 60 ('x' is the function name)
|
Needless to say, the example is unnecessarily confusing. It is worth understanding, however, especially since arguments only have a scope within a particular function definition (and the same names can be used in other function definitions).
One thing you will have noticed is that function definitions in Haskell tend to be extremely short compared to those in other languages. This is partly due to the concise syntax of Haskell, but a greater reason is because of the emphasis in functional programming of breaking down problems into their component parts (rather than just sort of "doing what needs to be done" at each point in an imperative program). This encourages reusability of parts, and allows much better verification that each part really does what it is supposed to do.
The small parts of function definitions may be broken out in several ways. One way is to define a multitude of useful support functions within a source file, and use them as needed. The examples in this tutorial have mostly done this. However, there are also two (equivalent) ways of defining support functions within the narrow scope of a single function definition: the let clause and the where clause. A simple example follows.
f n = n+n*n
f2 n
= let sq = n*n
in sq+n
f3 n
= sq+n
where sq = n*n
|
The three definitions are equivalent, but f2 and f3 chose to define a (trivial) support function sq within the definition scope.
Haskell also supports a module system that allows for larger scale modularity of functions (and also for types, which we have not covered in this introductory tutorial). The two basic elements of module control are specification of imports and specification of exports. The former is done with the import declaration; the latter with the module declaration. Some examples include:
-- declare the current module, and export only the objects listed
module MyNumeric ( isPrime, factorial, primes, sumSquares ) where
import MyStrings -- import everything MyStrings has to offer
-- import only listed functions from MyLists
import MyLists ( quicksort, findMax, satisfice )
-- import everything in MyTrees EXCEPT normalize
import MyTrees hiding ( normalize )
-- import MyTuples as qualified names, e.g.
-- three = MyTuples.third (1,2,3,4,5,6)
import qualified MyTuples
|
You can see that Haskell provides considerable, fine-grained control of where function definitions are visible to other functions. This module system helps build large-scale componentized systems.


