Info Directions & Tutorials

This document contains information regarding the required software for this course, a short introduction to Haskell with code examples and requirements for the submission of worksheets.

Required Software (GHC, Shrimp, csv-Viewer)

The Glasgow Haskell Compiler (GHC) is required to execute Haskell code. The minimal installer is sufficient. If you do not want to install Haskell via a package manager, you can download the binaries directly at https://www.haskell.org/ghc (Download -> Current Stable Release). If you decide to use the binaries, don't forget to add the path to the directory containing the programs ghc and ghci to the PATH environment variable.

In addition to GHC the program Shrimp (spc) is required to solve certain tasks. Shrimp allows you to write programs as flow-charts and then translates them into Haskell. To learn how to use Shrimp please follow the tutorial in the documentation.

When a Shrimp program (also called sp-program) is executed, an execution trace can be generated and writen as csv-file. It is recommended to use a spreadsheet program such as LibreOffic Calc to view these csv-files.

Short Introduction to Haskell

The file AB/Examples.hs contains examples which you can test and modify. To do so, open the file with the interpreter ghci. You can input expressions such as 2*(3+4) or couple "abc" "123" into the interpreter and press enter to let the interpreter evaluate them. With the command :r you can reload the currently loaded hs-files. This is useful when you have modified an hs-file and don't want to restart the interpreter. With the command :q you can quit the interpreter. With the command :t you can ask for the type of a function. For example, try :t couple. Using an exclamation mark after the colon allows you to execute shell commands, e.g. :!clear (resp. :!cls for Windows) clears the terminal.

Functions and Expressions

Functions are defined using expressions. The control flow, which is described using control structures (if, for, while, etc.) in imperative languages, has to be expressed via recursion in Haskell.

Expressions are written in prefix notation. A call f(x, g(y, z), myVar) would be written as (f x (g y z) myVar) in Haskell. Binary operators are functions with two input parameters whose name does not consist of alphanumeric characters, e.g. +, -, *, &&, ||, ==. These are used in infix notation: (((3 * (2 + x)) > y) && (fun z)). The leading and trailing parenthesis can be omitted, e.g. f x (g y z) myVar would work as well.

Function and variable names have to start with a lower case letter. They may contain an apostrophe ' provided it is not the first character. The following code defines a function f2 which has three input parameters x, y, y' and returns y if x is odd and y' otherwise.

f2 x y y' = (if' ((mod x 2) == 1) y y')
The function if' has three input parameters and returns the second if the first is true, else the third.

A constant can be seen as a function without input parameters. Examples for different types of literals (a literal is a constant whose name defines its value):

pi = 3.14 
baseNo = 2 
str = "a string" -- type String
chr = 'x' -- type Char
flag_q2 = False -- type Bool 
list = [2,3,5,7] -- list of numbers
{-
 the following constant contains a 3x3 matrix
 with entries from 1 to 9
-}
matrix = 
  [
    [1,2,3],    
    [4,5,6],
    [7,8,9]
  ]
An expression can be written over multiple lines as shown in the case of matrix. Every line of the expression after the first must be indented with more whitespaces than the first character of the function name (in this case m). Tabs should not be used. The first example could be written as follows:
f2 x y y' = 
  (if' ((mod x 2) == 1) 
    y 
    y'
  )

The type of a unary function whose input parameter has the type Int and whose return value has the type Bool is Int -> Bool. Assume that in the case of f2 the variable x has type Int and the variables y, y' have type String. Then the signature of this function would look as follows:

f2 :: Int -> String -> String -> String
The types of the input parameters are separated by -> and the last type is that of the return value. The operator -> is right-associative and the above type expression is therefore interpreted as Int -> (String -> (String -> String)). This means f2 can be seen as unary function whose single input parameter has type Int and whose return value has the type (String -> (String -> String)); this means the return value is a function itself. The interpretation of a non-unary function as a sequence of unary functions is called Currying. The expression (f2 1) returns a function which is the same as the function f2 except that the first input parameter is now assumed to be 1. This is called partial application. If you do not provide a signature for a function the compiler/interpreter tries to infer the most general signature for the function.

