Declarative Programming
Real World Haskell
Real World Haskell
Table of Contents
- Getting started
- Types and Functions
- Binary Tree
- List Comprehension
- Modules
- Lazy evaluation
- I/O and Monads
- Partial Application and Currying
- Kind
- Functors
- Applicative Functors
- Monoid Type Class
Getting started
hugs: interpreter primarily used for teachingghc: Glasgow Haskell Compiler, used for real workghci: REPL for Haskellrunghc: program for running Haskell programs as scripts without compilationPrelude: standard library of useful functions- Haskell requires type names to start with an uppercase letter, and variable names to start with a lowercase letter
Types and Functions
- Haskell types are: strong, static, and can be automatically inferred, making it safer than popular statically typed languages, and more expressive than dynamically typed languages. Much of the debugging gets moved to compile time
- strength refers to how permissive a type system is, with a stronger type system accepting fewer expressions as valid than a weaker one
- strong: type system guarantees a program cannot errors from trying to write expressions that don’t make sense
- well typed expressions obey the languages type rules
- Haskell doesn’t perform automatic coercion
- static: compiler knows the type of every value and expression at compile time before any code is executed
- compiler detects when you try to use expressions whose types don’t match
- makes type errors at runtime impossible
- type inference: compiler can automatically deduce the types of most expressions
- type signature:
:: Type - side effect: dependency between global state of the system and the behaviour of a function
- invisible inputs to/outputs from functions
- pure function: has no side effects, the default in Haskell
- impure function: has side effects
- can be identified by type signature: the result begins with
IO
- can be identified by type signature: the result begins with
- variables in Haskell allow you to bind a name to an expression. This permits substitution of the variable for the expression
- lazy evaluation: aka non-strict evaluation. Track unevaluated expressions as thunks and defer evaluation until when it is really needed
- parametric polymorphism: most visible polymorphism supported by Haskell, that has influenced the generics and templates of Java/C++. This is the ability to specify behaviour without knowing the type.
- Haskell doesn’t support subtype polymorphism as it isn’t object oriented, nor does it support coercion polymorphism as a deliberate design choice to avoid automatic coercion
- in
ghciyou can list the type of an expression using:tor:type
Comment on Purity
- makes understanding code easier: you know things the function cannot do (e.g. talk to the network), what valid behaviours could be, and it is inherently modular, because each function is self-contained with a well-defined interface
- pure code makes working with impure code simpler: code that must have side effects can be separated from code that doesn’t need side effects. Impure code is kept simple, with heavy lifting in pure code.
- minimises attack surface
Type constructors
[] and (,) are type constructors: they take types as input and build new types from them
Strings
String in Haskell is a type synonym with [Char]
Defining type synonyms
- similar to C’s
typedef
type Pair = (Int, Int)
Type Classes
- use type classes to restrict applicable types in a function with parametric polymorphism
- type classes are like interfaces in Java: if you have implementation of functions
+,-, and other numerical operations, it can be considered aNum
e.g. for sum:
Num a => [a] -> a`
Num: collection of types for which addition, multiplication, and other numerical operations make senseOrd: collection of types for which comparison operations (e.g.<,>,==) are defined
Type Definitions
Define a new type with the data keyword. Possible values are separated by |
-- e.g. our own implementation of Bool
data MyBool = MyTrue | My Falsek
-- e.g. point to store 2D Cartesian coordinates
data Point = Pt Float Float
Here we have defined a Point, which can be a Pt which also carries two Floats
Typically, you use the same name for the type and data constructor:
data Point = Point Float Float
- value constructor/data constructor: creates a new value of a specified type
newtype
- used when you want to take one type and wrap it in something to present it as another type
- faster than
data - can only use it if there is a single value constructor with a single field
Comparison of type, newtype, data
data: useful when you’re trying to make something newtype: useful when you want to make type signatures more descriptivenewtype: useful for wrapping existing types in new types to make it an instance of a typeclass
Recursive Data Types
e.g. implementation of linked list: here’s a type List, which can be a ListNode carrying with it an Int, and another List value.
Otherwise, it can be a ListEnd (just a constant)
data List = ListNode Int List | ListEnd
ListNode 20 (ListNode 10 ListEnd):Listcontaining 20 and 10
To make this polymorphic with respect to type, introduce type parameter a:
data List a = ListNode a (List a) | ListEnd
Now List is a type constructor, rather than a type. To get a type, you need to provide List with the type to use, e.g. List Int.
List Charroughly corresponds to Java’sLinkedList<Character>
Defining operations on custom types
Eq: type class for which equality makes senseshow: provides string representationShow: type class that can be converted to string representation- to automatically generate default behaviour (i.e. two values are equal when they have the same structure, and show strings that look like the code you use to write the values):
data List a = ListNode a (List a) | ListEnd
deriving (Eq, Show)
Binary Tree
data Tree a = Node a (Tree a) (Tree a) | Empty
deriving Show
tree :: Tree Int
data Tree a = Node a (Tree a) (Tree a) | Empty
deriving Show
-- returns contents of a tree
elements :: Tree a -> [a]
elements Empty = []
elements (Node x l r) = elements l ++ [x] ++ elements r
-- insert element into binary search tree
insert n Empty = (Node n Empty Empty)
insert n (Node x l r)
| n == x = (Node x l r)
| n <= x = (Node x (insert n l) r)
| n > x = (Node x l (insert n r))
-- build a binary search tree from list of values
buildtree :: Ord a => [a] -> Tree a
buildtree [] = Empty
buildtree [x] = insert x Empty
buildtree (x:xs) = insert x (buildtree xs)
-- build a BST then return sorted values
treesort :: (Ord a) => [a] -> [a]
treesort [] = []
treesort x = elements (buildtree x)
List Comprehension
[ expression | generator ]
- generator: pull items from list one at a time to operate on
- expression: what to do with each item of the generator
> [x^2 | x<- [1..6]]
[1,4,9,16,25,36]
> [(-x) | x <- [1..6]]
[-1,-2,-3,-4,-5,-6]
> [even x | x <- [1..6]]
[False,True,False,True,False,True]
- you can also add filters to restrict which input items should be processed
> [x^2 | x <- [1..12], even x]
[4, 16, 36, 64, 100, 144]
- multiple generators and filters can be combined:
- each generator introduces a variable that can be used in filters
- each filter cuts down the input items which proceed to subsequent filters/generators
- the futher down a generator is on the list, the faster it will cycle through its values
-
nested generators and filters are analogous to nested
forloops andifstatements as you would use in the procedural approach - using variables in later generators
[ x | x <- [1..4], y <- [x..5], even (x+y) ]
- Pythagorean triples
pyth :: [(Integer,Integer,Integer)]
pyth = [(a,b,c) | c <- [1..], a <- [1..c], b <- [1..c], a^2 + b^2 == c^2]
Output:
Prelude> take 5 pyth
[(3,4,5),(4,3,5),(6,8,10),(8,6,10),(5,12,13)]
zip
zip xs ys: returns a list of pairs of elements (x, y)
> zip [1,2,3,4] [5,6,7,8]
[(1,5), (2,6), (3,7), (4,8)]
- e.g. compute dot product with list comprehension
dot xs ys = sum [x*y | (x, y) <- zip xs ys]
Modules
- import using
import Module.Namekeyword - define a module using
module Module.Name
where
Lazy evaluation
You can think of execution in a pure functional language as evaluation by rewriting through substitution
e.g. with the following definitions:
f x y = x + 2*y
g x = x^2
You can evaluate the following by applicative order/call by value, rewriting the expression at the innermost level first:
g (f 1 2)
= g (1 + 2*2) -- use f's definition
= g (1 + 4)
= g 5
= 5^2 -- use g's definition
= 25
Alternatively, you can evaluate it using normal order/call by name, where you pass an un-evaluated expression, rewriting the outermoost level first:
g (f 1 2)
= (f 1 2)^2 -- use g's definition
= (1 + 2*2)^2 -- use f's definition
= (1 + 4)^2
= 5^2
= 25
Haskell uses normal order evaluation, unlike most programming languages. This will always produce the same value as applicative order evaluation, but sometimes produces a value when applicative order evaluation would fail to terminate (e.g. asking for a result on an infinite list).
Haskell uses call by need: a function’s argument is evaluated at most once if needed, otherwise never. This
evaluation isn’t all-or-nothing: Haskell is lazy in that it only evaluates on demand. This allows
Haskell to operate on infinite data structures: data constructors are simply functions that are also lazy (e.g. cons :)
e.g. take 3 [1..] evaluates to [1,2,3]
Infinite Lists
Here is an efficient Fibonacci implementation that uses a recursive definition of the infinite sequence as fibs:
fibs :: [Integer]
fibs = 1 : 1 : zipWith (+) fibs (tail fibs)
fib n = fibs !! n
Here’s another example for an infinite sequence of powers of 2:
powers :: [Integer]
powers = 1 : map (*2) powers
What drives evaluation?
-- zipWith takes a 2-argument function and 2 lists and applies the function element-wise across the lists
zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
zipWith f [] _ = []
zipWith f _ [] = []
zipWith f (x:xs) (y:ys) = f x y : zipWith f xs ys
Let’s consider how Haskell evaluates zipWidth f e1 e2:
- Haskell does a pattern match on the 3 cases defined: this requires a small amount of evaluation of
e1ande2to determine that they are non-empty lists (for the 1st and 2nd cases respectively) zipWithcauses the spines ofe1/e2to be evaluated until one of the lists is exhastedzipWithdoesn’t cause any of the list elements to be evaluated
Sieve of Eratosthenes
Algorithm for computing primes:
- write out list of integers
[2..] - put the list’s first number
paside - remove all multiples of
pfrom the list - repeat from step 2
This works with infinite lists, removing an infinite number of multiples. Haskell can evaluate using it:
primes :: [Integer]
primes = sieve [2..]
where sieve (x:xs) = x : sieve [y | y <- xs, y `mod` x > 0]
Now you can generate lists of primes rapidly:
*Main> take 100 primes
[2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101,103,107,109,113,127,131,137,139,149,151,157,163,167,173,179,181,191,193,197,199,211,223,227,229,233,239,241,251,257,263,269,271,277,281,
283,293,307,311,313,317,331,337,347,349,353,359,367,373,379,383,389,397,401,409,419,421,431,433,439,443,449,457,461,463,467,479,487,491,499,503,509,521,523,541]
I/O and Monads
- monad: representation or encapsulation of computational action
To preserve purity in computation, Haskell:
- uses typing to distinguish pure functions from effectual functions. While all functions return a result, some functions also have associated actions. The type system distinguishes between the two, helping isolate side effects and enabling the mixing of pure functions with effectful monads in a principled manner
- actions/things of monadic type should be able to works on lists, actions can be elements of lists, etc.
- Haskell describes recipes for action, which can be combined to create more complex recipes
- when its time for the actions to occur, you call
main :: IO () - monads allow sequencing, dereferencing, destructive assignment, I/O etc. to be expressed within a pure functional language. The resulting programs appear imperative but retain the properties of pure functional programs
- I/O is an example of monadic programming
Input Actions
Input returns a value. Something of type IO a is an input/output action which returns a
result of type a to the caller.
getChar :: IO Char
Output Actions
Output actions have type IO (), an instance of a monad.
putChar :: Char -> IO ()
print :: Show a => a -> IO ()
>>is used to put actions in a sequence.a >> bdenotes the combined action ofafollowed byb
(>>) :: IO () -> IO () -> IO ()
(): the unit type, with a single inhabitant(). Used for I/O actions that return nothing of interest. Used to represent no value.
File I/O
type FilePath = String
readFile :: FilePath -> IO String
writeFile :: FilePath -> String -> IO ()
appendFile :: FilePath -> String -> IO ()
Binding and Sequencing
-- bind: pass result of first action to the next
(>>=) :: IO a -> (a -> IO b) -> IO b
-- sequence: second action doesn't care about result of the first action
(>>) :: IO a -> IO b -> IO b
return :: a -> IO b
Lambda abstraction for f: \x -> f x function that takes an argument x and return f x
>> is defined in terms of >>=:
m >> k = m >>= \_ -> k
e.g. read an input file and write to an output file, excluding non-ASCII characters:
main
= readFile "inp" >>= \s ->
writeFile "outp" (filter isAscii s) >>
putStr "Filtering successful\n"
Do
Syntactic sugar time: write things under do
action1 >>= \x -> action2 using x
becomes:
x <- action1
action2 using x
action1 >> action2
becomes:
action1
action2
e.g. purely functional code that asks user for input, reads/writes a file, writes to standard out
import Data.Char(isAscii)
main
= do
putStr "Input file: "
ifile <- getLine
putStr "Output flie: "
ofile <- getLine
s <- readFile ifile
writeFile ofile (filter isAscii s)
putStrLn "All done"
- Haskell is layout sensitive: you need to indent actions the same or else they won’t be considered
part of the
doexpression
Partial Application and Currying
- all Haskell functions actually take one parameter: a function
a -> b -> ctakes one parameter of typeaand returns a functionb -> c, which takes one parameter and returnsc - this means we can call a partially apply a function by providing it with insufficient arguments, and it gives us back a function that takes the remaining number of arguments
- function application is left-associative:
a b c dis equivalent to(((a b) c) d) ->is right-associative i.e.a -> b -> cis interpreteda -> (b -> c)
Kind
- kind: type of a type
- use
:kinghcito list the kind of something *: indicates the type is concrete* -> *: indicates the type takes one concrete type as a type parameter
- use
Functors
Functoris a type class of types that can be mapped over, and requires the definition of one function,fmap
class Functor f where
fmap :: (a -> b) -> f a -> f b
- types that act like a box can be functors: a list can be thought of as a box that is empty of has something in it
fmapthen applies the function to the values inside the box, like performing keyhole surgery<$>is an infix alias forfmap-
think of functors as values within contexts
- examples of
Functors:ListMaybe(->) rIO
Functoris of kind* -> *: it takes exactly one concrete type
Mapping over an IO action:
instance Functor IO where
fmap f action = do
-- get a result from the IO action
result <- action
-- inject the value of the function applied to the result
return (f result)
Mapping over Maybe
- When you try to
mapover a list ofMaybes, you find you need to unpack each value to apply the function to the value held within theMaybe. - As
Maybeis aFunctor, you can insteadfmapover the list to get the desired result
-- lookup will return a Maybe Int
fmap sum (Map.lookup student marks)
Functions as Functors
- function type
r -> acan be written in prefix notation as(->) r a - from this perspective,
(->)is just a type constructor taking two type parameters - as
Functoraccepts exactly one concrete type,(->)needs to already be partially applied to typer
instance Functor ((->) r) where
fmap f g = (\x -> f (g x))
- to map a function over a function, you do function composition.
- An equivalent definition is:
instance Functor ((->) r) where
fmap = (.)
- functions as functors are also values in contexts
- lifting a function: revisiting the type of
fmap, adding right associative parenthesesfmap :: (a -> b) -> (f a -> f b), we see that we can think offmapas taking a function froma -> band returns a new function that takes a functor value as a parameter, and returns a functor value as the result
fmap perspectives
You can consider fmap in 2 ways:
- as a function taking a function and a functor value, then mapping that function over the functor value (keyhole surgery)
- as a function
ftaking a functiong, wherefliftsgso that it operates on functor values
Functor Laws
When you define a functor, you need to check that it conforms to the expected properties for it to be well-behaved:
- identity:
fmap id = id - composition:
fmap (f . g) = fmap f . fmap g
Applicative Functors
Functoronly works for unary functions, but not for anything of greater arity: we can’t map a function inside a functor value over another functor value usingfmapApplicativetype class: applicative functors are beefed-up functors that contain functions that can be applied to other functors
class (Functor f) => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
-- c.f. fmap :: (a -> b) -> f a -> f b
- to make a type an instance of
Applicative, you need to definepureand(<*>)functions pure:- inserts a value into the applicative functor
- takes a value of any type and returns an applicative value with that value inside it (boxing)
- alternatively you could say it takes a value and puts it in some pure/minimal context
(<*>): beefed-upfmapf (a -> b): takes a functor value with a function (call itg :: a -> b)f a: takes another functor- maps
gonto the value in the second functor
- normal functors: mapping a function over a functor doesn’t allow you to get a result out in a general way
- applicative functors give you a way to operate on several functors using a single function
- every
Applicativemust also be aFunctor, just as everyOrdmust beEq (<*>)is in some ways similar to a Cartesian product
> (++) <$> ["a", "b", "c"] <*> ["1", "2"]
["a1", "a2", "b1", "b2", "c1", "c2"]
Maybe
Maybeis an instance ofApplicative:
instance Applicative Maybe where
-- to wrap it in an applicative value, we use Just (the value constructor)
pure = Just
-- can't extract a function from Nothing
Nothing <*> _ = Nothing
-- pull out the function, and apply it to the functor something with fmap
(Just f) <*> something = fmap f something
Applicative Style
- with
Applicative, we can chain the use of<*>for operation across several applicative values
> pure (+) <*> Just 3 <*> Just 5
Just 8
- breaking this down, by left-association of
<*>:(pure (+) <*> Just 3) <*> Just 5 (+)gets put in an applicative value: aMaybecontaining(+)Just (+) <*> Just 3producesJust (3+)by partial application-
Just (3+) <*> Just 5producesJust 8 - observation: we can apply a function that doesn’t expect any applicative
values (like
(+)) and apply it to a bunch of applicative values pure f <*> xis the same asfmap f x:pure f <*> x <*> y <*> ...can be rewritten asfmap f x <*> y <*> ...- using infix notation:
f <$> x <*> y <*> ...
- to apply a function
fbetween three applicative valuesx, y, z:f <$> x <*> y <*> z, where we would normally writef x y z
Lists
- List type constructor
[]is an applicative functor
instance Applicative [] where
-- pure inserts a value into a minimal context yielding the value, i.e. a singleton
pure x = [x]
-- applies functions in the list on the lhs to all values on the rhs
fs <*> xs = [f x | f <- fs, x <-xs]
- using the applicative style on lists can be a good replacement for list comprehension
[ x*y | x <- [2,5,10], y <- [8,10,11]]
(*) <$> [2,5,10] <*> [8,10,11]
IO
- also an applicative functor
instance Applicative IO where
-- inject a value into a minimal context that still holds the value with return
pure = return
-- takes IO action a, which gives a function
a <*> b = do
-- perform the function, bind the result to f
f <- a
-- perform b and bind the result to x
x <- b
-- apply f to x and yield that as the result
return (f x)
Maybe,[]:<*>was extracting a function from LHS and applying it to RHS- with
IO, we are still extracting a function, but also sequencing actions (as to extract a result from an IO action it needs to actually be performed)
Functions (->) r
- also applicative functors
- not often used. See LYAH for details
> (+) <$> (+3) <*> (*100) $ 5
508
- this makes a function that uses
+on the results of(+3)and(*100), and applies it to5
Monoid Type Class
- monoid: made of an associative binary function and a value acting as an identity for that function
1is identity for*[]is identity for++
class Monoid m where
mempty :: m
mappend :: m -> m -> m
mconcat :: [m] -> m
mconcat = foldr mappend mempty
mempty: polymorphic constant, representing the identity value of the monoidmappend: binary function, taking 2 values of a type and returning a value of the same type- don’t read too much into the name: doesn’t necessarily have anything to do with appending
mconcat: takes a list of monoid values and reduces them to a single value usingmappend
Lists
instance Monoid [a]
mempty = []
mappend = (++)