In this course we will learn functional programming through haskell. The main motivation of this course is to prepare people for real world programming. To be a successful haskell programmer, I believe, one needs the uncluttered thought of a mathematician combined with the pragmatism of an Engineer. One can learn the basics in a matter of days. However, one requires regular practice to be an accomplished programmer.
Covering the entire language, the mathematical background and the libraries is impossible in a semester long course. So in the class we will learn the basics of haskell and some of the mathematical notions. In fact, most of the class will be devoted to understanding these mathematical concepts. Programming in haskell is a good exercise to your mathematical muscle and thus this is an important component of this course. But I hope this course does not end up being a collection of abstract nonsense. Therefore, we will have regular (i.e. weekly) ungraded assignments to pull us back to real world.
Your grade will depend on your performance in exams (which constitutes 70% of the marks) and in the semester long project (which takes up the remaining 30% of marks). There will be zero credit for the weekly assignments. However, they are what will help you build the knowledge.
Projects can be done in a group of 3 or less. Beside the projects have to be under version control and since it is expected to be in haskell, should be released as a cabal package. I recommend using darcs as the version control program. We will NOT discuss darcs or cabal in the course, you will have to learn it on your own. You may choose on what you want to work on but I will put up a list of sample projects for you to get an idea of the scope of haskell as an application development framework.
Besides, I encourage you to use haskell in your other programming needs like B.Tech project, M.Tech/Ph.D Thesis etc.
We will start with the haskell interpreter ghci
. To start the interpreter type the following on the command line. You might see something like this.
1 |
|
The Prelude>
is the prompt and the interpreter is expecting you to type in stuff. What you can type is any valid haskell expression. You can for example use ghci like a calculator.
1 |
|
The line 5 sqrt 2
is a function application, the function sqrt is applied on 2. In haskell, applying a function f
on an argument x
is done by placing f
on the left of x
. Function application associates to the left, i.e. f x y z
means ((f x) y)z
.
Although it did not appear to be the case in this limited interaction, Haskell is a strongly typed language unlike python or scheme. All values in haskell have a type. However we can almost always skip types. This is because the compiler/interpreter infers the correct type of the expression for us. You can find the type of an expression prefixing it with `:type' at the prompt. For example
1 |
|
In haskell the a type of a value is asserted by using ::
(two colons). The interpreter just told us that 'a'
is an expression of type Char
.
Some basic types in Haskell are given in the table below
Haskell type | What they are |
---|---|
Bool | Boolean type |
Int | Fixed precision integer |
Char | Character |
Integer | Multi-precision integer |
A list is one of the most important data structure in Haskell. A list is denoted by enclosing its components inside a square bracket.
1 |
|
A string in Haskell is just a list of characters. The syntax of a string is exactly as in say C with escapes etc. For example "Hello world"
is a string.
Unlike in other languages like python or scheme, a list in Haskell can have only one type of element, i.e. [1, 'a']
is not a valid expression in haskell. A list of type t
is denoted by [t]
in haskell. Example the type String
and [Char]
are same and denote strings in Haskell.
In haskell functions are first class citizens. They are like any other values. Functions can take functions as arguments and return functions as values. A function type is denoted using the arrow notation, i.e. A function that takes an integer and returns a character is denoted by Integer -> Char
.
1 |
|
Notice that the interpreter tells us that length is a function that takes the list of a
and returns an Int
(its length). The type a
in this context is a type variable, i.e. as far as length is concerned it does not care what is the type of its list, it returns the length of it. In Haskell one can write such generic functions. This feature is called polymorphism. The compiler/interpreter will appropriately infer the type depending on its arguments
1 |
|
Polymorphism is one of the greatest strengths of Haskell. We will see more of this in time to come.
Let us now define a simple haskell function.
fac 0 = 1
fac n = n * fac (n - 1)
Save this in a file, say fac.hs
and load it in the interpreter
1 |
|
A haskell function is defined by giving a set of equations. A function equation looks like
f pat_1_1 pat_1_2 ... pat_1_n = expr_1
f pat_2_1 pat_2_2 ... pat_2_n = expr_2
...
f pat_m_1 pat_2_2 ... pat_m_n = expr_m
The formal parameters can be patterns. We will define what patterns in detail latter on but a constant like 0
or a variable like n
is a pattern. When a function is evaluated, its actual arguments are matched with each of the equations in turn. We cannot explain what matching means in full detail here because we have not explained patterns yet. However, for constant patterns like 0
or a variable it is easy. An actual argument can match a constant if and only if the argument evaluates to that constant. On the other hand a variable pattern matches any argument. If a function f
is defined using equations, then while evaluating the function on a set of arguments each equation is tried out in order. The first equation of f
whose formal parameters match the actual argument is used for evaluation.
To illustrate we consider the function fac
that we defined. The expression fac 0
evaluates to 1 because the actual parameter matches with the formal parameter 0 in the first equation. On the other hand, in the expression fac 2
the actual argument 2
cannot match the formal argument 0 in the first equation of fac
. Therefore the next equation is tried namely fac n = n * fac (n-1)
. Here the formal parameter n
matches 2 (recall a variable pattern can match any argument). Therefore, fac 2
evaluates to 2 * fac (2 - 1)
which by recursion evaluates to 2.
We give two alternate defintions of the factorial function, the first using guards and the next using if then else
.
fac1 n | n == 0 = 1
| otherwise = n * fac1 (n -1)
fac2 n = if n == 0 then 1 else n * fac2 (n -1)
To summarise
Haskell functions are polymorphic
They can be recursive.
One can use pattern matching in their definition.
In the last lecture we, defined the factorial function and illustrated the use of pattern matching. In this chapter we elaborate on it a bit more.
We have already seen the list type. There is an algebraic way of defining a list. A list either an empty list or an element attached to the front of an already constructed list. An empty list in Haskell is expressed as []
where as a list whose first element is x
and the rest is xs
is denoted by x:xs
. The notation [1,2,3]
is just a syntactic sugar for 1:(2:(3:[]))
or just 1:2:3:[]
as :
is a right associative operator. We now illustrate pattern matching on list by giving the example of the map
function. The map
function takes a function and a list and applies the function on each element of the list. Here is the complete definition including the type
1 |
|
Since a list can either be empty or of the form (x:xs)
this is a complete definition. Also notice that pattern matching of variables is done here.
Before we proceed further, let us clarify the >
's at the beginning of the of the lines. This is the literate programming convention that Haskell supports. Literate programming is a style of programming championed by Knuth, where comments are given more importance than the code. It is not restricted to Haskell alone; in fact TeX and METAFONT were written first written by Knuth in a literate version Pascal language called WEB and later on ported to CWEB, a literate version of the C Programming language. The ghc
compiler and the ghci
interpreter supports both the literate and non-literate version of Haskell.
Normally any line is treated as part of the program unless the commented. In literate haskell all lines other than
\begin{code}
\end{code}
are treated as comments. We will use this literate style of programming; we will use only the first form i.e. program lines start with a >
. The advantage is that one can download the notes directly and compile and execute them.
We now illustrate the use of wild card patterns. Consider the function that tests whether a list is empty. This can be defined as follows.
1 |
|
The pattern _
(under score) matches any expression just like a variable pattern. However, unlike a variable pattern where the matched value is bound to the variable, a wild card discards the value. This can be used when we do not care about the value in the RHS of the equation.
Besides lists Haskell supports the tuple type. Tuple types corresponds to taking set theoretic products. For example the tuple (1,"Hello")
is an ordered pair consisting of the integer 1
and the string "Hello"
. Its type is (Int,String)
or equivalently (Int,[Char])
as String
is nothing but [Char]
. We illustrate the pattern matching of tuples by giving the definition of the standard functions curry
and uncurry
.
In haskell functions are univariate functions unlike other languages. Multi-parameter functions are captured using the process called currying. A function taking two arguments a
and b
and returning c
can be seen as a function taking the a
and returning a function that takes b
and returning c
. This kind of function is called a curried
function. Another way in which we can represent a function taking 2 arguments is to think of the function as taking a tuple. This is its uncurried form. We now define the higher order functions that transforms between these two forms.
1 |
|
The above code clearly illustrates the power of Haskell when it comes to manipulating functions. Use of higher order functions is one of the features that we will find quite a bit of use.
In this lecture we saw
Pattern matching for lists,
Tuples and pattern matching on them,
Literate haskell
Higher order functions.
In this lecture, we look at an algorithm to find primes which you might have learned in school. It is called the Sieve of Eratosthenes. The basic idea is the following.
Enumerate all positive integers starting from 2.
Forever do the following
Take the smallest of the remaining uncrossed integer say and circle it.
Cross out all numbers that are the factors of the circled integer .
All the circled integers are the list of primes. For an animated description see the wiki link http://en.wikipedia.org/Sieve_of_Eratosthenes
.
We want to convert this algorithm to haskell code. Notices that the Sieve seperates the primes from the composites. The primes are those that are circled and composites are those that are crossed. So let us start by defining
1 |
|
i.e. the primes are precisely the list of circled integers.
An integer gets circled if and only if it is not crossed at any stage of the sieving process. Furthermore, a integer gets crossed in the stage when its least prime factor is circled. So to check whether an integer is crossed all we need to do is to check whether there is a prime which divides it.
1 |
|
The function isCircle x
checks whether x
will eventually be circled. One need not go over all primes as the smallest non-trivial prime that divides a composite number should always be less than or equal to . This explains the first guard statement of the check function.
We now explain another feature of Haskell namely guards. Look at the definition of the function check
. Recall that a function is defined by giving a set of equations. For each such equation, we can have a set of guards. The syntax of these guarded equation looks like
f p_1 ... p_n | g_1 = e_1
| g_2 = e_2
| g_3 = e_3
| ...
| g_m = e_m
Each of the guards g_i
is a boolean expression. You should read this as "if f
's arguments match the patterns p1 ... pn
then its value is e_1
if g_1
is true, e_2
if g_2
is true, etc e_m
if g_m
is true". If multiple guards are true then the guard listed first has priority. For example consider the following function
1 |
|
Then f 0
is the string "non-negative"
. If you want to add a default guard, then use the keyword otherwise
. The keyword otherwise
is nothing but the boolean value True
. However, in guards it is a convention to write otherwise
instead of True
.
Finally, we want to define the list of circledInteger
. Clearly the first integer to be circled is 2. The rest are those integers on which the function isCircled
returns true. And here is the Haskell code.
1 |
|
Here filter is a function that does what you expect it to do. Its first argument is a predicate, i.e it take an element and returns a boolean, and its second argument is a list. It returns all those elements in the list that satisfies the predicate. The type of filter is given by filter :: (a -> Bool) -> [a] -> [a]
. This completes the program for sieving primes. Now load the program into the interperter
1 |
|
The take n xs
returns the first n
elements of the list xs
.
One thing you might have noticed is that the list primes
has a circular definition. The compiler is able to grok this circularity due to the fact that Haskell is a lazy language. No expression is evaluated unless it is required. For each integer in the list primes
to decide that it is circled, we need to consider only the primes that are less than its square root. As a result the definition of primes does not blow up into an infinitary computation.
We now explain another feature of a Haskell. Notice the use of mod
in the expression n `mod` x == 0
in the definition of the function divides
and in the guard x `divides` n
in the definition of the function check
. We have used a two argument function (i.e. its type is a -> b -> c
) like an operator. For any such function foo
we can convert it into a binary operator by back quoting it, i.e. `foo`
.
The converse is also possible, i.e. we can convert an operator into the corresponding function by enclosing it in a bracket. For example the expression (+)
(there is no space in side the bracket otherwise it is an error) is a function Int -> Int -> Int
.
1 |
|
The two function incr1
and incr2
both does the same thing; increments all the elements in a list of integers by 1.
In continuation with the theme of the last lecture we define another infinite series --- the fibonacci series. Ofcourse all of you know the Fibonacci Series. It is defined by the linear recurrence relation . We assume that and to begin with.
The defintion is straight forward; it is just a one liner, but we use this as an excuse to introduce two standard list functions
The zip of the list and is the list of tuples where is the minimum of and .
1 |
|
The function zipWith
is a general form of ziping which instead of tupling combines the values with an input functions.
1 |
|
We can now give the code for fibonacci numbers.
1 |
|
Notice that the zip of fib
and tail fib
gives a set of tuples whose caluse are consecutive fibonacci numbers. Therefore, once the intial values are set all that is required is zipping through with a (+)
operation.
Will be up soon.
We have already seen an example of a compound data type namely list. Recall that, a list is either an empty list or a list with a head element and rest of the list. We begin by defining a list data type. Haskell already provides a list data type so we do not need to define a user defined data type. However, we do this for illustration
1 |
|
One reads this as follows "List of a
is either EmptyList
or a Cons
of a
and List
of a
". Here the variable a
is a type variable. The result of this is that List
is now a polymorphic data type. We can instatiate this with any other Haskell data types. A list of integers is then List Integer
.
The identifiers EmptyList
and Cons
are the two constructors of our new data type List
. The constructors can now be used in Haskell expressions. For example EmptyList
is a valid Haskell expression. So is Cons 2 EmptyList
and Cons 1 (Cons 2 EmptyList)
. The standard list actually has two constructors, namely []
and (:)
.
We can now define functions on our new data type List
using pattern matching in the most obvious way. Here is a version of sum
that works with List Int
instead of [Int]
1 |
|
As the next example, we two functions to convert from our list type to the standard list type.
1 |
|
map
foldr
and foldl
for our new list type.We now give the general syntax for defining data types.
data Typename tv_1 tv_2 tv_n = C1 te_11 te_12 ... te_1r1
| C2 te_21 te_22 ... te_2r2
| . . .
| Cm te_m1 te_m2 ... te_mrm
Here data is a key word that tells the complier that the next equation is a data type definition. This gives a polymorphic data type with type arguments tv_1,...,tv_n
. The te_ij
's are arbitrary type expressions and the identifiers C1
to Cm
are the constructors of the type. Recall that in Haskell there is a constraint that each variable, or for that matter type variable, should be an identifer which starts with a lower case alphabet. In the case of type names and constructors, they should start with upper case alphabet.
Constructors of a data type play a dual role. In expressions they behave like functions. For example in the List
data type that we defined the EmptyList
constructor is a constant List (which is the same as 0-argument function) and Cons
has type a -> List a -> List a
. On the other hand constructors can be used in pattern matching when defining functions.
We now look at another example the binary tree. Recall that a binary tree is either an empty tree or has root and two children. In haskell this can be captured as follows
1 |
|
To illustrate function on tree let us define the depth
function
1 |
|
Our goal is to build a simple calculator. We will not worry about the parsing as that requires input output for which we are not ready yet. We will only build the expression evaluator. We start by defining a data type that captures the syntax of expressions. Our expressions are simple and do not have variables.
1 |
|
Now the evaluator is easy.
1 |
|
Either
data typeYou might have noticed that that we do not handle the division by zero errors So we want to capture functions that evaluates to a value or returns an error. The Haskell library exports a data type called Either
which is useful in such a situation. For completeness, we give the definition of Either
. You can use the standard type instead.
data Either a b = Left a
| Right b
The data type Either
is used in haskell often is situation where a computation can go wrong like for example expression evaluation. It has two constructors Left
and Right
. By convention Right
is used for the correct value and Left for the error message: A way to remeber this convention is to remember that "Right is always right".
We would want our evaluator to return either an error message or a double. For convenience, we capture this type as Result
.
1 |
|
The above expression is a type alias. This means that Result a
is the same type as Either String a
. As far as the compiler is concerned, they are both same. We have already seen a type aliasing in action namely String
and [Char]
.
We are now ready to define the new evaluator. We first give its type
1 |
|
The definition of eval'
is similar to eval for constructors Const
, Plus
, Minus
and Mul
except that it has to ensure.
Ensure each of the sub expressions have not resulted a division by zero error.
If the previous condition is met has to wrap the result into a Result
data type using the Right
constructor.
Instead of explicit pattern matching we define a helper function for this calle app
that simplify this.
1 |
|
The constructor Div
has to be handled seperately as it is the only operator that generates an error. Again we write a helper here.
1 |
|
Now we are ready to define eval'
.
1 |
|
Let us load the code in the interpreter
1 |
|
We now look at lambda calculus, the theoretical stuff that underlies functional programming. It was introduced by Alonzo Church to formalise two key concepts when dealing with functions in mathematics and logic namely: function definition and function application. In this lecture, we build enough stuff to get a lambda calculus evaluator.
1 |
|
We start with an example. Consider the squaring function, i.e. the function that maps to . In the notation of lambda calculus this is denoted as . This is called a lambda abstraction. Apply a function on an expression is written as . The key rule in expression evaluation is the -reduction: the expression reduces under -reduction to the expression with substituted in it. We now look at lambda calculus formally.
The goal of this lecture is to introduce basics of lambda calculus and on the way implement a small lambda calculus interpreter.
As with any other formal system we first define its abstract syntax. A lambda calculus expression is defined via the grammar
Here and expressions themselves. We now capture this abstract syntax as a Haskell data type
1 |
|
The notion of free and bound variables are fundamental to whole of mathematics. For example in the integral , the variable occurs free where as the variables occurs bound (to the corresponding ). Clearly the value of the expression does not depend on the bound variable; in fact we can write the same integral as .
In lambda calculus we say a variable occurs bound if it can be linked to a lambda abstraction. For example in the expression the variable is bound where as occurs free. A variable can occur free as well as bound in an expression --- consider in .
Formally we can define the free variables of a lambda expression as follows.
We turn this into haskell code
1 |
|
We often need to substitute variables with other expressions. Since it is so frequent we give a notation for this. By , we mean the expression obtained by replacing all free occurrence of in by . Let us code this up in haskell.
1 |
|
You are already familiar with this in mathematics. If we have an integral of the kind we can rewrite it as by a change of variable. The same is true for lambda calculus. We say call the ``reduction'' as the -reduction. However care should be taken when the new variable is chosen. Two pitfalls to avoid when performing -reduction of the expression to is
The variable should not be free in for otherwise by changing from to we have bound an otherwise free variable. For example if then becomes which is clearly wrong.
If has a free occurrence of in the scope of a bound occurrence of then we cannot perform change the bound variable to . For example consider . Then will become which is clearly wrong.
Clearly, one safe way to do -reduction on is to use a fresh variable , i.e. a variable that is neither free nor bound in . We write a helper function to compute all the variables of a lambda calculus expression.
1 |
|
We now give the code to perform a safe change of bound variables.
1 |
|
The way lambda calculus captures computation is through reduction. We already saw what is reduction. Under beta reduction, an expression reduces to , where denotes substitution of free occurrences of by . However, there is a chance that a free variable of could become bound suddenly. For example consider to be just and to be . Then reducing to will bind the free variable in .
We now give the code for reduction. It performs one step of beta reduction that too if and only if the expression is of the form .
1 |
|
We saw that for our evaluator we needed a source of fresh variables. Our function fresh is given a set of variables and its task is to compute a variable name that does not belong to the list. We use diagonalisation, a trick first used by Cantor to prove that Real numbers are of strictly higher cardinality than integers.
1 |
|
Read up about -normal forms. Write a function that converts a lambda calculus expression to its normal form if it exists. There are different evaluation strategies to get to -normal form. Code them all up.
The use of varOf
in -reduction is an overkill. See if it can be improved.
Read about -reduction and write code for it.
Will be up soon.
A powerful feature of Haskell is the automatic type inference of expressions. In the next few lectures, we will attempt to give an idea of how the type inference algorithm works. Ofcourse giving the type inference algorithm for the entire Haskell language is beyond the scope of this lecture so we take a toy example. Our aim is to give a complete type inference algorithm for an enriched version of lambda calculus, that has facilities to do integer arithmetic. Therefore, our lambda calulus expressions have, besides the other stuff we have seen, integer constants and the built in function '+'. We limit ourselves to only one operator because it is straight forward to extend our algorithm to work with other operation
The syntax is given below. Here and stands for arbitrary variable and , stands for arbitrary expressions.
We will write the lambda calculus expression in its infix form for ease of readability.
The haskell datatype that captures our enriched lambda calculus expression is the following
1 |
|
Clearly stuff like A (C 2) (C 3)
are invalid expressions but we ignore this for time being. One thing that the type checker can do for us is to catch such stuff.
We now want to assign types to the enriched lambda calculus that we have. As far as we are concerned the types for us are
Here is an arbitrary type variable. Again, we capture it in a Haskell datatype.
1 |
|
We will follow the following convention when dealing with type inference. The lambda calculus expressions will be denoted by Latin letters , , etc with appropriate subscripts. We will reserve the Latin letters , , and for lambda calculus variables. Types will be represented by the Greek letter and with the letters and reserved for type variables.
The notion of type specialisation is intuitivly clear. The type is a more general type than . We use to denote the fact that is specialisation of . How do we formalise this notion of specialisation ? Firstly note that any constant type like for example integer cannot be specialised further. Secondly notice that a variable can be specialised to a type as long as does not have an occurance of in it. We will denote a variable specialisation by . When we have a set of variable specialisation we have to ensure that there is no cyclicity indirectly. We doe this as follows. We say a sequence is a consistent set of specialisation if for each , does not contain any of the variables , . Now we can define what a specialisation is. Given a consistent sequence of specialisation let denote the type obtained by substituting for variables in with their specialisations in . Then we say that if there is a specialisation such that . The specialisation order gives a way to compare two types. It is not a partial order but can be converted to one by appropriate quotienting. We say two types are isomorphic, denoted by if and . It can be shown that forms an equivalence relation on types. Let denote the equivalence class associated with then, it can be show that is a partial order on .
Recall that the value of a closed lambda calculus expression, i.e. a lambda calculus expression with no free variables, is completely determined. More generally, given an expression , its value depends only on the free variables in it. Similary the type of an expression is completely specified once all its free variables are assigned types. A type environment is an assignment of types to variables. So the general task is to infer the type of a lambda calculus expression in a given type environment where all the free varaibles of have been assigned types. We will denote the type environments with with capital Greek letter with appropriate subscripts if required. Some notations that we use is the following.
We write to denote that the variable has been assigned the type .
For a variable , we use to denote the type that the type environment assigns to .
We write if assigned a type for the variable .
The type environment denotes the the type environment such that if and otherwise, i.e. the second type environment has a precedence.
As we described before, given a type environment , the types of any lambda calculus expression whose free variables are assigned types in can be infered. We use the notation to say that under the type environment one can infer the type for .
The type inference is like theorem proving: Think of infering as proving that the expression has type . Such an inference requires a set of rules which for us will be the type inference rules. We express this inference rules in the following notation
The type inference rules that we have are the following
Rule Const : where is an arbitrary integer.
Rule Plus :
Rule Var :
Rule Apply :
Rule Lambda :
Rule Specialise :
The goal of the type inference algorithm is to infer the most general type, i.e. Given an type environment and an expression find the type that satisfies the following two conditions
and,
If then .
Show that the specialisation order defined on types is a pre-order.
Given any pre-oder define the associated relation as if and . Prove that is an equivalence class. Show that can be converted into a natural partial order on the equivalence class of .
On of the key steps involved in type inference is the unification algorithm. Given two type and a unifier if it exists is a type such that is a specialisation of both and . The most general unifier of and , if it exists, is the unifier which is most general, i.e. it is the unifier of and such that all other unifiers is a specialisation of . In this lecture we develop an algorithm to compute the most general unifier of two types and .
1 |
|
Although we defined the unifier as a type, it is convenient when computing the unifier of and to compute the type specialisation that unifies them to their most general unifier. We will therefore abuse the term most general unifier to also mean this specialisation. Also for simplicity we drop the adjective ``most general'' and just use unifier to mean the most general unifier.
1 |
|
A type specialisation is captured by a Map
from type variable names to the corresponding specialisation. We given an function to compute given a specialisation .
1 |
|
We generalise the unification in two ways:
1 |
|
genUnify'
. Given, genUnify
it is easy to define genUnify'
.1 |
|
What is left is the definition of genUnify
.
1 |
|
The order of the pattern matches are important. For example notice that in line 6, both ap1
and ap2
have to be an arrow type (why?) Also in lines 3 and 4, t can either be INTEGER
or an arrow type.
So our rules of unification can are split into unifying a variable with a type , a constant types (here Integer) with a type and unifying two arrow types. We capture these rules with functions unifyV
, unifyI
and unifyA
respectively.
1 |
|
Unification of variables can be tricky since specialisations are involved. Firstly, notice that if occurs in a type , we cannot unify with unless is itself. We first give a function to check this.
1 |
|
The rules of unification of a variable and a type are thus
If has a specialisation , unify and tau,
If is or can be specialised to then we already have the specialisation.
If is unspecialised then specialise it with provided there occurs no recursion either directly or indirectly.
1 |
|
Notice here that M.lookup x sp
returns Nothing
if x
is not specialised under the specialisation sp
. The function unifies only an unspecialised variable with .
Notice here that the type is not a variable. Then it can only be Integer or an arrow type.
1 |
|
1 |
|
We now document the helper functions used in the unification algorithm. First we give an algorithm to pretty print types. This makes our error messages more readable. Notice that this version does not put unnecessary brackets.
1 |
|
Since the lecture used the module of the previous lecture you need to give the following commandline arguments.
1 |
|
Will be up some day.
Consider the following interaction with the ghci
1 |
|
Here the interpreter is able to display the value 3
but not the function (+) 1
. The error message is instructive. It says that there is no instance of Show (a0 -> a0)
for use with print
. Let us see what Show
and print
are by using the :info
command of the interpreter.
1 |
|
To understand what it means we need to know how the interpreter behaves. When even you enter an expression on the command line the interpreter evaluates and tries to print it. However not all haskell types are printable. The function print
has the type Show a => a -> IO ()
which means print
argument type a
is not a general type variable but is constranined to take types which are an instance of Show
. Here Show
is a type class.
Note that from :info Show
it is clear that the standard types like Int
, Bool
etc are all instances of the class Show
. Therefore the interpreter could print any value of those type. However, a function type is not an instance of Show
and hence could not be printed.
Will be up soon
Will be up soon
Will be up soon
Consider the printf function in C. The number of arguments it take depends on the format string that is provided to it. Can one have similar functions in Haskell ?
Let us analyse the type of printf
in various expressions. In the expression printf "Hello"
, printf
should have type String -> IO ()
where as in an expression like printf "Hello %s" "world"
, it should have the type String -> String -> IO()
. This chamelon like behaviour is what we need to define printf.
In this lecture we show how to hack Haskell's type class mechanism to simulate such variable argument functions. The stuff we will do is beyond Haskell 98 and hence we need to enable certain extensison. We do it via the following compiler pragmas. The very first set of lines in the source code if they are comments that start with {-#
and end with #-}
are treated as compiler pragmas. In this case we want to allow the extensions FlexibleInstances OverlappingInstances and InvoherentInstances. One can also give these extensions at compile time; to enable the extension Foo use the flag -XFoo at compile time.
Ofcourse it is difficult to remember all the extensions that you need for this particular code to work. The easiest way to know what extensions are required is to go ahead and use it in your file. GHC will warn you with an appropriate error message if it expects an extension.
1 |
|
Recall that an expression using printf would look something like the one below.
printf fmt e1 e2 ... em
We want our type system to infer IO ()
for this expression. The type of printf
in such a case should be String -> t1 -> ... -> tm -> IO ()
where ti
is the type of ei
. The main idea is to define a type classes say Printf
whose instances are precisely those types that are of the form t1 -> ... -> tm -> IO ()
. As a base case we will define an instance for IO ()
. We will then inductively define it for other types.
The definition of the type class Printf
and therefore printf
will be easier if we first declare a data type for formating.
1 |
|
We would rather work with the more convenient [Format]
instead of the format string. Writing a function to convert from format string to [Format]
is not too difficult.
Now for the actual definition of the Printf
class.
1 |
|
The member function printH
of the class Printf
is a helper function that will lead us to definition of printf
. Intutively it takes as argument an IO action which prints whatever arguments it has seen so far and returns the rest of the formating action required to carry out s.
We will define the Printf
instances for each of the type t1 -> ... tm -> I0 ()
inductively. The base case is when there are no arguments.
1 |
|
Now the inductive instance declaration.
1 |
|
We give a specialised instance for string which depending on whether the formating character is %s or %g uses putStr or print.
1 |
|
What is remaining is to define printLit
1 |
|
We can now define printf
1 |
|
printf
In the last lecture we saw a type class based solution to create functions that take variable number of arguments. Here we give a template haskell based solution.
Template haskell is the Haskell way of doing Meta programming. At the very least one can use it like a macro substitution but it can be used to do much more. The idea is to process the Haskell code at compile time using Haskell itself. A programmer can write Haskell functions to manipulate Haskell code and using special syntax arrange the compiler to manipulate code at compile time. In this lecture we will see how to define a version of printf using template haskell.
Template Haskell consists of two important steps.
Quoting: To allow user defined function to manipulate the Haskell code one needs to represent the program as a value in some suitable data type. The data types defined in the module Language.Haskell.TH.Syntax
is used for this purpose. For example the type Exp
defined in the above module is used to represent a valid Haskell expression. Have a look into the documentation of that module.
Splicing. Once the Haskell language fragment is processed using various function defined by the user, it needs to be compiled by the compiler. This processing is called splicing.
One point to be noted though is that template haskell does not splice the code directly but only those that are expressions that are inside the quoting monad Q. This monad is required because while generate code various side effects are created. For example a variable x
used in a fragment of the code has a different meaning if there is a local binding defined on it. Besides one would want to read in data from files (think of config files) to perform compile time operations.
There are two syantactic extensions to Haskell that makes template Haskell possible. If a haskell expression is written between [|
and |]
, the compiler will replace it with the corresponding representation in Language.Haskell.TH.Syntax
. For example, the expression [| "Hello" |]
is of type Q Exp
. The corresponding Exp
value is LitE (StringL "Hello")
.
The following are the extra syntactic conventions used.
[e| ... |]
or just [|
... '|]' for quoting expressions. This has type Q Exp
.
[d| ... |]
for quoting declarations. This has type Q [Decl]
[t| ... |]
for quoting types. This has type Q Type
.
[p| ... |]
for quoting patterns. This has type Q Pat
.
The splicing is done using the syntax $(...)
(no space between $
and (
)
The types Q Exp
and Q Pat
etc occur so often that there are aliases for them ExpQ
and PatQ
respectively. As an exercise guess the types aliases for Q Type
and Q Decl
.
Whenever possible it is better to make use of the [| ... |]
notation to build quoted expressions. However sometimes it is better to use the constructors of Exp
directly. Recall that we can splice only quoted expressions, i.e values of type Q Expr (or equivalently ExpQ). Say you have a quoted expressions qe
and qf
which correspondes to the haskell expression e
and f
respectively. If one wants to obtaine the quoted expression which correspondes to the application of the function f
on the expression e
, we would have to do something like the following
1 |
|
To make this efficient there is a combinator called appE
which does essentially what the constructor AppE
does but works on Q Exp
rather than Exp
.
appE :: ExpQ -> ExpQ -> ExpQ
The above code will then look like appE qe qf
. There are such monadic version of all the constructors of Exp
available. Make use of it.
First note that we have enabled template Haskell using the compiler pragma given above. It should be the very first line of your source code. A unrecognised pragma is ignored by the compiler but sometimes a warning is issued.
1 |
|
Few things about enabling the template haskell. Strictly speaking this module does not need TemplateHaskell, rather it can be written without using template Haskell. This is because all it does is define functions that process objects of type Expr
or ExprQ
. I have enabled it so as to write appE [|show|]
instead of the more complicated. appE (varE 'show)
First let us capture the formating via a data type
1 |
|
We need a function that will parse a string and the give the corresponding list for format. The exact details are not really of interest as far as template haskell is concerned. See the end of the file for an implementation.
1 |
|
The printf function can then be defined as.
1 |
|
We would rather implement the prinfP function. Let the list of formatting instructions be [f_1,..,f_n], then we want prinfP [f_1,...,f_n]
when spliced to return the code.
\ x0 ... xm -> concat [e1,...,e_n]
Here e_i depends on the ith format instruction f_i. If f_i is a literal then it will just be a literal string. Otherwise it would be an appropriate variable. In our case it should be xj
where j
is the number of non-literal, i.e. S
or G
, formating instructions to the left of e_i. The number is the total number of non-literal formatting instructions in the list [f_1,...,f_n] and should be less than or equal to n.
Suppose we know the number of variables to the left of a format instruction f_i, how do we generate the expression e_i ? The following function does that
1 |
|
Here we make use of the following helper Template haskell functions which we have defined subsequently.
1 |
|
The printfP function then is simple. Recall that when spliced it should generate the expression
\ x0 ... xm -> concat [e1,...,e_n]`
The complete definition is given below.
1 |
|
Here are the definition of the helper functions
1 |
|
We now come to the parsing of the format string. For simplicity of implementation if % occurs before an unknow format string it is treated as literal occurance.
1 |
|
To try it out load it with ghci using the option -XTemplateHaskell. You need this option to tell ghci that it better expect template haskell stuff like Oxford bracket [| |]
and splicing $(...)
1 |
|
Write a function that will optimise the format instructions by merging consecutive literal strings. Is there any point in optimising the format instructions?
Rewrite the printf module to not use any template haskell itself.
Haskell has a very good support for concurrent programming. In this lecture we see a very basic introduction to concurrent programming.
The Haskell runtime implements threads which are really lightweight. This means that on a decent machine you can open say 10K threads and still be have decent performance. This makes Haskell a great platform to implement high connectivity servers like http/ftp servers etc.
However you need to be carefull about FFI calls to C code. If one of your thread makes a call to a C function that blocks then all the threads will block. Sometimes the call to C functions might be indirect, you might use a library that uses Haskells FFI to talk to an already implemented C library. Then you can be in trouble if you are not careful.
One creates threads in Haskell using the forkIO :: IO () -> IO ThreadId
function. The threads created using forkIO
are really local to you ghc process and will not be visible in the rest of the OS. There is a similar forkOS
function that creates an OS level thread. As long as the code uses pure haskell functions forkIO
is all that you need.
We give here a basic function that forks a lot of worker process. The module to import is Control.Concurrent
.
1 |
|
This is the worker process. In real life program this is were stuff happen.
1 |
|
Here is where the process is created. Note that forkIO . worker
takes as input an integer and runs the worker action on it in a seperate thread
1 |
|
And finally this is the main function where all the command line arguments are parsed and things done.
1 |
|
There is a big bug in the code you just saw. All threads are terminated as soon as the main thread terminates. For real world applications one wants main thread to hang around till the others are done. We will see one way to handle this in the next lecture.
Last modified on Monday (31 October 2011 10:14:13 UTC)