Imperative vs Functional
Imperative | Functional |
---|---|
Mutation of state | Reduction of expressions |
Tell the computer how you want to do something | Tell the computer what you want to compute and let it work out how to do it |
Statements executed in order specified | Sub-expressions can often be evaluated in an arbitrary order |
Loops | Recursion |
The Compiler
Haskell is a statically typed functional programming language. This means that types are known at compile-time instead of run-time.
This allows the GHC compiler to help us write better programs. When our code is compiling
- It first goes through a parser that converts ASCII source code into data in memory.
- Then the GHC checks types and infers missing ones.
- Types are then erased after the type checking phase, and the compiler will generate binaries for the CPU to be able to run the program. (Types are not available at runtime – this is type erasure)
Expressions and definitions
Haskell evaluates programs by “reducing” expressions to a “normal” (simplest) form in a lazy manner. This is when a complicated statement has the rules defined by the language and the rest of the program applied to it to reduce its complexity, as it is a declarative language, for example 2+2
would be reduced to 4
by applying the definition of the +
function.
Definitions are when expressions are assigned to named variables, so they can be referenced elsewhere without having to be defined again inline
Anonymous functions
The \
is used to resemble the Lambda character from Lambda calculus, and it denotes a function without a name, which is just a transformation on an argument. For example, a function to multiply a parameter by two can be written as:
\x -> x * 2
All functions in Haskell are actually just anonymous functions, which are assigned to variables, as functions are treated as first class objects, but for simplicity and neatness, we needn’t define them this way in code, we can instead use syntactic sugar
Syntactic sugar for functions
Haskell syntax can be verbose if they are written using only anonymous nomenclature, so there is “syntactic sugar”, which can be used to simplify how things are written, for example, the following statements are equivalent
Function definitions can be expressed in various ways. Internally, they are allocating anonymous functions names, but syntactic sugar can be used to make this prettier. For example, the following two functions are equivalent:
x = \a -> a + 1
x a = a + 1
Currying
The process of converting a function which takes multiple arguments into a sequence of functions, each of which take one argument
In Haskell, we fundamentally only create functions which apply an operation to a single input value, and generate a single output value.
We can make functions which look like they take multiple arguments by having a function which takes a parameter, and returns another function, which is specialised based on that parameter. Then, the second (or nth) parameter can be applied to this returned function, yielding the final value.
For example, to write a function to add two numbers, we can say:
add :: Int -> Int -> Int
add = \x -> \y -> x + y
When this is evaluated, the first parameter would “specialise” the function, giving:
(add 5) :: Int -> Int
add 5 = \x -> x + 5
So we can see when the first parameter is evaluated, another function is returned, which can then be evaluated with the next parameter. Then, with the second parameter, it just resolves to a single value:
(add 5 6) :: Int
add 5 6 = 6 + 5
This process is called “partial application”, as each parameter is “partially applied” to the function
Uncurry
Contrastingly, there is a function uncurry
that converts a curried function and converts it into a function on pairs.
uncurry :: (a -> b -> c) -> (a,b) -> c
What this does it to make it more like function in mathematics, where arguments are taken together.
f (a,b) = a + b
-- vs --
f = \a -> \b -> a + b
Conditionals
In function definitions, we often want to be able to write conditional statements. For example, if we want to implement the “not” boolean operator, we might say “if the argument is True, then False, otherwise, True”. This can be written in Haskell in a number of ways:
-
If..then..else statements
not' = \x -> if True then False else True
This can then be re-written using syntactic sugar to an in-line expression
not' x = if True then False else True
It is worth noting that if an if..then..else statement returns a boolean value, we should always try to replace it with just a simple expression, for example,
if x==5 then True else False
should be written asx==5
. -
Guards, which similarly act like syntactic sugar for conditional expressions
min x y | x < y = x | otherwise = y
-
Case statements
not' = \x -> case x of True -> False False -> True
Lists
In Haskell, the list data type is implemented as a linked list. This effects how they should be used efficiently, for example, indexing inefficient, but looking at the first item is very efficient
Lists are homogenous, meaning all elements must have the same data type. As a result of this, the type of a list is the polymorphic type signature [] :: [a]
, since they can store any type, but each element must have the same type
Lists are almost always written with their syntactic sugar, but they are in fact constructed by prepending elements to an empty list
x = [1,2,3]
x = 3 : (2 : (1 : []))
The :
or “cons” is used as an infix operator to prepend elements to the list, building it up from an empty list
- This relates to how list data type is implemented as a linked list, as it encodes the fact that items are added as the head of the linked list sequentially
- The type of the “cons” operator is
(:) :: a -> [a] -> [a]
, as it takes an element to prepend, and a list containing the same type, and returns a list of that type
Ranges
Ranges are a way of generating lists, rather than having to explicitly enumerate them. Haskell can handle ranges for many data types, and will infer patterns from them. For example
> [1..4]
[1,2,3,4]
> ['a'..'c']
['a','b','c']
> [1,3..10]
[1,3,5,7,9]
The last example shows the way the compiler can infer patterns - it is effective, but can only handle fairly simple patterns.
Since Haskell is lazily evaluated, it can store ranges of infinite length, and the data will only be used as it is needed
> [0..]
[0,1,2,3,4,5,...] -- takes infinite time to print all numbers out
> take 3 [0..]
[0,1,2] -- takes finite time to pull numbers off the front of an infinite array
List comprehensions
List comprehensions are a further way to generate lists, and offer a greater ability to select and filter the data. They have the general form:
[expression | generator(s) (predicate)]
They have a large number of variations, including:
-
Expressions can be applied to the values being iterated over
[even n | n <- [0..5]] => [True,False,True,False,True,False]
-
Multiple generators can be used
[n * m | n <- [0..2], m <- [0..2]] => [0,0,0, 0,1,2, 0,2,3]
where every
m
is iterated over for eachn
-
Variables to the left are in scope of those to the right, so the following is also valid
[n * m | n <- [0..2], m <- [0..n]] => [0,0,1,0,2,4]
-
The left hand side of a generator can be pattern matched on
[x | (x:xs) <- [[1,2,3], [4,5,6,7]]] => [1,4]
-
Finally, predicates can be used along with generators to select whether a value being iterated over should be included in the list
[x | x <- [0..5], even x]=> [0,2,4]
Type Classes
Type class constraints are used to restrict type variables to only types which support the functions or operators specified by the type class.
Num
is a type class in the Standard Library
Like names of types, type class names must start with an upper-case character.
Type Class Definitions
In a type class definition, we define the method typings that an arbitrary type
a
must implement to be an instance of that type class.
Example.
class Num a where
(+) :: a -> a -> a
(-) :: a -> a -> a
abs :: a -> a
...
-
Num
is the name of the type class we are defining. a
is a type variable.- The definitions are the typing of the methods that a
Num
must adhere to.
Type Instance
When we define an instance of a type class, we have to adhere to the typing of the method(s) in the type class definition.
class Show a where
show :: a -> String
instance Show Bool where
show True = "True"
show False = "False"
In a Haskell module, there are a bunch of definitions of expressions. If we have a definition for something – we can refer to it by its name in the program. The Haskell compiler works out the typing for each definition is – when we use it it checks if the type is compatible with the expression.
Hence, we say that a type class brings function typings into scope.
(+) :: Num a => a -> a -> a
(-) :: Num a => a -> a -> a
abs :: Num a => a -> a
Num a =>
is a type class constraint.
Constraints on instances
You can also place constraints on instances.
Example. Let’s say we want to define an instance of Show
for pairs.
instance Show (a,b) where
show (x,y) = ???
Because we are using polymorphic types a
and b
, we obviously can’t pattern match on all possible values. Hence, the best way to do this is to place constraints that say both a
and b
must be instances of Show
themselves.
instance (Show a, Show b) => Show (a,b) where
show (x,y) = "(" ++ show x ++ "," ++ show y ++ ")"
Superclass Constraints
Sometimes, certain type classes have a superclass constraint stating that a type must also be an instance of a superclass to be an instance of the current class.
class Eq a => Ord a where
(<) :: a -> a -> Bool
(<=) :: a -> a -> Bool
...
- In order for
a
to be an instance ofOrd
, it must also be an instance ofEq
.
As a result, an Ord
constraint on a function implies an Eq
constraint
greaterOrEqual :: Ord a => a -> a -> Bool
greaterOrEqual x y = x > y || x == y
Polymorphism
Parametric polymorphism
Allows us to reuse the same data structure for different types of elements. (Generics in Java)
Example of Parametric Polymorphism in Haskell
-- The identity function works on elements of any type
id :: a -> a
id x = x
-- Same as head function, works on lists that contain any type
head :: [a] -> a
head (x:xs) = x
Ad-hoc polymorphism
A kind of polymorphism that is open to future extension.
In Haskell, type class constraints is called ad-hoc polymorphism as you can define a function that works with a certain type class K
, but does not necessarily work with any type just yet. In the future, as long as you define an instance of K
for an arbitrary type, this function will accept/work with this arbitrary type.
Subtype polymorphism
A synonym of this is dynamic polymorphism in Java.
Associativity
Function associativity binds the strongest.
Haskell | Maths |
---|---|
f x * g y |
\(f(x) \times g(y)\) |
Function expressions associates to the right.
xor = \a -> \b -> (a || b) && not (a && b)
-- is the same as
xor = \a -> (\b -> (a || b) && not (a && b))
Function application associates to the left.
xor True True
-- is the same as
(xor True) True
Function types associates to the right.
xor :: Bool -> Bool -> Bool
-- is the same as
xor :: Bool -> (Bool -> Bool)