Functional Programming - Exhaustive Course Notes
Foundations of Functional Programming: General Introduction
Importance of Diverse Programming Languages for Computer Scientists:
Expression of Ideas: Familiarity with various concepts allows for better expression of ideas during software development.
Project Suitability: Knowledge is required to select the most appropriate language for specific projects.
Adaptability: Learning conceptually different languages makes it easier to acquire new ones later.
Language Design: Computer scientists are often tasked with designing new languages, which must build on existing foundations.
Comparison of Paradigms:
Imperative Programming:
Programs are sequences of instructions executed one after another.
They modify variable values in memory (side effects).
Closely linked to the "Von Neumann" architecture.
Examples: C (low-level efficiency, manual memory management), Java.
Declarative Programming:
Focuses on specifying what should be calculated rather than how.
The interpreter/compiler determines the execution path.
It is problem-oriented rather than machine-oriented.
Sub-divided into Functional and Logic programming.
Case Study: Calculating List Length:
Java (Imperative):
Uses a
whileloop to iterate through memory pointers.Side Effect: In the provided example, calculating length
len(l)setsl.head = null, essentially emptying the list as a side effect. To avoid this, developers must manually manage memory/cloning.Haskell (Functional):
Defined by recursive equations:
(A) If list is empty, length is .
(B) If list is not empty (), length is .
Implementation:
len :: [a] -> Int(Type declaration)len [] = 0(Base case)len (x:xs) = 1 + len xs(Recursive case)
Uses Pattern Matching to determine which equation to apply.
Core Properties of Haskell:
No Loops: Control flow is managed via recursion.
Parametric Polymorphism: Functions (like
len) work on lists of any typea. Systems guarantee data is not misinterpreted.Referential Transparency: No side effects. Applying a function to the same arguments always yields the same result.
Automatic Memory Management: No explicit pointer manipulation or memory allocation/deallocation.
Functions as First-Class Citizens: Functions can be arguments or results of other functions (Higher-Order Functions).
Lazy Evaluation: Arguments are only evaluated if necessary for the result.
Advantages of Functional Programming:
Programs are shorter, clearer, and easier to maintain.
Less error-prone, facilitating more reliable software and verification.
Faster development cycles and ideal for prototyping.
Better reusability and modularity.
Course Overview:
Chapter 1: Introduction to Haskell syntax and techniques.
Chapter 2: Denotational Semantics (formal mathematical meaning of programs).
Chapter 3: Lambda Calculus (-Calculus) as the fundamental base of all functional languages.
Chapter 4: Type checking and inference (Algorithm W).
Chapter 1: Introduction to the Haskell Programming Language
Basic Structure:
A program is a sequence of Declarations.
Declarations must be left-aligned (Offside Rule).
Syntax is defined by context-free grammars.
Comments:
Block:
{- Comment -}Line:
-- Comment
Types and Function Declarations:
Type Declaration:
square :: Int -> Int.The first
Intis the domain; the second is the codomain.var1, ..., varn :: typedefines types for multiple variables.Function Declaration:
square x = x * x.Left side: Function name and parameters.
Right side: Expression defining the result.
Standard Libraries: Haskell loads "Prelude" by default, containing arithmetic operations (
+,*,-,/), Boolean types (True,False), and logic (not,&&,||).
Program Execution and Term Rewriting:
Execution involves evaluating expressions via Term Replacement.
Redex (Reducible Expression): A sub-expression matching the left side of a defining equation.
Evaluation Steps:
Find a redex.
Replace it with the right side of the equation, substituting variables.
Evaluation Strategies:
Strict (Leftmost Innermost / Call-by-Value / Eager): Evaluates the leftmost innermost redex first.
Non-Strict (Leftmost Outermost / Call-by-Name): Evaluates the leftmost outermost redex. Arguments are passed unevaluated.
Lazy Evaluation: Non-strict evaluation that uses pointers to avoid redundant calculations of the same sub-term.
Result Invariance: If any strategy terminates, the result is identical regardless of the strategy (Church-Rosser property), though termination behavior varies.
Advanced Declarations:
Conditional Equations (Guards):
maxi(x, y) | x >= y = x | otherwise = yCurrying: Named after Haskell B. Curry. Converting a function with a tuple of arguments into a sequence of functions with single arguments.
plus :: Int -> Int -> Intis equivalent toplus :: Int -> (Int -> Int).Allows Partial Application:
plus 1results in a successor function.Local Declarations:
whereblock: Follows a right side; values calculated once and shared.let ... in ...: Expression-level local binding.Offside Rule: Determined by the indent of the first symbol in a declaration block.
Infix Operators:
Operators consist of special characters. Constructor operators (like
:) start with a colon.Any prefix function can be infix using backquotes:
2 `plus` 3.Any infix operator can be prefix using parentheses:
(+) 2 3.Properties:
Associativity:
infixl(left),infixr(right), orinfix(none).Binding Priority: Integer from to ( is highest). Default is .
Chapter 1.1.2 - 1.1.4: Expressions, Patterns, and Types
Expression Types:
var(variables),constr(constructors starting with uppercase),integer,float,char('a'),string("hallo", synonymous with[Char]).Lists:
[1,2,3]is shorthand for1:(2:(3:[])).Tuples:
(10, False). Elements can have different types. Empty tuple()has type().Conditionals:
if exp1 then exp2 else exp3(requiresexp1 :: Bool).Case Distinction:
case exp of {pat1 -> exp1; ...}.Lambda Abstraction:
\pat1 ... patn -> exp. Represents anonymous functions.
Patterns:
Used for decomposing objects via matching.
Linearity: A variable can appear only once in a pattern to ensure deterministic evaluation.
Pattern Types:
Variable: Matches anything, binds it.
Joker (
_): Matches anything, no binding.Constants: Matches exact values (Int, Char, etc.).
Constructors:
(Cons x xs)matches a non-empty list.As-Patterns:
var@pat(bindsvarto the entire matched structure).Tupel/List Patterns:
(p1, p2),[p1, p2, p3].
Types & Polymorphism:
Parametric Polymorphism: Functions use type variables (e.g.,
id :: a -> a) to work regardless of concrete types.Ad-hoc Polymorphism: Overloading. The same symbol behaves differently based on type.
Type Definitions:
type Name = Typ(Type synonym/abbreviation).data Name vars = C1 t1 ... | C2 t1 ...(Algebraic data type).Example:
data List a = Nil | Cons a (List a).
Type Classes:
Sets of types that implement specific methods.
Common Classes:
Eq: Requires(==)and(/=).Ord: Requires comparison such as(<),(<=).Show: Requiresshow :: a -> String.Num: Requires arithmetics like(+),(*),(-).Contexts:
Eq a => [a] -> Boolrestricts the type variableato members of classEq.Instancing:
instance Eq Int where (==) = primEqInt.Deriving:
deriving (Eq, Show)automatically generates standard implementations.
Chapter 1.2: Higher-Order Functions
Definition: Functions that take functions as arguments or return them as results.
Function Composition (
.):Defined as:
(.) :: (b -> c) -> (a -> b) -> (a -> c).f . g = \x -> f (g x).
Curry and Uncurry:
curry :: ((a,b) -> c) -> (a -> b -> c).uncurry :: (a -> b -> c) -> ((a,b) -> c).
Standard Recursion Patterns:
Map: Applies a function to every element of a structure.
map :: (a -> b) -> [a] -> [b].map f [] = [],map f (x:xs) = f x : map f xs.ZipWith: Combines two lists using a binary function.
zipWith f (x:xs) (y:ys) = f x y : zipWith f xs ys.Filter: Removes elements that do not satisfy a predicate.
filter :: (a -> Bool) -> [a] -> [a].Fold (Reduction): Replaces constructors with functions and initial values.
foldr :: (a -> b -> b) -> b -> [a] -> b.foldr f e (x:xs) = f x (foldr f e xs).Example:
sum = foldr (+) 0.
List Comprehension:
Syntax:
[ exp | qual1, ..., qualn ].Qualifiers are generators (
var <- list) or guards (Boolean expressions).Translation:
[exp | var <- exp', Q] = concat (map (\var -> [exp | Q]) exp').[exp | guard, Q] = if guard then [exp | Q] else [].Quicksort Implementation:
qsort [] = []qsort (x:xs) = qsort [y | y <- xs, y < x] ++ [x] ++ qsort [y | y <- xs, y >= x].
Chapter 1.3 - 1.4: Lazy Evaluation and Monads
Programming with Lazy Evaluation:
Non-strictness allows for Infinite Data Objects.
from x = x : from (x+1)results in[x, x+1, ...].take ncan be used to extract finite parts from infinite structures.Sieve of Eratosthenes: Calculating infinite prime numbers by iteratively filtering multiples of the first list element.
Cyclic Objects:
ones = 1 : onescreates a self-referencing list in memory.Hamming Problem: Generating a sorted list of numbers whose prime factors are only .
Linear complexity achieved using un-evaluated list pointers and the
mer(merge) function.
The IO Monad:
To preserve Referential Transparency, side effects are encapsulated in the
IO atype.IO arepresents an action that, when executed, yields a value of typea.Primitive Actions:
putChar :: Char -> IO ()(Output).getChar :: IO Char(Input).return :: a -> IO a(Encapsulate a value in a trivial action).Combinators:
(>>) :: IO a -> IO b -> IO b(Sequencing, ignore first result).(>>=) :: IO a -> (a -> IO b) -> IO b("Bind", pass result to next action).Do-Notation: Syntactic sugar for bind chains.
do { x <- p ; q }translates top >>= \x -> q.
General Monads:
Monad Laws:
p >>= return = p(Right Identity).return x >>= f = f x(Left Identity).(p >>= f) >>= g = p >>= (\x -> f x >>= g)(Associativity).
Evaluator Example (Modularity):
Identity Monad (
Value): For simple evaluation.Maybe Monad: For error handling (division by zero results in
Nothing).State Monad (
ST): To count evaluation steps (States).Combination Monad (
STE): Handling both state and exceptions.
Chapter 2: Semantics of Functional Programs
Approach:
Denotational Semantics: Maps each expression to a mathematical object (the "meaning").
Operational Semantics: Defines meaning via the steps an abstract machine takes during evaluation.
Complete Partial Orders (CPOs):
Bottom (): Represents the "undefined" value or non-terminating calculation.
Partial Order (): Ordered by information content (degree of definition).
x \nsqsubseteq y means is less defined than or equal to .
Flat Domains: E.g., . Only is below other values.
Product Domains: iff for all .
Function Domains: iff for all .
CPO Requirements:
Smallest element exists.
For every Chain (sequence ), a Least Upper Bound (LUB) exists.
Monotonicity and Continuity:
Monotonic: . Essential for computability.
Continuous: . Boundary values match limits of approximations.
Haskell functions are interpreted as Continuous Functions over CPOs.
Fixed Point Theory:
Recursion is modeled via fixed points. .
Tarski/Kleene Fixed Point Theorem: In a CPO, a continuous function has a Least Fixed Point (LFP) defined as:
.
Domain Construction:
Lifting (): Adds a new element below an existing set/domain.
Coalesced Sum (): Union of domains sharing the same .
Chapter 3: Lambda Calculus (-Calculus)
Syntax ():
Constants: .
Variables: .
Application: .
Abstraction: .
Reduction Rules:
-Conversion: Renaming bound variables ().
-Reduction: Executing function calls ().
-Reduction: Rules for predefined constants (e.g., ).
Confluence (Church-Rosser): If a term can be reduced along different paths, those paths eventually converge. This ensures unique normal forms.
Weak Head Normal Form (WHNF):
Evaluation in Haskell stops at WHNF to allow lazy evaluation.
A term is in WHNF if it is:
A Lambda abstraction: .
A constructor application: .
A variable application: .
Fixed Point Combinators:
Used to represent recursion in the "pure" Lambda Calculus.
(Turing Combinator): .
Property: .
Chapter 4: Type Checking and Inference
Type Schemes:
Polymorphic types are quantified: .
Shallow Type Schemes: All quantifiers appear at the very beginning of the type.
Unification:
Finding a substitution such that .
Most General Unifier (MGU): The unique simplest unifier for two types.
Algorithm W (Milner):
Input: Type assumption and Term .
Logic:
Variables/Constants: Fetch from , instantiate new variables for all quantifiers.
Abstraction (): Assume ( new), infer type of , return function type.
Application (): Infer types for both, then unify results with a new variable ().
Polymorphic Recursion:
Occurs if a recursive function is called with a different type than declared.
Requires non-shallow type schemes or explicit type declarations; Haskell's inference often defaults to more specific types if declarations are missing.
Haskell Implementation Overview:
Programs are translated from complex Haskell to simple Haskell, then to Lambda-Calculus terms.
The resulting terms are type-checked using Algorithm W before execution via WHNO (Weak Head Normal Order) reduction.
Static type checking ensures that no type errors occur at runtime (Safety Property).