470 likes | 620 Views
PROGRAMMING IN HASKELL. Modules. Based on lecture notes by Graham Hutton The book “ Learn You a Haskell for Great Good ” (and a few other sources). Type Declarations. In Haskell, a new name for an existing type can be defined using a type declaration. type String = [Char].
E N D
PROGRAMMING IN HASKELL Modules Based on lecture notes by Graham Hutton The book “Learn You a Haskell for Great Good” (and a few other sources)
Type Declarations In Haskell, a new name for an existing type can be defined using a type declaration. type String = [Char] String is a synonym for the type [Char].
Type declarations can be used to make other types easier to read. For example, given type Pos = (Int,Int) we can define: origin :: Pos origin = (0,0) left :: Pos Pos left (x,y) = (x-1,y)
Like function definitions, type declarations can also have parameters. For example, given type Pair a = (a,a) we can define: mult :: Pair Int Int mult (m,n) = m*n copy :: a Pair a copy x = (x,x)
Type declarations can be nested: type Pos = (Int,Int) type Trans = Pos Pos However, they cannot be recursive: type Tree = (Int,[Tree])
Data Declarations A completely new type can be defined by specifying its values using a data declaration. data Bool = False | True Bool is a new type, with two new values False and True.
Note: • The two values False and True are called the constructors for the type Bool. • Type and constructor names must begin with an upper-case letter. • Data declarations are similar to context free grammars. The former specifies the values of a type, the latter the sentences of a language.
Values of new types can be used in the same ways as those of built in types. For example, given data Answer = Yes | No | Unknown we can define: answers :: [Answer] answers = [Yes,No,Unknown] flip :: Answer Answer flip Yes = No flip No = Yes flip Unknown = Unknown
The constructors in a data declaration can also have parameters. For example, given data Shape = Circle Float | Rect Float Float we can define: square :: Float Shape square n = Rect n n area :: Shape Float area (Circle r) = pi * r^2 area (Rect x y) = x * y
Note: • Shape has values of the form Circle r where r is a float, and Rect x y where x and y are floats. • Circle and Rect can be viewed as functions that construct values of type Shape: Circle :: Float Shape Rect :: Float Float Shape
Recursive Types In Haskell, new types can be declared in terms of themselves. That is, types can be recursive. data Nat = Zero | Succ Nat Nat is a new type, with constructors Zero :: Nat and Succ :: Nat Nat.
Note: • A value of type Nat is either Zero, or of the form Succ n where n :: Nat. That is, Nat contains the following infinite sequence of values: Zero Succ Zero Succ (Succ Zero)
= 1 + (1 + (1 + 0)) 3 • We can think of values of type Nat as natural numbers, where Zero represents 0, and Succ represents the successor function 1+. • For example, the value Succ (Succ (Succ Zero)) represents the natural number
Using recursion, it is easy to define functions that convert between values of type Nat and Int: nat2int :: Nat Int nat2int Zero = 0 nat2int (Succ n) = 1 + nat2int n int2nat :: Int Nat int2nat 0 = Zero int2nat (n+1) = Succ (int2nat n)
Two naturals can be added by converting them to integers, adding, and then converting back: add :: Nat Nat Nat add m n = int2nat (nat2int m + nat2int n) However, using recursion the function add can be defined without the need for conversions: add Zero n = n add (Succ m) n = Succ (add m n)
= Succ (add (Succ Zero) (Succ Zero)) = Succ (Succ (add Zero (Succ Zero)) = Succ (Succ (Succ Zero)) Note: • The recursive definition for add corresponds to the laws 0+n = n and (1+m)+n = 1+(m+n). For example: add (Succ (Succ Zero)) (Succ Zero)
+ 1 2 3 Arithmetic Expressions Consider a simple form of expressions built up from integers using addition and multiplication.
Using recursion, a suitable new type to represent such expressions can be declared by: data Expr = Val Int | Add Expr Expr | Mul Expr Expr For example, the expression on the previous slide would be represented as follows: Add (Val 1) (Mul (Val 2) (Val 3))
Using recursion, it is now easy to define functions that process expressions. For example: size :: Expr Int size (Val n) = 1 size (Add x y) = size x + size y size (Mul x y) = size x + size y eval :: Expr Int eval (Val n) = n eval (Add x y) = eval x + eval y eval (Mul x y) = eval x * eval y
Note: • The three constructors have types: Val :: Int Expr Add :: Expr Expr Expr Mul :: Expr Expr Expr • Many functions on expressions can be defined by replacing the constructors by other functions using a suitable fold function. For example: eval = fold id (+) (*)
5 7 3 6 9 1 4 Binary Trees In computing, it is often useful to store data in a two-way branching structure or binary tree.
Using recursion, a suitable new type to represent such binary trees can be declared by: data Tree = Leaf Int | Node Tree Int Tree For example, the tree on the previous slide would be represented as follows: Node (Node (Leaf 1) 3 (Leaf 4)) 5 (Node (Leaf 6) 7 (Leaf 9))
We can now define a function that decides if a given integer occurs in a binary tree: occurs :: Int Tree Bool occurs m (Leaf n) = m==n occurs m (Node l n r) = m==n || occurs m l || occurs m r But… in the worst case, when the integer does not occur, this function traverses the entire tree.
Now consider the function flatten that returns the list of all the integers contained in a tree: flatten :: Tree [Int] flatten (Leaf n) = [n] flatten (Node l n r) = flatten l ++ [n] ++ flatten r A tree is a search tree if it flattens to a list that is ordered. Our example tree is a search tree, as it flattens to the ordered list [1,3,4,5,6,7,9].
Search trees have the important property that when trying to find a value in a tree we can always decide which of the two sub-trees it may occur in: occurs m (Leaf n) = m==n occurs m (Node l n r) | m==n = True | m<n = occurs m l | m>n = occurs m r This new definition is more efficient, because it only traverses one path down the tree.
Exercise Node (Node (Leaf 1) 3 (Leaf 4)) 5 (Node (Leaf 6) 7 (Leaf 9)) A binary tree is complete if the two sub-trees of every node are of equal size. Define a function that decides if a binary tree is complete. data Tree = Leaf Int | Node Tree Int Tree occurs :: Int Tree Bool occurs m (Leaf n) = m==n occurs m (Node l n r) = m==n || occurs m l || occurs m r
Modules So far, we’ve been using built-in functions provided in the Haskell prelude. This is a subset of a larger library that is provided with any installation of Haskell. (Google for Hoogle to see a handy search engine for these.) Examples of other modules: - lists - concurrent programming - complex numbers - char - sets - …
Example: Data.List To load a module, we need to import it: import Data.List All the functions in this module are immediately available: numUniques::(Eqa)=>[a]->Int numUniques=length.nub This is a function in Data.List that removes duplicates from a list. function concatenation
You can also load modules from the command prompt: ghci>:m+Data.List Or several at once: ghci>:m+Data.ListData.MapData.Set Or import only some, or all but some: importData.List(nub,sort) importData.Listhiding(nub)
If duplication of names is an issue, can extend the namespace: importqualifiedData.Map This imports the functions, but we have to use Data.Map to use them – like Data.Map.filter. When the Data.Map gets a bit long, we can provide an alias: importqualifiedData.MapasM And now we can just type M.filter, and the normal list filter will just be filter.
Data.List has a lot more functionality than we’ve seen. A few examples: ghci>intersperse'.'"MONKEY" "M.O.N.K.E.Y" ghci>intersperse0[1,2,3,4,5,6] [1,0,2,0,3,0,4,0,5,0,6] ghci>intercalate""["hey","there","guys"] "heythereguys" ghci>intercalate[0,0,0][[1,2,3],[4,5,6], [7,8,9]] [1,2,3,0,0,0,4,5,6,0,0,0,7,8,9] 30
And even more: ghci>transpose[[1,2,3],[4,5,6], [7,8,9]] [[1,4,7],[2,5,8],[3,6,9]] ghci>transpose["hey","there","guys"]["htg","ehu","yey","rs","e"] ghci>concat["foo","bar","car"] "foobarcar" ghci>concat[[3,4,5],[2,3,4],[2,1,1]] [3,4,5,2,3,4,2,1,1] 31
And even more: ghci>and$map(>4)[5,6,7,8] True ghci>and$map(==4)[4,4,4,3,4] False ghci>any(==4)[2,3,5,6,1,4] True ghci>all(>4)[6,9,10] True 32
A nice example: adding functions Functions are often represented as vectors: 8x^3 + 5x^2 + x - 1 is [8,5,1,-1]. So we can easily use List functions to add these vectors: ghci>mapsum$transpose[[0,3,5,9], [10,0,0,9],[8,5,1,-1]] [18,8,6,17] 33
There are a ton of these functions, so I could spend all semester covering just lists. More examples: group, sort, dropWhile, takeWhile, partition, isPrefixOf, find, findIndex, delete, words, insert,… Instead, I’ll make sure to post a link to a good overview of lists on the webpage, in case you need them. In essence, if it’s a useful thing to do to a list, Haskell probably supports it! 34
The Data.Char module: includes a lot of useful functions that will look similar to python, actually. Examples: isAlpha, isLower, isSpace, isDigit, isPunctuation,… ghci>allisAlphaNum"bobby283" True ghci>allisAlphaNum"eddythefish!"False ghci>groupBy((==)`on`isSpace) "heyguysitsme" ["hey","","guys","","its","","me"] 35
The Data.Char module has a datatype that is a set of comparisons on characters. There is a function called generalCategory that returns the information. (This is a bit like the Ordering type for numbers, which returns LT, EQ, or GT.) ghci>generalCategory'' Space ghci>generalCategory'A' UppercaseLetter ghci>generalCategory'a' LowercaseLetter ghci>generalCategory'.' OtherPunctuation ghci>generalCategory'9' DecimalNumber ghci>mapgeneralCategory" ¥t¥nA9?|" [Space,Control,Control,UppercaseLetter,DecimalNumber,OtherPunctuation,MathSymbol]] 36
There are also functions that can convert between Ints and Chars: ghci>mapdigitToInt"FF85AB" [15,15,8,5,10,11] ghci>intToDigit15 'f' ghci>intToDigit5 '5' ghci>chr97 'a' ghci>mapord"abcdefgh" [97,98,99,100,101,102,103,104] 37
Neat application: Ceasar ciphers A primitive encryption cipher which encodes messages by shifted them a fixed amount in the alphabet. Example: hello with shift of 3 encode::Int->String->String encodeshiftmsg= letords=mapordmsg shifted=map(+shift)ords inmapchrshifted 38
Now to use it: ghci>encode3"Heeeeey" "Khhhhh|" ghci>encode4"Heeeeey" "Liiiii}" ghci>encode1"abcd" "bcde" ghci>encode5"MarryChristmas!Hohoho!” "Rfww~%Hmwnxyrfx&%Mt%mt%mt&" 39
Decoding just reverses the encoding: decode::Int->String->String decodeshiftmsg= encode(negateshift)msg ghci>encode3"Imalittleteapot" "Lp#d#olwwoh#whdsrw" ghci>decode3"Lp#d#olwwoh#whdsrw" "Imalittleteapot" ghci>decode5.encode5$"Thisisasentence" "Thisisasentence" 40
Making our own modules We specify our own modules at the beginning of a file. For example, if we had a set of geometry functions: moduleGeometry (sphereVolume ,sphereArea ,cubeVolume ,cubeArea ,cuboidArea ,cuboidVolume )where
Then, we put the functions that the module uses: sphereVolume::Float->Float sphereVolumeradius=(4.0/3.0)*pi* (radius^3) sphereArea::Float->Float sphereArearadius=4*pi*(radius^2) cubeVolume::Float->Float cubeVolumeside=cuboidVolumesidesideside … 42
Note that we can have “private” helper functions, also: cuboidVolume::Float->Float->Float ->Float cuboidVolumeabc=rectangleAreaab*c cuboidArea::Float->Float-> Float->Float cuboidAreaabc=rectangleAreaab*2+rectangleAreaac*2+rectangleAreacb*2 rectangleArea::Float->Float->Float rectangleAreaab=a*b 43
Can also nest these. Make a folder called Geometry, with 3 files inside it: • Sphere.hs • Cubiod.hs • Cube.hs • Each will hold a separate group of functions. • To load: import Geometry.Sphere Or (if functions have same names): import qualified Geometry.Sphere as Sphere 44
The modules: moduleGeometry.Sphere (volume ,area )where volume::Float->Float volumeradius=(4.0/3.0)*pi*(radius^3) area::Float->Float arearadius=4*pi*(radius^2) 45
moduleGeometry.Cuboid (volume ,area )where volume::Float->Float->Float->Float volumeabc=rectangleAreaab*c … 46