The where-clause allows one to define subexpressions separately. This can be used to structure big expressions and write them more compactly if certain subexpressions occur multiple times.

f3 x = z
  where
    y = x + x
    z = y * y 
The identifiers y and z are only valid within the definition of f3. If y is already defined on a global level then the definition from the where-clause is preferred (shadowing). It is possible to nest where-clauses.

Data Types and Pattern Matching

The keyword type can be used to introduce type aliases, e.g. type Age = Int. An alias is treated identically to the type expression on the right side which defines it. Thus, each occurrence of Age can be replaced with Int or vice versa without affecting the semantics of the program.

A data type in Haskell consists of a name and at least one constructor. A constructor describes how a data type is represented. A constructor consists of a name and a sequence of type expressions. For example, a color can be represented using RGB or HSV. In both cases three values are required which can be expressed as Float

data Color = RGB Float Float Float | HSV Hue Saturation Value
type Hue = Float        -- [0°,359°] = [0.0,1.0]
type Saturation = Float -- [0%,100%] = [0.0,1.0]
type Value = Float      -- [0%,100%] = [0.0,1.0]

black  = RGB 0.0 0.0 0.0
black' = HSV 0.5 0.6 0.0
red    = RGB 1.0 0.0 0.0
red'   = HSV 0.0 1.0 1.0

Identifiers of data types and constructors (their names) always start with a capital letter. This makes it possible to distinguish variables and functions from data types and constructors. A constructor may have the same name as its data type (this is common if the data type only has a single constructor). Type aliases can be used to document what kind of value one expects at a specific position of a constructor, as seen in the case of HSV above. A constructor can be called like a function to define a value.

Pattern matching can be used to access the values of a constructor. A pattern is an expression on the right-hand side of = and which may only consist of constructors and variables. A pattern describes a set of values which have a certain structure (e.g. lists with at least two elements) and also binds parts of this structure to variables (e.g "let y be the second element of the list").

redVal (RGB x _ _) = x --redVal red == 1.0
swapRedBlue (RGB r g b) = (RGB b g r)
The function redVal returns the red value of a value of type Color which has been defined with the constructor RGB. The underscores mean that the values at these positions are not required. The expression redVal red' would cause an error Non-exhaustive patterns because the function is not defined for the constructorHSV. One could add another line redVal (HSV h s v) = ... which computes the red value from the HSV representation to solve this.

A constructor can also have no type expressions. For example, the data type Bool is defined as data Bool = True | False. A data type which only consists of constructors without type expressions can be seen as an enumeration. In such a case pattern matching can be used to distinguish whether a variable of type Bool is true or false.

if' True  x y = x
if' False x y = y

Lists have two special constructors [] and :. On the right-hand side of = the expression (2:[3,4,5]) means the same as prepend 2 [3,4,5], which means [2,3,4,5]. On the left-hand side they are interpreted as patterns and have the following meaning:

listTest [x,y] = "list with exactly two elements"
listTest (x:(y:z)) = "list with at least two elements"
listTest z = "all other cases"
When the function listTest is called, the patterns are checked one by one and the first definition whose pattern matches the structure of the value of the input parameters is chosen. In this example this means the following. First, it is checked whether the list has exactly two elements. If this is not the case, then it is checked whether it has at least two elements. In all other cases the third definition is chosen. If one would swap the first and third line then the function would always return "all other cases"

Type Classes

A type class describes a set of types for which particular functions are available. For example, two values of type Int oder Float can always be compared with the function <. This is possible because both types are members of the type class Ord (ordered). Members of this type class must define the functions <, <=, >, >=. Type classes are similar to what is called interfaces in other programming languages.

Members of the type class Eq (equal) have to define the functions == and /=. Except for function types (types of the form* -> *), all types in this course are members of Eq and thus can be checked for equality.

Members of the type class Show can be converted into a string with the function show. Members of the type class Read can be converted back from a string into a value of that type with the function read. The type of the return value must be declared explicitly, e.g. ( (read "[1,2,3]") :: [Int] ).

For self-defined data types it is possible to let the compiler/interpreter automatically derive the functions required for a certain type class using the keyword deriving. This turns the self-defined data type into a member of that type class.

