Functional Programming
Functional Programming
Introduction to Haskell
Haskell is a purely functional programming language known for its strong type system, immutability, and
emphasis on declarative programming. It is named after the mathematician Haskell Curry and is widely used
in academia and industry for its advanced features and theoretical foundations.
1. Purely Functional: Haskell treats functions as first-class citizens and ensures that functions have no
side effects, meaning that they don't alter the state of the system or produce observable changes
beyond returning a value.
2. Immutability: Once a value is created in Haskell, it cannot be modified. This immutability
simplifies reasoning about code and enhances reliability.
3. Strong Static Typing: Haskell uses a strong and expressive type system to catch errors at compile
time. Types are inferred automatically but can be explicitly defined by the programmer.
4. Lazy Evaluation: Haskell uses lazy evaluation, meaning expressions are not evaluated until their
values are needed. This can improve performance and allows for defining infinite data structures.
5. High-Level Abstractions: Haskell supports advanced programming constructs such as higher-order
functions, monads, and type classes, enabling concise and expressive code.
Introduction to GHCi
GHCi (Glasgow Haskell Compiler Interactive) is the interactive shell for Haskell. It provides a REPL
(Read-Eval-Print Loop) environment for evaluating Haskell expressions, testing functions, and exploring
Haskell code interactively.
1. Interactive Evaluation: You can type Haskell expressions and see their results immediately, making
it easy to experiment with code snippets and debug functions.
2. Loading Modules: You can load Haskell source files or compiled modules into GHCi, allowing you
to test and interact with your code.
3. Type Inspection: GHCi provides commands to inspect the types of expressions and functions, which
can help in understanding and debugging code.
4. Execution of Haskell Scripts: You can run Haskell scripts directly from GHCi, which is useful for
testing and development.
5. Support for Debugging: GHCi includes tools for setting breakpoints, inspecting values, and
stepping through code, aiding in the debugging process.
• Starting GHCi: Run ghci in the terminal to start the interactive session.
bash
Copy code
$ ghci
• Loading Files: Use the :load or :l command to load Haskell source files.
haskell
Copy code
Prelude> :load MyFile.hs
• Evaluating Expressions: Type Haskell expressions directly and see their results.
haskell
1
Copy code
Prelude> 2 + 2
4
• Inspecting Types: Use the :type or :t command to check the type of an expression or function.
haskell
Copy code
Prelude> :type length
length :: Foldable t => t a -> Int
• Quitting GHCi: Use the :quit or :q command to exit the interactive session.
haskell
Copy code
Prelude> :quit
Example Usage
1. Define a Function:
In functional programming languages like Haskell, a function is defined with a name, a list of parameters,
and a body that describes the computation.
Higher-Order Functions
A higher-order function is a function that takes one or more functions as arguments or returns a function as a
result. This is a powerful concept in functional programming, allowing for the creation of flexible and
reusable functions.
In functional programming, you can define functions without naming them, using lambda expressions (also
known as anonymous functions). Lambda expressions are useful for short, one-off functions that are not
reused.
You can use lambda functions directly in expressions, especially with higher-order functions:
haskell
Copy code
-- Applying a lambda function in map
map (\x -> x * 2) [1, 2, 3, 4] -- Result: [2, 4, 6, 8]
Currying
Currying is a technique where a function with multiple arguments is transformed into a sequence of
functions, each taking a single argument. In functional programming, all functions are technically curried by
default.
3
Copy code
-- A function that adds three numbers
addThree :: Int -> Int -> Int -> Int
addThree x y z = x + y + z
Recursion
In functional programming, recursion is a common technique to solve problems by defining a function that
calls itself.
• factorial is defined with a base case (factorial 0 = 1) and a recursive case (factorial n = n
* factorial (n - 1)).
Pattern Matching
Pattern matching is a powerful feature that allows functions to be defined based on the structure of their
input data. It's especially useful for working with complex data types like lists, tuples, and algebraic data
types.
• length' is defined with two cases: one for an empty list ([]), and one for a non-empty list ((_:xs)).
• The (_:xs) pattern matches a list with at least one element, where _ represents the first element
(ignored) and xs is the rest of the list.
Guards
Guards provide a way to define functions based on Boolean conditions. They are useful for branching logic
within a function definition.
Example: Guards
haskell
Copy code
-- A function to categorize a number as positive, negative, or zero
signum' :: Int -> String
signum' x
| x > 0 = "Positive"
| x < 0 = "Negative"
4
| otherwise = "Zero"
• signum' uses guards to check the value of x and return a string based on the condition.
Explain guards in functional programming
Guards are a feature in functional programming that allows you to choose between different actions based
on Boolean expressions. They are particularly useful when you need to define a function with multiple
conditional cases. Guards make the code more readable and concise compared to using nested if-
else statements, especially when dealing with multiple conditions.
A guard is a Boolean expression that evaluates to either True or False. When defining a function, you can
use guards to specify different behaviors depending on the value of the input. The first guard that evaluates
to True determines the result of the function.
Syntax of Guards
The syntax for using guards in Haskell (a common functional programming language) looks like this:
haskell
Copy code
functionName args
| condition1 = result1
| condition2 = result2
| otherwise = defaultResult
• | introduces a guard.
• Each guard is followed by a condition.
• If the condition is True, the corresponding expression (result1, result2, etc.) is returned.
• otherwise is a catch-all guard that is equivalent to True and is typically used as the last guard to
handle any cases not covered by the previous guards.
Consider a function bmiCategory that categorizes a person's BMI (Body Mass Index) into different health
categories:
haskell
Copy code
bmiCategory :: Double -> String
bmiCategory bmi
| bmi < 18.5 = "Underweight"
| bmi < 25.0 = "Normal weight"
| bmi < 30.0 = "Overweight"
| otherwise = "Obesity"
Here’s a simple example where guards are used to define a function that returns the maximum of two
numbers:
5
haskell
Copy code
max' :: (Ord a) => a -> a -> a
max' x y
| x > y = x
| otherwise = y
1. Readability: Guards make the code easier to read by clearly separating different conditional
branches.
2. Conciseness: Guards reduce the need for nested if-else statements, which can clutter the code.
3. Pattern Matching: Guards can be combined with pattern matching for more expressive function
definitions.
Guards can be used alongside pattern matching to define more complex functions. For instance, consider the
following function that calculates the sign of an integer:
haskell
Copy code
signum' :: Int -> Int
signum' n
| n > 0 = 1
| n == 0 = 0
| otherwise = -1
• The function signum' uses guards to check whether n is positive, zero, or negative.
• It returns 1, 0, or -1 based on the condition.
pattern matching
Pattern matching is a powerful feature in functional programming that allows you to deconstruct data
structures and selectively execute code based on their shape or structure. It’s a way to check the structure of
a value and extract its components directly in the function definition. This technique is especially useful for
working with complex data types like lists, tuples, and custom data types.
In pattern matching, you specify patterns to which the input data is compared. If the input matches a pattern,
the corresponding code block is executed.
haskell
Copy code
functionName pattern1 = expression1
functionName pattern2 = expression2
functionName pattern3 = expression3
A common use case for pattern matching is working with lists. You can match a list against specific
patterns, like an empty list, a list with one element, or a list with multiple elements.
6
haskell
Copy code
-- A function that calculates the length of a list
listLength :: [a] -> Int
listLength [] = 0 -- Base case: the list is empty
listLength (_:xs) = 1 + listLength xs -- Recursive case: match head and tail
Pattern matching is also commonly used with tuples, where you can directly access the elements of the
tuple.
haskell
Copy code
-- A function that swaps the elements of a pair
swap :: (a, b) -> (b, a)
swap (x, y) = (y, x)
In Haskell, you can define your own data types and use pattern matching to handle them.
haskell
Copy code
-- Define a simple data type for a shape
data Shape = Circle Float | Rectangle Float Float
• Shape is a custom data type with two constructors: Circle and Rectangle.
• The area function uses pattern matching to determine the shape and calculate its area accordingly.
o If the shape is a Circle, the pattern (Circle r) matches and binds r to the radius.
o If the shape is a Rectangle, the pattern (Rectangle l w) matches and binds l to the length
and w to the width.
• Wildcard _: Sometimes, you don’t need to use all components of a pattern. The wildcard _ matches
anything but doesn't bind the matched value to a variable.
haskell
Copy code
-- A function that checks if a list has at least one element
hasElements :: [a] -> Bool
hasElements [] = False
hasElements (_:_) = True -- Matches any non-empty list
• As-Patterns (@): An as-pattern allows you to match a pattern and simultaneously bind the whole
value to a variable.
haskell
7
Copy code
-- A function that returns the first element of a list along with the entire list
firstAndRest :: [a] -> (a, [a])
firstAndRest xs@(x:_) = (x, xs)
o In the above example, xs@(x:_) matches a non-empty list and binds the entire list to xs and
the first element to x.
• Clarity and Conciseness: Pattern matching allows you to write clear and concise code by directly
specifying the structure of the data you want to process.
• Safety: Pattern matching enforces exhaustive checking, ensuring that all possible patterns are
accounted for, reducing the risk of runtime errors.
• Expressiveness: Pattern matching can make complex data manipulations more readable and
intuitive.
Pattern matching is a technique used to deconstruct data structures, such as lists, tuples, or custom data
types, and perform actions based on the shape or content of the data. It allows you to define functions that
behave differently depending on the input structure, leading to clear and concise code.
Recursion is a method of solving problems where a function calls itself to solve smaller instances of the
same problem. In functional programming, recursion is often used instead of loops to process data,
particularly in the context of lists or other recursive data structures.
When combined, pattern matching and recursion allow for elegant solutions to problems involving recursive
data structures, like lists or trees. The pattern matching handles the deconstruction of the data structure,
while recursion processes each part of the structure in turn.
Examples
Consider a function that sums the elements of a list. The function will use pattern matching to handle
different cases: an empty list and a non-empty list. Recursion is then used to sum the elements.
haskell
Copy code
-- A function that sums all the elements in a list
sumList :: [Int] -> Int
sumList [] = 0 -- Base case: the sum of an empty list is 0
sumList (x:xs) = x + sumList xs -- Recursive case: sum the head with the sum of the
tail
8
• sumList [] = 0: This is the base case. If the list is empty ([]), the sum is 0.
• sumList (x:xs) = x + sumList xs: This is the recursive case. The list is deconstructed into its
head (x) and tail (xs). The function adds x to the result of summing the rest of the list (sumList xs).
Here’s a classic example: the factorial of a number n is defined as the product of all positive integers less
than or equal to n. We can define this function recursively with pattern matching.
haskell
Copy code
-- A function that calculates the factorial of a number
factorial :: Int -> Int
factorial 0 = 1 -- Base case: 0! is 1
factorial n = n * factorial (n-1) -- Recursive case: n * (n-1)!
3. Fibonacci Sequence
The Fibonacci sequence is another example that can be defined using recursion and pattern matching. The
Fibonacci sequence is defined such that each number is the sum of the two preceding ones, usually starting
with 0 and 1.
haskell
Copy code
-- A function that calculates the nth Fibonacci number
fibonacci :: Int -> Int
fibonacci 0 = 0 -- Base case: fib(0) is 0
fibonacci 1 = 1 -- Base case: fib(1) is 1
fibonacci n = fibonacci (n-1) + fibonacci (n-2) -- Recursive case: sum of the two
preceding numbers
Consider a binary tree structure where each node can either be a leaf with a value or an internal node with a
left and right subtree. We can use pattern matching and recursion to sum all the values in the tree.
haskell
Copy code
-- Define a binary tree data type
data Tree a = Leaf a | Node (Tree a) (Tree a)
• sumTree (Leaf x) = x: If the tree is a leaf, the sum is just the value of the leaf.
• sumTree (Node left right) = sumTree left + sumTree right: If the tree is a node, the sum
is the sum of the left and right subtrees.
Key Points
9
• Pattern Matching: Allows you to destructure and inspect the input data directly within the function
definition, leading to more readable and concise code.
• Recursion: Provides a way to solve problems by breaking them down into smaller, similar problems.
Each recursive call simplifies the problem, eventually reaching a base case.
• Combining Both: When used together, pattern matching and recursion are powerful tools for
processing recursive data structures like lists, trees, and other complex types in a natural and
declarative manner.
In functional programming, lists are one of the most commonly used data structures. They are fundamental
in many functional languages, including Haskell, Lisp, and ML. Lists are ordered collections of elements of
the same type and are typically used for sequential data processing.
Properties of Lists
Here are some basic operations that can be performed on lists in functional programming:
1. List Construction
Lists can be created by specifying the elements between square brackets, separated by commas.
haskell
Copy code
-- A list of integers
numbers = [1, 2, 3, 4, 5]
2. List Access
Accessing elements in a list is often done through pattern matching or by using built-in functions.
• Tail: The tail function returns all elements of the list except the first one.
haskell
Copy code
tail [1, 2, 3, 4] -- Result: [2, 3, 4]
10
• Indexing: You can access an element at a specific index using the !! operator.
haskell
Copy code
[1, 2, 3, 4] !! 2 -- Result: 3 (indexing starts at 0)
3. List Manipulation
• Adding Elements: Use the cons operator : to add an element to the beginning of a list.
haskell
Copy code
0 : [1, 2, 3] -- Result: [0, 1, 2, 3]
• Reverse: The reverse function returns a new list with the elements in reverse order.
haskell
Copy code
reverse [1, 2, 3, 4] -- Result: [4, 3, 2, 1]
4. List Comprehensions
List comprehensions provide a concise way to create lists by specifying a set of rules or conditions.
haskell
Copy code
-- A list of squares of numbers from 1 to 5
squares = [x * x | x <- [1..5]] -- Result: [1, 4, 9, 16, 25]
• Map: The map function applies a given function to each element of a list, returning a new list.
haskell
Copy code
map (*2) [1, 2, 3] -- Result: [2, 4, 6]
• Filter: The filter function returns a new list containing only the elements that satisfy a given
predicate.
haskell
Copy code
filter odd [1, 2, 3, 4, 5] -- Result: [1, 3, 5]
6. Folding (Reduce)
Folding is a powerful technique for processing lists by iteratively applying a function to combine the
elements.
• Foldl (Left Fold): Processes the list from the left to the right.
haskell
Copy code
11
foldl (+) 0 [1, 2, 3, 4] -- Result: 10
• Foldr (Right Fold): Processes the list from the right to the left.
haskell
Copy code
foldr (+) 0 [1, 2, 3, 4] -- Result: 10
The difference between foldl and foldr becomes significant when using operations that are not associative
or when working with infinite lists.
Recursion is often used in functional programming to process lists, especially in the absence of loops. For
example, summing a list can be implemented recursively.
haskell
Copy code
-- A recursive function to sum the elements of a list
sumList :: [Int] -> Int
sumList [] = 0 -- Base case: empty list
sumList (x:xs) = x + sumList xs -- Recursive case: head + sum of the tail
Pattern matching is commonly used with lists to handle different cases, such as an empty list or a non-empty
list.
haskell
Copy code
-- A function that returns the first element of a list
firstElement :: [a] -> a
firstElement (x:_) = x -- Match the first element (head)
firstElement [] = error "Empty list!" -- Handle empty list case
• Simplicity: Lists provide a simple way to store and manipulate sequences of data.
• Flexibility: Due to their recursive nature, lists can be easily processed using recursive functions.
• Immutability: The immutability of lists in functional programming ensures that operations on lists
do not have side effects, leading to more predictable code.
In many functional programming languages, such as Haskell, a string is essentially a list of characters. For
example:
haskell
Copy code
-- A string in Haskell
greeting :: String
greeting = "Hello, world!"
-- "Hello, world!" is equivalent to ['H', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r',
'l', 'd', '!']
12
Basic String Operations
Since strings are lists of characters, most list operations can be applied to strings as well.
1. Concatenation
Strings can be concatenated using the same operator used for list concatenation, typically ++ in languages
like Haskell:
haskell
Copy code
greeting1 = "Hello, "
greeting2 = "world!"
fullGreeting = greeting1 ++ greeting2 -- Result: "Hello, world!"
2. Accessing Elements
You can access individual characters in a string using indexing (with the !! operator in Haskell) or by
pattern matching:
haskell
Copy code
firstChar = greeting !! 0 -- Access the first character, Result: 'H'
3. Length of a String
You can find the length of a string using the same length function used for lists:
haskell
Copy code
lengthOfGreeting = length greeting -- Result: 13
The map function can be used to apply a function to each character in a string:
haskell
Copy code
-- Convert a string to uppercase
toUpperCase :: String -> String
toUpperCase = map toUpper -- Result: "HELLO, WORLD!" for input "Hello, world!"
5. Filtering Strings
You can filter a string based on a condition, such as removing all spaces:
haskell
Copy code
-- Remove spaces from a string
removeSpaces :: String -> String
removeSpaces = filter (/= ' ') -- Result: "Hello,world!" for input "Hello, world!"
Pattern matching is a powerful technique in functional programming and is often used to deconstruct and
analyze strings:
haskell
13
Copy code
-- Function to check if a string starts with 'H'
startsWithH :: String -> Bool
startsWithH ('H':_) = True
startsWithH _ = False
In this example, the pattern 'H':_ matches any string that starts with 'H', where _ matches the rest of the
string.
Since strings are lists, you can use recursion to process them, just as you would with any list:
haskell
Copy code
-- A recursive function to count the number of vowels in a string
countVowels :: String -> Int
countVowels [] = 0
countVowels (x:xs)
| x `elem` "aeiouAEIOU" = 1 + countVowels xs
| otherwise = countVowels xs
In this function, we recursively check each character to see if it’s a vowel and count it if it is.
String Comprehensions
In languages like Haskell, you can use list comprehensions to generate or manipulate strings:
haskell
Copy code
-- Create a string of vowels from another string
vowelsOnly :: String -> String
vowelsOnly str = [c | c <- str, c `elem` "aeiouAEIOU"] -- Result: "eoo" for input
"Hello, world!"
• Reusability: Treating strings as lists allows you to reuse list-processing functions and techniques for
string manipulation.
• Simplicity: The list-based approach provides a uniform way to handle sequences, whether they are
strings, lists of numbers, or other types of lists.
• Flexibility: This approach allows for easy integration of pattern matching and recursion in string
processing.
Performance Considerations
While treating strings as lists is conceptually simple and powerful, it may not always be the most efficient
approach, especially for large strings. This is because operations like concatenation or indexing have a time
complexity that depends on the length of the string (since lists in languages like Haskell are linked lists, not
arrays).
Some functional programming languages or libraries offer more efficient string data types and operations
optimized for performance. For example:
• Text (in Haskell): The Text type in Haskell is optimized for handling large amounts of text more
efficiently than the basic String type, which is just a list of characters.
• ByteString (in Haskell): The ByteString type is another alternative, particularly for binary data or
text in a known encoding.
14
tuples In functional programming
In functional programming, tuples are a type of data structure that allow you to store a fixed number of
elements together as a single compound value. Unlike lists, tuples can contain elements of different types,
and their size is fixed after creation. Tuples are commonly used when you need to group together related
data of different types.
1. Fixed Size: The number of elements in a tuple is determined when the tuple is created and cannot be
changed. For example, a tuple with two elements is known as a pair, one with three elements is
a triple, and so on.
2. Heterogeneous Types: Unlike lists, which must contain elements of the same type, tuples can
contain elements of different types.
3. Immutability: In most functional programming languages, tuples are immutable. Once a tuple is
created, you cannot modify its contents.
4. Indexing: Elements in a tuple can be accessed using their position (index) in the tuple, often starting
from 1 or 0 depending on the language.
1. Creating Tuples
Tuples are typically created by grouping elements within parentheses, separated by commas.
• Haskell Example:
haskell
Copy code
-- A pair (2-tuple) containing an integer and a string
pair :: (Int, String)
pair = (42, "Answer to the Ultimate Question")
In languages like Haskell, tuples do not have direct indexing functions like lists do. Instead, you typically
use pattern matching to access elements in a tuple.
• Pattern Matching:
haskell
Copy code
-- Extracting elements from a pair using pattern matching
extractPair :: (Int, String) -> String
extractPair (x, y) = y -- This function takes a pair and returns the second
element
Haskell also provides functions like fst and snd to access the first and second elements of a pair:
haskell
Copy code
fst (42, "Answer") -- Result: 42
snd (42, "Answer") -- Result: "Answer"
15
3. Tuples in Functions
-- Usage
result = divideAndRemainder 10 3 -- Result: (3, 1)
In this example, the function divideAndRemainder returns both the quotient and the remainder of a division
operation as a tuple.
4. Nested Tuples
Tuples can also be nested, meaning a tuple can contain other tuples as elements:
haskell
Copy code
nestedTuple :: ((Int, String), Bool)
nestedTuple = ((42, "Answer"), True)
• Multiple Return Values: Functions that need to return more than one value often use tuples.
• Grouping Data: Tuples are useful for grouping related but heterogeneous data together without
creating a new data structure.
• Pattern Matching: Tuples allow for concise and readable pattern matching, making it easy to
destructure and work with compound data.
• Fixed Size vs. Variable Size: Tuples have a fixed size, while lists can grow or shrink.
• Heterogeneous vs. Homogeneous: Tuples can hold elements of different types, whereas lists
typically hold elements of the same type.
• Use Cases: Tuples are generally used for grouping a fixed number of related but potentially different
types of data, while lists are used for collections of similar elements where the number of elements
may vary.
Examples in Haskell
-- Usage
dimensions = (10, 5)
result = areaAndPerimeter dimensions -- Result: (50, 30)
16
2. Working with Nested Tuples:
haskell
Copy code
-- A nested tuple example
nestedExample :: ((Int, Int), (String, Bool))
nestedExample = ((1, 2), ("Nested", True))
-- Usage
result = extractValues nestedExample -- Result: (3, "Nested")
-- Usage
swapped = swap (1, "one") -- Result: ("one", 1)
1. Pure Functions
• Definition: A pure function is a function that always produces the same output given the same input
and has no side effects (it doesn’t alter any state or perform actions like modifying a global variable,
writing to a file, etc.).
• Characteristics:
o Deterministic: The function's output depends solely on its input parameters.
o No Side Effects: It doesn’t modify external variables or states.
• Examples:
haskell
Copy code
add :: Int -> Int -> Int
add x y = x + y
In this example, add is a pure function because it only computes the sum of x and y without any side
effects.
2. Higher-Order Functions
• Definition: A higher-order function is a function that takes one or more functions as arguments,
returns a function as its result, or both.
• Usage: They are used for operations like mapping, filtering, reducing, and function composition.
• Examples:
haskell
Copy code
map :: (a -> b) -> [a] -> [b]
map f [] = []
map f (x:xs) = f x : map f xs
17
map is a higher-order function that applies a function f to each element of a list.
3. First-Class Functions
• Definition: In functional programming, functions are first-class citizens, meaning they can be passed
as arguments to other functions, returned as values from other functions, and assigned to variables.
• Characteristics: Functions can be used as any other value or data type.
• Example:
haskell
Copy code
applyTwice :: (a -> a) -> a -> a
applyTwice f x = f (f x)
Here, applyTwice takes a function f and an argument x, and applies the function f to x twice.
• Definition: An anonymous function, also known as a lambda function, is a function defined without
a name. It’s often used for short-lived operations.
• Syntax: They are defined using the \ symbol in Haskell.
• Examples:
haskell
Copy code
-- Anonymous function to square a number
square = \x -> x * x
5. Recursive Functions
• Definition: A recursive function is a function that calls itself in its definition. Recursion is a
fundamental concept in functional programming, used to iterate over data structures like lists.
• Characteristics:
o Base Case: A condition where the recursion ends.
o Recursive Case: The case where the function calls itself with modified parameters.
• Examples:
haskell
Copy code
factorial :: Int -> Int
factorial 0 = 1
factorial n = n * factorial (n - 1)
6. Partial Functions
• Definition: A partial function is a function that is not defined for all possible inputs of its type. It
may produce an error or undefined result for some inputs.
• Examples:
haskell
Copy code
head :: [a] -> a
head [] = error "empty list"
head (x:xs) = x
The head function is partial because it fails (throws an error) when applied to an empty list.
18
7. Total Functions
• Definition: A total function is the opposite of a partial function; it is defined for every possible input
of its type.
• Characteristics: It always produces a valid output for any valid input.
• Examples:
haskell
Copy code
safeHead :: [a] -> Maybe a
safeHead [] = Nothing
safeHead (x:xs) = Just x
safeHead is a total function because it handles all possible inputs, including an empty list.
8. Curried Functions
• Definition: Currying is the process of transforming a function that takes multiple arguments into a
series of functions that each take a single argument. In Haskell, all functions are curried by default.
• Usage: Allows partial application of functions.
• Examples:
haskell
Copy code
add :: Int -> Int -> Int
add x y = x + y
-- Using currying
increment = add 1
increment 5 -- Result: 6
Here, add is a curried function. When partially applied, it can be used as an increment function.
• Definition: These functions adhere strictly to the principles of functional programming, being both
pure and stateless. They do not depend on or alter any external state.
• Characteristics:
o No Side Effects: They do not perform any I/O operations, modify variables, or interact with
the outside world.
o Referential Transparency: The function’s output depends only on its input and can be
substituted with its output without changing the program's behavior.
• Examples:
haskell
Copy code
addPure :: Int -> Int -> Int
addPure x y = x + y
• Definition: An impure function is one that interacts with the external world or has side effects, such
as reading from or writing to a file, modifying a global variable, or generating random numbers.
• Usage: Necessary for performing real-world operations, but often minimized or controlled in
functional programming.
• Examples:
haskell
Copy code
-- An impure function that reads a line from standard input
getLine :: IO String
19
11. Generator Functions
• Definition: A generator function produces a sequence of values, typically one at a time, rather than
producing the entire sequence at once. This is useful for working with potentially infinite sequences
or large datasets.
• Examples:
haskell
Copy code
-- A simple infinite list generator
numbersFrom n = n : numbersFrom (n + 1)
• Definition: These are functions that operate on types rather than values. They are often used in the
context of generic programming and type-level computation.
• Examples: In Haskell, type classes like Functor and Monad use higher-kinded functions:
haskell
Copy code
fmap :: Functor f => (a -> b) -> f a -> f b
Here, fmap is a higher-kinded function that applies a function over a structure (like a list or maybe).
1. Parametric Polymorphism
Definition: Parametric polymorphism allows a function or data type to be written generically so that it can
handle values uniformly without depending on their type. The function operates on any type in a way that is
type-independent.
Characteristics:
• The function’s behavior does not depend on the specific type of its arguments.
• Often implemented using type variables.
Examples:
• Identity Function: A simple example is the identity function, which returns its input unchanged:
haskell
Copy code
identity :: a -> a
identity x = x
Here, a is a type variable, meaning identity can take any type as input and return the same type as
output. The function is polymorphic because it works with any type.
20
Lists in Haskell are another example of parametric polymorphism:
haskell
Copy code
length :: [a] -> Int
length [] = 0
length (_:xs) = 1 + length xs
The length function works for a list of any type, indicated by the type variable a.
Advantages:
• Reusability: Functions written with parametric polymorphism can be reused with different types,
reducing code duplication.
• Type Safety: Even though the function is generic, it is still type-safe because it operates consistently
regardless of the type.
Definition: Ad-hoc polymorphism refers to the ability of a function to behave differently depending on the
types of its arguments. This is typically achieved through function overloading or using type classes in
languages like Haskell.
Characteristics:
• The same function name can have different implementations based on the type of its arguments.
• Often associated with type classes in functional programming.
Examples:
Haskell uses type classes to achieve ad-hoc polymorphism. For instance, the + operator works with
different numeric types (integers, floating-point numbers) through the Num type class:
haskell
Copy code
class Num a where
(+) :: a -> a -> a
...
Here, the + operator is polymorphic because its behavior is determined by the type of its arguments,
but different instances can define different behaviors.
You can define your own type classes to create ad-hoc polymorphic functions. For example,
a Printable type class might be used to define how different types should be converted to strings:
haskell
Copy code
class Printable a where
printIt :: a -> String
21
instance Printable Int where
printIt x = show x
The printIt function can be used with any type that has an instance of the Printable class, but its
behavior will vary depending on the type.
Advantages:
• Flexibility: Functions can be tailored to handle specific types in specific ways, allowing for more
expressive code.
• Code Clarity: Overloading allows functions to share the same name when they conceptually
represent the same operation but act differently based on type.
Although subtyping polymorphism is more commonly associated with object-oriented programming, it can
appear in functional programming when using languages that support both functional and object-oriented
paradigms (e.g., Scala).
Definition: Subtyping polymorphism allows a function to operate on arguments of different types that are
related by some hierarchy (such as a parent-child class relationship).
Example in Scala:
scala
Copy code
class Animal {
def sound(): String = "Some sound"
}
Here, makeSound can operate on any Animal or its subtype, such as Dog, exhibiting subtyping
polymorphism.
4. Polymorphic Recursion
Polymorphic recursion occurs when a function calls itself with a different type. This is a more advanced
form of polymorphism and is not common in most functional programming languages, but it can be found in
certain contexts.
Example:
haskell
Copy code
data NestedList a = Elem a | List [NestedList a]
22
In this example, flatten is polymorphically recursive because it processes elements of potentially different
types during its recursive calls.
Conclusion
Polymorphism in functional programming is a powerful concept that enables more abstract, reusable, and
flexible code. The primary forms of polymorphism in functional programming are:
• Parametric Polymorphism: Functions and data structures operate uniformly across any type.
• Ad-hoc Polymorphism: Functions can behave differently depending on the type of their arguments,
often implemented via type classes in languages like Haskell.
• Subtyping Polymorphism: (Less common in pure functional languages) Allows a function to
operate on a hierarchy of types.
1. map
• Definition: map applies a function to every element of a list, producing a new list with the results.
• Type Signature: (a -> b) -> [a] -> [b]
• Usage:
haskell
Copy code
-- Example function to double each element in a list
double :: Int -> Int
double x = x * 2
Explanation: In this example, map takes the double function and applies it to each element of the
list [1, 2, 3, 4].
2. filter
• Definition: filter takes a predicate function (a function that returns a boolean) and a list, returning
a new list containing only those elements that satisfy the predicate.
• Type Signature: (a -> Bool) -> [a] -> [a]
• Usage:
haskell
Copy code
-- Example predicate to check if a number is even
isEven :: Int -> Bool
isEven x = x `mod` 2 == 0
• Definition: foldl (fold left) and foldr (fold right) are used to reduce a list to a single value by
repeatedly applying a function, starting from the left or right side of the list, respectively.
• Type Signature:
o foldl: (b -> a -> b) -> b -> [a] -> b
o foldr: (a -> b -> b) -> b -> [a] -> b
• Usage:
haskell
Copy code
-- Example using foldl to sum a list of integers
sumList :: [Int] -> Int
sumList xs = foldl (+) 0 xs -- Result: sumList [1, 2, 3, 4] -> 10
Explanation:
o foldl processes the list from left to right, starting with an initial accumulator value.
o foldr processes the list from right to left, which is useful when the operation is naturally
right-associative or when constructing data structures like lists.
4. zipWith
• Definition: zipWith takes a function and two lists, and applies the function pairwise to the elements
of the lists, producing a new list of results.
• Type Signature: (a -> b -> c) -> [a] -> [b] -> [c]
• Usage:
haskell
Copy code
-- Example using zipWith to add corresponding elements of two lists
addPairs :: [Int] -> [Int] -> [Int]
addPairs xs ys = zipWith (+) xs ys -- Result: addPairs [1, 2, 3] [4, 5, 6] ->
[5, 7, 9]
Explanation: zipWith combines the corresponding elements of two lists using the provided function
(+ in this case).
Explanation:
o takeWhile stops processing the list once the predicate is no longer true.
o dropWhile discards elements while the predicate is true, returning the remainder of the list.
• Definition: These functions check whether all or any elements of a list satisfy a predicate:
o all returns True if the predicate is True for all elements.
o any returns True if the predicate is True for at least one element.
• Type Signatures:
o all: (a -> Bool) -> [a] -> Bool
o any: (a -> Bool) -> [a] -> Bool
• Usage:
haskell
Copy code
-- Example using all to check if all elements are positive
all (> 0) [1, 2, 3, 4] -- Result: True
Explanation:
• Definition: These are similar to foldl and foldr, but they do not require an initial accumulator
value. Instead, they use the first (or last) element of the list as the initial value.
• Type Signatures:
o foldl1: (a -> a -> a) -> [a] -> a
o foldr1: (a -> a -> a) -> [a] -> a
• Usage:
haskell
Copy code
-- Example using foldl1 to find the maximum element in a list
maximum :: [Int] -> Int
maximum xs = foldl1 max xs -- Result: maximum [1, 2, 3, 4] -> 4
Explanation:
o foldl1 and foldr1 are useful when you know the list is non-empty and you want to avoid
specifying an initial value.
25
• Definition: These functions are similar to foldl and foldr, but they return a list of all intermediate
accumulator states.
• Type Signatures:
o scanl: (b -> a -> b) -> b -> [a] -> [b]
o scanr: (a -> b -> b) -> b -> [a] -> [b]
• Usage:
haskell
Copy code
-- Example using scanl to show the cumulative sum of a list
scanl (+) 0 [1, 2, 3, 4] -- Result: [0, 1, 3, 6, 10]
-- Example using scanr to show the cumulative sum from the right
scanr (+) 0 [1, 2, 3, 4] -- Result: [10, 9, 7, 4, 0]
Explanation:
Map
The map function is a fundamental higher-order function in functional programming, used to apply a given
function to each element of a list, resulting in a new list of transformed elements. It's widely used for list
processing and transformation, enabling concise and expressive code.
Definition
In Haskell and similar functional programming languages, the map function has the following type signature:
haskell
Copy code
map :: (a -> b) -> [a] -> [b]
• (a -> b): This represents a function that takes an element of type a and returns a value of type b.
• [a]: This is a list of elements of type a.
• [b]: The result is a list of elements of type b, which is the result of applying the function to each
element in the input list.
The map function iterates over each element in the input list, applies the given function to each element, and
collects the results into a new list.
Example Usage
Suppose you have a list of integers and you want to double each number:
haskell
Copy code
double :: Int -> Int
double x = x * 2
26
Here, map applies the double function to each element of the list [1, 2, 3, 4], resulting in [2, 4, 6,
8].
You can also use anonymous functions (lambdas) directly with map without defining a separate function:
haskell
Copy code
doubledList = map (\x -> x * 2) [1, 2, 3, 4]
-- Result: [2, 4, 6, 8]
This example achieves the same result but uses a lambda expression (\x -> x * 2) directly in the map call.
3. Transforming Strings
map can also be used with functions that transform characters in strings:
haskell
Copy code
uppercase :: Char -> Char
uppercase c = toUpper c
Here, map converts each character in the string "hello" to uppercase, resulting in "HELLO".
4. Complex Transformations
You can perform more complex transformations by passing more elaborate functions to map. For example,
suppose you have a list of tuples representing points in a 2D space, and you want to translate all points by a
certain offset:
haskell
Copy code
translate :: (Int, Int) -> (Int, Int)
translate (x, y) = (x + 10, y + 5)
In this case, map applies the translate function to each point, shifting each by (10, 5).
Advantages of map
• Immutability: map does not modify the original list but returns a new list with the transformed
elements, preserving immutability.
• Conciseness: Using map often leads to more concise and readable code compared to explicit loops.
• Higher Abstraction: map abstracts the pattern of applying a function to each element of a list,
promoting code reuse and functional composition.
Filter
The filter function is another key higher-order function in functional programming that allows you to
selectively include elements from a list based on a predicate function. This function is essential for list
processing, enabling you to create new lists that contain only those elements that satisfy certain conditions.
Definition
27
In Haskell and similar functional programming languages, the filter function has the following type
signature:
haskell
Copy code
filter :: (a -> Bool) -> [a] -> [a]
• (a -> Bool): This is a predicate function that takes an element of type a and returns a boolean
value (True or False), determining whether the element should be included in the result.
• [a]: This is the input list of elements of type a.
• [a]: The result is a new list of elements of type a that satisfy the predicate (i.e., for which the
predicate returns True).
The filter function iterates over each element of the input list, applies the predicate function to each
element, and constructs a new list containing only the elements for which the predicate returns True.
Example Usage
Suppose you have a list of integers and want to keep only the even numbers:
haskell
Copy code
isEven :: Int -> Bool
isEven x = x `mod` 2 == 0
In this example, filter applies the isEven predicate to each number in the list [1, 2, 3, 4, 5, 6],
resulting in a new list with only the even numbers.
You can also use anonymous functions (lambdas) directly with filter:
haskell
Copy code
evenNumbers = filter (\x -> x `mod` 2 == 0) [1, 2, 3, 4, 5, 6]
-- Result: [2, 4, 6]
This example achieves the same result but uses a lambda expression (\x -> x mod 2 == 0) directly in
the filter call.
3. Filtering Strings
filter can also be used to filter characters in a string. For instance, to keep only the alphabetic characters
from a string:
haskell
Copy code
import Data.Char (isAlpha)
28
Here, filter uses the isAlpha predicate to keep only the alphabetic characters, removing punctuation and
digits.
You can combine multiple conditions in a predicate. For example, to filter out numbers that are both positive
and divisible by 3:
haskell
Copy code
isPositiveAndDivisibleBy3 :: Int -> Bool
isPositiveAndDivisibleBy3 x = x > 0 && x `mod` 3 == 0
In this example, filter keeps only the numbers that are both positive and divisible by 3.
Advantages of filter
• Selective Inclusion: filter allows you to include only the elements that satisfy a given condition,
making it easy to extract relevant data from a list.
• Immutability: Like map, filter returns a new list and does not modify the original list, maintaining
immutability.
• Declarative Code: Using filter often results in more declarative and readable code, as it abstracts
away the details of the iteration and condition checking.
List Comprehension
List comprehension is a concise and expressive way to create and manipulate lists in functional
programming. It provides a syntactic construct for generating lists by specifying their elements in terms of
existing lists, applying transformations, and filtering elements.
• expression: This is the value that will be included in the resulting list. It can involve variables
defined by the qualifiers.
• qualifier1, qualifier2, ..., qualifierN: These are constraints or generators that define how
the elements of the resulting list are derived. Qualifiers can include:
o Generators: These specify lists to iterate over.
o Filters: These specify conditions that elements must satisfy to be included.
Basic Examples
29
Here, x <- [1, 2, 3, 4, 5] is a generator that iterates over each number, and x^2 computes the square of
each number.
2. With Filtering
In this example:
3. Multiple Generators
Here, x is generated from the first list and y from the second list, resulting in all possible pairs.
Advanced Examples
In this example:
To extract all the words that start with a capital letter from a list of sentences:
haskell
Copy code
sentences = ["Hello world", "Functional programming is fun", "Learn Haskell"]
capitalWords = [word | sentence <- sentences, word <- words sentence, isUpper (head
word)]
-- Result: ["Hello", "Functional", "Learn", "Haskell"]
Here:
30
Advantages of List Comprehensions
• Readability: List comprehensions provide a clear and declarative way to create lists.
• Conciseness: They often result in more concise code compared to using explicit loops and
intermediate lists.
• Expressiveness: They allow for expressive manipulation of lists with both transformations and
filtering in a single construct.
Computation as rewriting
Computation as rewriting is a conceptual approach to understanding computation where programs and
data are treated as expressions that can be rewritten according to specific rules. This perspective is often
used in the context of functional programming and formal systems, where computation is seen as the process
of transforming expressions through the application of rewriting rules.
Key Concepts
1. Rewriting Systems: Rewriting systems consist of a set of rules for transforming expressions. These
rules specify how parts of an expression can be replaced with other expressions. A common example
is the lambda calculus, where functions and their applications are rewritten based on specific rules.
2. Reduction Rules: In computation as rewriting, reduction rules are used to simplify or transform
expressions. These rules define how one expression can be transformed into another. For example, in
lambda calculus, function application follows the rule of β-reduction, where (λx.M) N is rewritten
to M[x := N].
3. Term Rewriting: In term rewriting systems, terms (which can be expressions or data structures) are
transformed according to a set of rewrite rules. The process involves systematically applying rules to
terms until a desired form is reached. This approach is used in programming languages and formal
proofs.
4. Normal Forms: A normal form is a form of an expression that cannot be reduced further using the
rewriting rules. In some systems, such as lambda calculus, an expression is considered fully
evaluated when it is in normal form. Finding normal forms is often an important goal in computation
as rewriting.
1. Lambda Calculus: Lambda calculus is a formal system where computation is expressed as function
application and reduction. For example:
o Expression: (λx. x + 1) 5
o Reduction: Applying the function to the argument, this reduces to 5 + 1, which simplifies
to 6.
2. String Rewriting Systems: In string rewriting, rules are used to transform strings. For example:
o Rules: AB -> BA, BA -> AA
o String: Starting with AB, applying the rules might transform it to BA and then to AA.
3. Logic Programming: In logic programming languages like Prolog, computation is performed by
rewriting goals into subgoals based on a set of inference rules. This process involves searching
through possible rule applications to find solutions to logical queries.
1. Formal Verification: Rewriting systems provide a formal framework for reasoning about program
correctness and proving properties of programs. They are used in formal methods to ensure that
programs meet their specifications.
31
2. Functional Programming: In functional programming languages, computation is often based on
rewriting expressions, where functions and their applications are transformed according to the rules
of the language. This leads to a more declarative and mathematical approach to programming.
3. Compilers and Interpreters: Rewriting techniques are used in compilers and interpreters to
optimize code and perform transformations such as inlining, constant folding, and loop optimization.
4. Proof Systems: Rewriting systems are used in proof systems to transform logical statements and
proofs into simpler forms. This includes rewriting logical formulas and manipulating mathematical
proofs.
Lazy Evaluations
Lazy evaluation is a programming concept where expressions are not evaluated until their values are
needed. This technique is primarily used in functional programming languages to improve performance and
enable new programming paradigms.
Key Concepts
1. Deferred Evaluation: In lazy evaluation, expressions are not immediately evaluated when they are
bound to variables. Instead, a "thunk" (a deferred computation) is created, which represents the
expression and its future result. The expression is only evaluated when its value is actually required.
2. Thunks: A thunk is a placeholder for a value that has not yet been computed. When the value is
needed, the thunk is evaluated to produce the result. This mechanism helps avoid unnecessary
computations and can lead to performance optimizations.
3. Memoization: To prevent repeated evaluations of the same expression, lazy evaluation often
involves memoization. Once an expression is evaluated, its result is stored and reused whenever the
expression is encountered again.
4. Infinite Data Structures: Lazy evaluation allows for the creation and manipulation of infinite data
structures, such as infinite lists. Since values are computed only when needed, it is possible to work
with data structures that conceptually have no end, as long as the program only requests a finite
portion of them.
Examples
1. Basic Example
Consider a simple example where lazy evaluation is used to compute the factorial of numbers only as
needed:
haskell
Copy code
factorials :: [Integer]
factorials = scanl (*) 1 [1..]
Here, factorials is an infinite list of factorials. Due to lazy evaluation, only the required portion of this list
is computed when take 5 factorials is called.
Lazy evaluation makes it possible to work with infinite lists. For example:
haskell
Copy code
naturals :: [Integer]
naturals = [1..]
32
-- To get the first 10 natural numbers:
take 10 naturals
-- Result: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
The list naturals is infinite, but only the first 10 elements are computed and returned due to the use
of take.
Lazy evaluation can help avoid unnecessary computations by delaying the evaluation of expressions until
their values are needed:
haskell
Copy code
let x = undefined
y = x + 1
in y
In this case, x is undefined, but y will only be evaluated if it is actually used. If y is never used, the
program avoids an error related to undefined.
Advantages
1. Efficiency: Lazy evaluation can improve efficiency by avoiding unnecessary computations and
reducing the amount of work done by the program.
2. Modularity: It enables more modular code by allowing functions and data structures to be defined in
a more declarative manner without worrying about the order of evaluation.
3. Infinite Data Structures: It allows for the creation and manipulation of infinite data structures,
which can be useful for problems that involve streams or potentially unbounded data.
4. Composition: Lazy evaluation facilitates the composition of functions and data transformations,
making it easier to build complex processing pipelines.
Disadvantages
1. Space Complexity: Lazy evaluation can sometimes lead to increased space usage due to the
accumulation of thunks, which may result in higher memory consumption if not managed properly.
2. Debugging Difficulty: Debugging can be more challenging in lazy evaluation due to the deferred
nature of computations. Tracing and understanding the flow of evaluations may be less
straightforward.
3. Performance Overheads: While lazy evaluation can improve performance in many cases, it can
also introduce overhead due to the management of thunks and potential multiple evaluations of the
same expression.
In functional programming languages like Haskell, lists and other data structures can be defined recursively
in such a way that they generate an endless sequence of elements. Because of lazy evaluation, these
elements aren't computed all at once; instead, they are produced on demand as the program requires them.
33
Examples of Infinite Data Structures
This list, naturals, is conceptually infinite. The range [1..] generates numbers starting from 1 and
continues indefinitely. However, due to lazy evaluation, Haskell only computes as many elements as are
actually required by the program.
Here, take 10 requests the first 10 elements from the naturals list. The program evaluates only those 10
elements, leaving the rest unevaluated.
In this example:
Just like with the naturals, only the first 10 Fibonacci numbers are computed.
This creates an infinite list where the string "ABC" repeats indefinitely. Accessing the first 9 characters
would look like this:
haskell
Copy code
take 9 cycleABC
34
-- Result: "ABCABCABC"
1. Elegant Solutions: Infinite data structures allow for very elegant and concise solutions to problems
that involve streams of data or iterative processes.
2. Composability: They enable easy composition of functions that process potentially unbounded data.
You can build pipelines that process elements as they are needed without worrying about the size of
the data set.
3. Modularity: By defining infinite data structures, you can separate concerns in your code, focusing
on how data is generated and processed separately from how much of it is needed.
1. Streams: Infinite lists are often used to model streams of data, such as real-time input or continuous
sequences of values.
2. Iterative Processes: Problems that involve generating sequences through iteration (like the
Fibonacci sequence) are naturally expressed using infinite data structures.
3. Lazy Algorithms: Algorithms that can produce partial results and work incrementally benefit from
being implemented using infinite data structures.
1. Termination: When using infinite data structures, it's important to ensure that your program
consumes only a finite portion. Otherwise, it could lead to non-terminating computations.
2. Memory Usage: Infinite data structures can lead to high memory consumption if not handled
properly. For example, if elements are retained in memory longer than necessary, it could cause a
memory leak.
3. Debugging: Understanding how and when elements of an infinite data structure are evaluated can be
challenging, especially in a lazy evaluation context.
1. Function Overloading:
o In many programming languages, you can define multiple versions of a function with the
same name but different parameter types or numbers of parameters.
o The compiler or interpreter determines which version of the function to invoke based on the
types of the arguments passed.
o For example, in C++:
cpp
Copy code
int add(int a, int b) {
return a + b;
}
35
Here, the add function is overloaded to handle both integers and doubles.
2. Operator Overloading:
o Operators like +, -, *, etc., can be overloaded to work with different types.
o For instance, in C++:
cpp
Copy code
class Complex {
public:
double real, imag;
Complex(double r, double i) : real(r), imag(i) {}
object ShowInstances {
implicit val intShow: Show[Int] = new Show[Int] {
def show(a: Int) = a.toString
}
36
▪ Here, the printShow function will automatically pick the correct implementation
based on the type of A.
1. Code Reusability: It allows for reusing the same function name or operator for different types,
leading to cleaner and more intuitive code.
2. Type Safety: The compiler checks that the correct version of a function or operator is being used,
reducing the chance of runtime errors.
3. Readability: Overloading allows functions to be named based on their functionality rather than the
type they operate on, improving code readability.
1. Complexity: Overloading can lead to ambiguity, especially if multiple overloads are possible for the
given arguments. This can make code harder to understand and maintain.
2. Potential for Misuse: If overloading is not done carefully, it can lead to unexpected behavior,
especially when implicit conversions or type coercions come into play.
3. Performance Overhead: In some languages, resolving which function or operator to use at compile-
time or run-time can introduce performance overhead.
void print(double d) {
std::cout << "Double: " << d << std::endl;
}
void print(std::string s) {
std::cout << "String: " << s << std::endl;
}
object ShowInstances {
implicit val intShow: Show[Int] = new Show[Int] {
37
def show(a: Int) = s"Int: $a"
}
println(showIt(42))
println(showIt(true))
type classes
Type classes are a powerful feature in functional programming languages like Haskell that allow for a form
of ad-hoc polymorphism, also known as conditional polymorphism. They enable you to define generic
interfaces that can be implemented by different types, allowing for polymorphic behavior where the specific
implementation used depends on the type of the arguments involved.
A type class is essentially a collection of types that support a specific set of operations. It defines a set of
functions or methods that can be applied to different types. When a type implements a type class, it provides
concrete implementations for those functions.
Here:
• Eq is a type class.
• The type variable a represents any type that can be compared for equality.
• The == and /= functions are part of the Eq interface and must be implemented for any type that is an
instance of Eq.
To use a type with a type class, you need to create an instance of the type class for that type, which means
providing implementations for the functions defined by the type class.
For example, let's create an instance of the Eq type class for a custom data type Point:
haskell
Copy code
data Point = Point Int Int
Here:
38
• Point is a custom data type representing a point in 2D space.
• The Eq instance for Point is defined by providing implementations for the == and /= functions.
p1 == p2 -- True
p1 /= p3 -- True
Type classes enable conditional polymorphism because the behavior of a function can change based on the
type of its arguments. For example, the == operator behaves differently depending on whether you are
comparing Ints, Strings, or Points, because each of these types has its own instance of the Eq type class.
Type classes enable ad-hoc polymorphism, where different types can have different implementations of the
same function, depending on the type's instance of the type class.
This contrasts with parametric polymorphism, where functions are written generically to work with any
type. For example, the function id :: a -> a works with any type a because it does not rely on any
specific operations on a.
Type Constraints
You can restrict a function to only work with types that are instances of a particular type class using type
constraints.
For example:
haskell
Copy code
maximum :: (Ord a) => [a] -> a
maximum [] = error "empty list"
maximum [x] = x
maximum (x:xs) = max x (maximum xs)
Here, the (Ord a) => part means that maximum only works with types a that are instances of the Ord type
class (i.e., types that can be ordered).
39
Haskell also supports multi-parameter type classes, where a type class can take more than one type
parameter. This allows you to define more complex relationships between types.
For example:
haskell
Copy code
class Convertible a b where
convert :: a -> b
Here, Convertible is a type class that defines a conversion between two types a and b.
User-defined data types are created using the data keyword in Haskell. A custom data type can have
multiple constructors, each of which can take different types of arguments.
In this example:
You can also define constructors that take arguments, which allows you to create more complex data types:
haskell
Copy code
data Point = Point Int Int
Here:
Usage example:
haskell
Copy code
40
origin :: Point
origin = Point 0 0
anotherPoint :: Point
anotherPoint = Point 5 7
Haskell's custom data types are often referred to as algebraic data types because they can be formed by
combining other types using "sum" (using the | symbol) and "product" (by grouping types together in a
constructor).
Sum types (using |) allow a type to have multiple possible forms. For example:
haskell
Copy code
data Shape = Circle Float | Rectangle Float Float
Here:
• Shape is a type that can either be a Circle (with a Float radius) or a Rectangle (with
a Float width and height).
Usage example:
haskell
Copy code
aCircle :: Shape
aCircle = Circle 5.0
aRectangle :: Shape
aRectangle = Rectangle 10.0 20.0
Data types can also be recursive, meaning they can refer to themselves. This is useful for defining structures
like lists or trees.
In this example:
• MyList is a generic list type that can hold elements of any type a.
• Empty represents an empty list.
• Cons is a constructor that takes an element of type a and another MyList a (the rest of the list).
Usage example:
haskell
Copy code
myList :: MyList Int
myList = Cons 1 (Cons 2 (Cons 3 Empty))
41
One of the powerful features of custom data types is that they can be deconstructed using pattern matching.
Pattern matching allows you to define functions that operate differently depending on which constructor was
used to create the value.
Here, the area function calculates the area of a Shape, handling Circle and Rectangle differently based on
the constructor used.
You can automatically make your custom data types instances of certain type classes like Eq, Ord, Show, and
others by using the deriving keyword.
Example:
haskell
Copy code
data Color = Red | Green | Blue deriving (Eq, Show)
Now Color can be compared for equality (because it derives Eq) and can be converted to a String (because
it derives Show):
haskell
Copy code
Red == Green -- False
show Red -- "Red"
• BinaryTree is a generic binary tree type that can store elements of any type a.
• EmptyTree represents an empty tree.
• Node represents a tree node containing a value and two subtrees (left and right).
Usage example:
haskell
Copy code
singleton :: a -> BinaryTree a
singleton x = Node x EmptyTree EmptyTree
42
Lists
In functional programming, particularly in languages like Haskell, lists are one of the most fundamental and
versatile data structures. They are used to store sequences of elements, all of the same type, and are highly
integrated into the language's syntax and standard library.
Basics of Lists
A list in Haskell is a collection of elements enclosed in square brackets [], with elements separated by
commas.
For example:
haskell
Copy code
numbers :: [Int]
numbers = [1, 2, 3, 4, 5]
Here:
List Syntax
Haskell provides a rich set of operations for working with lists. Here are some of the most common ones:
1. Concatenation (++)
2. Cons (:)
The : operator (also known as "cons") adds an element to the front of a list.
haskell
Copy code
1 : [2, 3, 4] -- Result: [1, 2, 3, 4]
4. List Length
5. Indexing
You can access an element at a specific position using the !! operator (zero-indexed).
haskell
Copy code
[1, 2, 3] !! 1 -- Result: 2
6. List Ranges
Haskell allows you to create lists of sequential values easily using ranges.
haskell
Copy code
[1..5] -- Result: [1, 2, 3, 4, 5]
['a'..'e'] -- Result: ['a', 'b', 'c', 'd', 'e']
[2,4..10] -- Result: [2, 4, 6, 8, 10] -- Even numbers from 2 to 10
7. List Comprehensions
List comprehensions provide a concise way to create lists by specifying a pattern for elements and optional
filtering conditions.
haskell
Copy code
[x*2 | x <- [1..5]] -- Result: [2, 4, 6, 8, 10]
-- With a condition
[x*2 | x <- [1..5], x*2 >= 6] -- Result: [6, 8, 10]
Haskell's standard library includes many higher-order functions that operate on lists. Some of the most
important ones include:
1. map
2. filter
44
These are fold functions that reduce a list to a single value by applying a function
recursively. foldl processes the list from the left, while foldr processes it from the right.
haskell
Copy code
foldl (+) 0 [1, 2, 3, 4] -- Result: 10 (1+2+3+4)
foldr (*) 1 [1, 2, 3, 4] -- Result: 24 (1*2*3*4)
Pattern matching is an essential feature for working with lists in Haskell, particularly in function definitions.
This function returns True if the list is empty and False otherwise.
Infinite Lists
Due to Haskell's lazy evaluation, you can work with infinite lists. They are defined just like regular lists, but
the elements are generated on demand.
You can safely work with this infinite list as long as you only access a finite portion of it:
haskell
Copy code
take 10 naturals -- Result: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Haskell's standard library includes many useful functions for working with lists, including:
45
• zip: Combines two lists into a list of pairs.
queues
In functional programming and computer science in general, a queue is an abstract data structure that
follows the First-In-First-Out (FIFO) principle. This means that the first element added to the queue will be
the first one to be removed. Queues are commonly used in scenarios where you need to manage a collection
of elements in the order they were added, such as in scheduling tasks, managing processes, or handling
requests.
In functional programming languages like Haskell, a queue can be implemented using different techniques.
Below, we'll discuss a few ways to implement a queue, focusing on both simple and more efficient
approaches.
A straightforward way to implement a queue in Haskell is to use a list. You can treat the head of the list as
the front of the queue and the tail as the end of the queue.
Example usage:
haskell
Copy code
let q = enqueue 1 (enqueue 2 (enqueue 3 []))
46
let (front, q1) = dequeue q -- front = 1, q1 = [2, 3]
let frontElement = peek q1 -- frontElement = 2
let emptyCheck = isEmpty q1 -- emptyCheck = False
While this implementation is simple, it has a performance drawback: the enqueue operation is O(n) because
it appends an element to the end of the list. This can be inefficient for large queues.
A more efficient queue implementation in Haskell uses two lists to represent the front and rear of the queue.
The idea is to keep the front of the queue in one list ( front), and the elements to be added in another list
(rear). When the front list becomes empty, the rear list is reversed and becomes the new front list.
Example usage:
haskell
Copy code
let q = enqueue 1 (enqueue 2 (enqueue 3 (Queue [] [])))
let (front, q1) = dequeue q -- front = 1, q1 = Queue [2, 3] []
let frontElement = peek q1 -- frontElement = 2
let emptyCheck = isEmpty q1 -- emptyCheck = False
In this implementation:
This approach makes the operations more efficient overall, particularly for queues that grow large.
Other Implementations
• Deque (Double-ended Queue): A deque allows insertion and removal from both ends. This can be
implemented using a pair of lists similar to the efficient queue above.
47
• Functional Data Structures: In functional programming, queues can also be implemented using
more advanced data structures like banker's queues or real-time queues, which are optimized for
purely functional environments.
• Task Scheduling: Managing tasks in a system where they need to be processed in the order they
arrive.
• Breadth-First Search (BFS): In graph algorithms, a queue is used to explore nodes layer by layer.
• Buffering: In networking, queues are used to buffer packets to manage flow control.
• Print Queue: Managing print jobs in the order they were sent to the printer.
Trees
Trees are a fundamental data structure used to represent hierarchical relationships in data. Unlike linear data
structures like arrays or lists, trees allow data to be stored in a hierarchical fashion, making them ideal for
representing data with a natural hierarchy, such as organizational charts, file systems, or the structure of
XML/HTML documents.
1. Node: The basic unit of a tree, which contains data and references (links) to other nodes.
2. Root: The topmost node in a tree. It is the starting point for any traversal of the tree.
3. Child: A node that is directly connected to another node when moving away from the root.
4. Parent: A node that has one or more children.
5. Leaf: A node that does not have any children (i.e., it is at the bottom of the tree).
6. Subtree: Any node and its descendants form a subtree.
7. Height/Depth: The height of a node is the number of edges on the longest downward path between
that node and a leaf. The depth of a node is the number of edges from the root to that node.
8. Binary Tree: A tree where each node has at most two children, typically referred to as the left and
right child.
Types of Trees
1. Binary Tree: A tree where each node has at most two children. It's one of the simplest and most
widely used types of trees.
2. Binary Search Tree (BST): A binary tree where the left child contains nodes with values less than
the parent node, and the right child contains nodes with values greater than the parent node. This
property makes it efficient for search operations.
3. Balanced Tree: A tree where the height of the left and right subtrees of any node differ by at most
one. Examples include AVL trees and Red-Black trees.
4. B-Tree: A self-balancing tree data structure that maintains sorted data and allows searches,
insertions, deletions, and sequential access in logarithmic time. It’s widely used in databases and file
systems.
5. Heap: A special type of binary tree where the parent node is either greater than or equal to (in a max-
heap) or less than or equal to (in a min-heap) the values of its children.
6. Trie (Prefix Tree): A tree used to store a dynamic set or associative array where the keys are usually
strings.
7. N-ary Tree: A tree where a node can have at most N children. This is a generalization of a binary
tree.
48
Common Operations on Trees
1. Traversal: Visiting all the nodes in the tree. The most common types of traversal are:
o Pre-order: Visit the root, traverse the left subtree, then traverse the right subtree.
o In-order: Traverse the left subtree, visit the root, then traverse the right subtree.
o Post-order: Traverse the left subtree, traverse the right subtree, then visit the root.
o Level-order (Breadth-First): Visit nodes level by level, starting from the root.
2. Insertion: Adding a new node to the tree while maintaining the tree’s properties.
3. Deletion: Removing a node from the tree while maintaining the tree’s properties.
4. Searching: Finding a node in the tree that contains a specific value.
5. Balancing: Adjusting the tree to maintain its height balance, especially after insertions and deletions
in structures like AVL trees or Red-Black trees.
In functional programming languages like Haskell, trees are typically implemented using recursive data
types. Here’s an example of how a simple binary tree might be defined in Haskell:
haskell
Copy code
-- Define a binary tree data type
data BinaryTree a = Empty
| Node a (BinaryTree a) (BinaryTree a)
deriving (Show, Eq)
-- Example of a tree
tree :: BinaryTree Int
tree = Node 1
(Node 2 Empty Empty)
(Node 3
(Node 4 Empty Empty)
Empty)
-- Pre-order traversal
preOrder :: BinaryTree a -> [a]
preOrder Empty = []
preOrder (Node value left right) =
[value] ++ preOrder left ++ preOrder right
-- In-order traversal
inOrder :: BinaryTree a -> [a]
inOrder Empty = []
inOrder (Node value left right) =
inOrder left ++ [value] ++ inOrder right
-- Post-order traversal
postOrder :: BinaryTree a -> [a]
postOrder Empty = []
postOrder (Node value left right) =
postOrder left ++ postOrder right ++ [value]
49
• Pre-order traversal would visit the nodes in the order: [1, 2, 3, 4]
• In-order traversal would visit the nodes in the order: [2, 1, 4, 3]
• Post-order traversal would visit the nodes in the order: [2, 4, 3, 1]
Input/output
In computing, input and output (I/O) refer to the communication between a system (such as a computer)
and the external environment (such as users or other systems). Understanding I/O operations is essential for
creating interactive applications, handling data, and managing system interactions.
Input/Output Overview
1. Input: Refers to data received by the system from external sources. This can include user input via
keyboard, mouse, or touch; data from files; or signals from other programs or devices.
2. Output: Refers to data sent from the system to external destinations. This can include displaying
information on a screen, writing data to a file, sending signals to other programs, or printing on
paper.
In functional programming languages like Haskell, I/O operations are managed in a way that preserves the
functional programming principles, particularly purity and immutability.
I/O in Haskell
In Haskell, I/O operations are handled using the IO monad, which encapsulates side effects and ensures that
functional purity is maintained.
50
putStrLn "Enter your name:"
name <- getLine
putStrLn ("Hello, " ++ name ++ "!")
Here, getLine reads a line of input from the user, and putStrLn prints a string to the
standard output.
o Writing to Files:
haskell
Copy code
main :: IO ()
main = do
writeFile "example.txt" "Hello, World!"
content <- readFile "example.txt"
putStrLn content
writeFile writes a string to a file, and readFile reads the content of a file.
2. Handling Errors: In Haskell, you can handle errors in I/O operations using mechanisms
like Either or Maybetypes, or more advanced libraries for exception handling.
haskell
Copy code
import System.IO.Error (catchIOError, ioError, userError)
main :: IO ()
main = do
result <- readFile "example.txt" `catchIOError` handleError
putStrLn result
3. Concurrency and Parallelism: Haskell supports concurrent and parallel programming, allowing
you to perform I/O operations in parallel or concurrently with other tasks.
haskell
Copy code
import Control.Concurrent (forkIO)
main :: IO ()
main = do
forkIO (putStrLn "This runs concurrently")
putStrLn "This runs in the main thread"
Overview of GHC
1. Purpose: GHC compiles Haskell code into machine code or intermediate code. It is designed to
handle Haskell's strong type system, lazy evaluation, and advanced features.
51
2. Features:
o Optimizations: GHC provides various optimization options to improve the performance of
generated code.
o Extensions: It supports many language extensions that go beyond the Haskell 98 standard,
allowing for more advanced programming techniques.
o Interactive Environment: GHCi is GHC's interactive environment, allowing for testing and
debugging code interactively.
Key Components
2. GHC Compiler:
o Command Line Usage: The GHC compiler can be used to compile Haskell programs into
executables or libraries. Basic usage involves running ghc followed by various flags and the
source file.
bash
Copy code
ghc -o myprogram MyProgram.hs
o Flags:
▪ -O2: Enables high-level optimizations.
▪ -Wall: Enables all warning messages.
▪ -threaded: Enables support for multi-threaded
programs.
3. Building Projects:
o Stack: A tool for managing Haskell projects, including building and dependency
management.
o Cabal: Another build tool and package manager for Haskell. It allows for more granular
control over project configuration and dependencies.
bash
Copy code
stack build
cabal build
Common Commands
2. Running GHCi:
bash
Copy code
ghci MyProgram.hs
3. Generating Documentation:
bash
52
Copy code
haddock MyProgram.hs
4. Profiling:
bash
Copy code
ghc -prof -rtsopts MyProgram.hs
./myprogram +RTS -p
Advanced Features
2. Parallel and Concurrent Programming: GHC includes support for concurrent and parallel
programming using libraries and extensions.
haskell
Copy code
import Control.Concurrent
3. Optimization: GHC offers various optimization levels to improve the performance of Haskell
programs, including inlining, strictness analysis, and more.
bash
Copy code
ghc -O2 -o optimizedProgram OptimizedProgram.hs
4. Profiling and Debugging: GHC provides tools for profiling and debugging programs,
including threadscope for visualizing performance and ghc-debug for inspecting runtime behavior.
Getting Started
1. Installation: GHC can be installed via the Haskell Platform, Stack, or Cabal. Installation instructions
are available on the GHC website.
2. Documentation: Extensive documentation is available, including the GHC User’s Guide and
Haskell Language Report. For more information, refer to the official GHC documentation.
Arrays
Arrays are a fundamental data structure used to store collections of elements, typically of the same type, in
a contiguous block of memory. They provide efficient access and manipulation of elements by their index.
Here’s a comprehensive overview of arrays:
Basic Concepts
1. Indexing:
o Arrays are accessed using indices, which are usually integers. The index specifies the
position of the element within the array.
2. Fixed Size:
o In many languages, arrays have a fixed size, meaning that their size must be specified at the
time of creation and cannot be changed afterward.
3. Contiguous Memory:
53
o Elements in an array are stored in contiguous memory locations, which allows for fast access
and iteration.
Types of Arrays
1. One-Dimensional Arrays:
o A single list of elements. For example, an array of integers or a list of names.
haskell
Copy code
-- In Haskell, lists can be used to represent one-dimensional arrays
let numbers = [1, 2, 3, 4, 5]
2. Two-Dimensional Arrays:
o An array of arrays, often used to represent matrices or grids.
haskell
Copy code
-- Example of a 2D array using lists in Haskell
let matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
3. Multi-Dimensional Arrays:
o Arrays with more than two dimensions, used for more complex data structures.
haskell
Copy code
-- Example of a 3D array using lists in Haskell
let tensor = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]]
Operations on Arrays
1. Accessing Elements:
o Access elements using indices. In many languages, this is done with zero-based indexing.
haskell
Copy code
-- Accessing elements in Haskell lists
let firstElement = head numbers -- 1
let secondElement = numbers !! 1 -- 2
2. Updating Elements:
o In mutable arrays, you can change the value of an element at a specific index.
haskell
Copy code
-- In Haskell, lists are immutable. To "update" an element, you create a new list
let updatedNumbers = take 2 numbers ++ [10] ++ drop 3 numbers
3. Iterating:
o Iterate over elements using loops or higher-order functions.
haskell
Copy code
-- Using list comprehension to double each element
let doubledNumbers = [x * 2 | x <- numbers]
4. Resizing:
o In languages with fixed-size arrays, resizing often involves creating a new array with the
desired size.
haskell
Copy code
-- Adding an element to a list (creating a new list)
let extendedNumbers = numbers ++ [6]
2. Python:
o Uses lists for dynamic arrays and the array module for fixed-type arrays. The numpy library
provides advanced array operations and multi-dimensional arrays.
python
Copy code
import numpy as np
3. Java:
o Provides fixed-size arrays and supports multi-dimensional arrays. Arrays are zero-based and
can be manipulated with built-in methods.
java
Copy code
int[] numbers = {1, 2, 3, 4, 5};
numbers[1] = 10;
4. C/C++:
o Supports fixed-size arrays with direct memory manipulation and pointer arithmetic. Multi-
dimensional arrays are also supported.
c
Copy code
int numbers[] = {1, 2, 3, 4, 5};
numbers[1] = 10;
Performance Considerations
1. Access Time:
o Arrays provide O(1) time complexity for accessing elements by index due to their contiguous
memory allocation.
2. Memory Usage:
o Fixed-size arrays have a predictable memory footprint, while dynamic arrays may involve
overhead for resizing operations.
3. Caching:
o Arrays benefit from spatial locality, which can improve cache performance.
55