data Color = RGB Float Float Float | HSV Hue Saturation Value
  deriving (Eq,Show,Read)
With the above definition it is possible to convert a value of type Color into a string using show and with read it can be converted back. Since Color is also a member of Eq its values can also be checked for equality. Two values are considered equal iff they were created using the same constructor and each component is equal. For example, red == red' would be False. The automatic derivation only works if all type expressions in every constructor are also members of the type class for which the functions should be derived.

Signatures of polymorphic functions can be expressed using type constraints. The signature of a function which sorts list which contains elements of arbitrary type would look as follows:

mySort :: (Ord a) => [a] -> [a]
An identifier in a type expression which starts with a small letter is a type variable. The expression [a] stands for a list with elements of type a where a can be an arbitrary type. Since a list can only be sorted if there exists an order relation on its elements, it must hold that a is a member of the type class Ord. This is expressed with the type constraint (Ord a) =>.

Further information regarding type classes can be found on Haskell/Classes and Types on Wikibooks.

Tracing / Debugging

The function trace can be used to print a string for debugging purposes. The first input parameter is a string which is printed, the second input parameter is the return value. A value of an arbitrary type can be converted into a string with the function show, provided it is a member of the type class Show.

The file Test.hs contains examples (starting after the comment --Tracing examples) on how to use this function for tracing. The order in which the trace calls are printed depends on the order of evaluation. Due to the lazy evaluation strategy of Haskell this order might not be quite obvious. For example, when evaluating reverse2 "abc" the strings starting with new res: are printed at the very end.

To test your code for the worksheets you may use the file AB/Testing.hs and add import statements to import modules as required, so you don't have to change the headers in the code templates for the solutions.

Modules

A module corresponds to an hs-file. The module name can consist of multiple parts separated by ., e.g. MyProject.ModuleSet1.ModuleName. By default GHC would assume the corresponding file to be MyProject/ModuleSet1/ModuleName.hs; this path is relative to the main hs-file which is being compiled/interpreted.

A module exports functions, types, constructors and type classes. If nothing is specified, all defined entities are exported. Alternatively, one can provide an explicit export list which states which entities should be exported. All entities not included in this list are not exported and thus cannot be accessed from outside the module (private). The module-line should always be placed at the beginning. The second line in the example below would only export the type MyType with the constructors ConsA and ConsB and the functions fun1 and fun2.

module MyModules.XyZ where
module MyModules.XyZ (MyType(ConsA,ConsB), fun1, fun2) where

To import modules the keyword import is used. If no import list is specified then all entities which are exported by the module are imported. The second line in the example below would only import the data type MyType without constructors and the function fun2.

import MyModules.XyZ
import MyModules.XyZ (MyType,fun2)
If entities with the same name are imported from different modules then they have to be referenced in a qualified manner, e.g. MyModules.XyZ.fun2. This can be abbreviated by giving an alias to a module, e.g. import MyModules.XyZ as Xy. Then the function can be referenced as Xy.fun2. Further options for importing modules are listed in the Haskell-Wiki.

Learn You a Haskell

Learn You a Haskell is a tutorial which explains the basics of Haskell in an easy to understand manner. If you feel that the above introduction is insufficient then the following parts of this tutorial can be helpful:

Directions regarding submissions

When submitting your solution for a worksheet ABi send all corresponding ABi_j.hs files and folders ABi_j as zip-archive via e-mail with the subject "Submission ABi" to sysprog@thi.uni-hannover.de. The zip-file should only contain hs- and sp-files! If there is no folder ABi_j for a file ABi_j.hs this means the use of sp-programs is prohibited.

Before the files ABi_j.hs are tested they are compiled with spc (if they don't contain sp-programs then nothing changes). Then the submission is run against a set of test cases. If the submission has passed all tests then you are notified and an oral examination with all group members via video chat follows. Each group member has to be able to answer questions regarding any part of the code.

If an error in the submission is spotted during the oral examination then the submission is counted as a failed attempt. If the execution of your submission for the test cases does not finish within a generous time limit then this is also regarded as a failed attempt.