Principles of Programming Languages
Principles of Programming Languages
Lecture Notes
Halil Özmen
Version: 2023-12-28
This document contains a brief summary of the subjects of "Principles of Programming Languages"
course. These notes are intended to be used as a check list by the instructor who teaches this course.
It is not a replacement for the text-books or the notes taken by the students in the classroom. The
students can use these notes as a complement to their own notes and the recommended text-books.
TABLE OF CONTENTS
1. Introduction to Principles of Programming Languages .................................................................3
1.1. Computer Architecture ............................................................................................................4
1.2. History of Programming Languages ........................................................................................5
1.3. Programming Domains ...........................................................................................................6
1.4. Programming Language Categories .......................................................................................6
1.5. Language Evaluation Criteria ..................................................................................................7
1.6. Implementation Methods.........................................................................................................8
1.7. Syntax Analysis ....................................................................................................................11
1.8. Lexical Analysis ....................................................................................................................13
2. Data Types and Variables ..........................................................................................................15
2.1. Data Types ...........................................................................................................................15
2.2. Variables...............................................................................................................................16
2.3. Type Checking ......................................................................................................................17
2.4. Scope ...................................................................................................................................18
2.5. Storage Bindings and Lifetime ..............................................................................................19
2.6. Exceptions and Exception Handling ......................................................................................21
3. Subprograms, Parameter Passing and Polymorphism ...............................................................24
3.1. Subprograms and Parameter Passing ..................................................................................24
3.2. Polymorphism .......................................................................................................................26
4. Functional Programming Languages .........................................................................................28
4.1. ML ........................................................................................................................................28
4.2. Haskell ..................................................................................................................................29
5. Imperative and Object-Oriented Programming Languages.........................................................39
5.1. Imperative Programming .......................................................................................................39
5.2. Object-Oriented Programming ..............................................................................................41
6. Logic Programming Languages .................................................................................................44
6.1. Prolog ...................................................................................................................................44
A. Regular Expressions ..................................................................................................................52
B. OCaml .......................................................................................................................................54
References:
• Concepts of Programming Languages, 11th Ed., Roberta W. Sebesta
• Principles of Programming Languages, M. Archana
• CS303 Lecture Notes of Assoc. Prof. Hilal Kazan
• Various web sites and text books
Arithmetic and
Control Unit Input and Output
Logic Unit
Modules
Registers
Memory
. (stores both instructions and data)
.
.
.
.
Fetch-Execute Cycle
The execution of a machine code program on a von Neumann architecture computer occurs in a
process called fetch-execute cycle:
initialize the program counter
repeat forever
fetch the instruction pointed by the program counter
increment the program counter to point at the next instruction
decode the instruction
execute the instruction
end repeat
Readability
The ease with which programs can be read and understood.
• Overall simplicity:
A manageable set of features and constructs, minimal feature multiplicity, minimal
operator overloading.
• Orthogonality:
Orthogonality in a programming language means that a relatively small set of
primitive constructs can be combined in a relatively small number of ways to build
the control and data structures of the language. Furthermore, every possible
combination of primitives is legal and meaningful.
• Data Types: Adequate facilities for defining data types and data structures
• Syntax Design:
Special words and methods of forming compound statements
Form and meaning: self-descriptive constructs, meaningful keywords
Writability
The ease with which a language can be used to create programs.
• Simplicity and Orthogonality: Few constructs, a small number of primitives and set of rules.
To be able to design a solution to a complex problem after learning only a simple set
of primitive constructs. A smaller number of primitive constructs and a consistent set
of rules for combining them (that is, orthogonality) is much better than having a large
number of primitives.
• Support for abstraction: The ability to define and use complex structures or operations in
ways that allow details to be ignored.
E.g. use of a subprogram to implement a sort algorithm that’s needed several times
in a program.
• Expressivity: convenient ways of specifying computations
Strength and number of operators and predefined functions
E.g.: count++ rather than count = count + 1 in C
Reliability
Conformance to specifications (i.e., performs to its specification)
• Type Checking: Type checking is simply testing for type errors in a given program, either
by the compiler or during program execution. Compile-time type checking is more
desirable.
• Exception Handling: The ability of a program to intercept run-time errors (as well as other
unusual conditions detectable by the program), take corrective measures, and then
continue is an obvious aid to reliability.
• Aliasing: Presence of two or more distinct referencing methods for the same memory
location (which is dangerous)
• Readability and Writability: A language that does not support natural ways of expressing
an algorithm will require the use of unnatural approaches, and hence reduced reliability.
Input data
Compiler Code Generator
Interpreter (Translator)
Input data
Input data
Results Interpreter
(Run-time engine)
Results
Compilation Interpretation Hybrid Implementation
Pure Compilation
Advantages:
• Good runtime performance: compiles down to machine language
Disadvantages:
• Longer edit-debug cycle due to the need of full compilation
• Software distribution is harder due to platform dependence
Pure Interpretation
Advantages:
• Short edit-debug cycle: no compilation step is needed
• debugging is easy -> the error message can easily indicate the source line of the error
• Platform independence, however there should be an interpreter available for each platform
supported (not a problem for the programmer)
Disadvantages:
• Low runtime performance, statement decoding is complex and timeconsuming, the same
statement has to be decoded
• Have to distribute source code
• Needs more space, source program + symbol table is stored
e.g. Java had bytecode as its intermediate code, and JVM as its virtual machine. Most JVM imp.
provide a JIT compiler embedded into the JVM.
Regular Language:
A regular language is a language that can be expressed with a regular expression or a deterministic or
non-deterministic finite automata or a state machine.
Almost all programming languages are non-regular.
There are three reasons why lexical analysis is separated from syntax analysis:
1. Simplicity: Techniques for lexical analysis are less complex than those required for syntax
analysis, so the lexical analysis process can be simpler if it is separate. Also, removing the low-
level details of lexical analysis from the syntax analyzer makes the syntax analyzer both smaller
and less complex.
2. Efficiency: Although it pays to optimize the lexical analyzer, because lexical analysis requires a
significant portion of total compilation time, it is not fruitful to optimize the syntax analyzer.
Separation facilitates this selective optimization.
3. Portability: Because the lexical analyzer reads input program files and often includes buffering
of that input, it is somewhat platform dependent. However, the syntax analyzer can be platform
independent. It is always good to isolate machine-dependent parts of any software system.
Grammar:
A grammar is a generative device for defining languages. The sentences of the language are
generated through a sequence of applications of the rules, beginning with a special nonterminal of the
grammar called the start symbol.
Derivation: This sequence of rule applications is called a derivation.
Context-Free Grammar:
A context-free grammar is a set of recursive rules used to generate patterns of strings. A context-free
grammar can describe all regular languages and more, but they cannot describe all possible
languages. Context-free grammars are developed by Noam Chomsky (mid 1950's), and BNF is
invented by John Backus to describe ALGOL 58 (1959).
Leftmost Derivation:
The replaced nonterminal is always the leftmost nonterminal in the previous sentential form.
The derivation continues until the sentential form contains no nonterminals. That sentential form,
consisting of only terminals, or lexemes, is the generated sentence.
Parse Tree
A parse tree is an ordered, rooted tree that represents the syntactic structure of a string
according to some context-free grammar.
Ambiguous Grammars:
• If a grammar results in derivations with different parse trees for the same string, then the
grammar is ambiguous.
• Ambiguous grammars create problems in parsing, since different parsing trees result in different
semantics. E.g.
+
Precedence: 3+4*5 means 3+(4*5) since * has higher precedence than +,
3 *
thus multiplication should be deeper in the parse tree.
Associativity: 10-4-3 means (10-4)-3 since - is left-associative, 4 5
thus the first subtraction should be deeper in the parse tree.
State transition diagram or just state diagram, is a directed graph and can be processed by finite
automata.
Exercise:
What language is this? (Express solution as a regexp.)
(Examine "a lexical analyzer system for simple arithmetic expressions" written in C, on pages 190-194
in the "4.2 Lexical Analysis" section of "Concepts of Programming Languages", 11th Ed., Roberta W.
Sebesta.)
Types:
• Primitive data types: can't be decomposed into other subvalues.
• Composite data types: struct, tuples, arrays, functions
• Recursive data types: lists, trees, etc.
Name (Identifier):
• A name is a string of characters used to identify some entity in the program.
• Languages often have various restrictions on names to make scanning and parsing easier.
• Names cannot include special characters.
2.2. Variables
Attributes of variables:
• name: identifier
• address: machine memory address of the variable
• value: contents of the memory associated with the variable
• type: range of values the variable can have
• scope: range of statements where the variable is visible
Binding:
Binding is an association between a name and what it refers to.
int x; /* x is bound to a stack location containing an int */
int f (int) { ... } /* f is bound to a function */
class C { ... } /* C is bound to a class */
let x = e1;; (* x is bound to e1 *)
Type Checking
• Type checking is the activity of ensuring that the operands of an operator are of compatible
types.
• A compatible type is one that is either legal for the operator, or is allowed under language rules
to be implicitly converted, by compiler- generated code, to a legal type. This automatic
conversion is called as coercion.
• A type error is the application of an operator to an operand of an inappropriate type.
• If all type bindings are static, nearly all type checking can be static.
• If type bindings are dynamic, type checking must be dynamic.
A programming language is strongly typed if type errors are always detected.
Type Conversions
If types are the same then, there are no issues.
But if the types are different, then conversions happen.
Coercion: implicit conversions. Can happen in assignments, arithmetic operations, function calls.
Coercions are supposed to be applied only when they preserve semantics.
E.g. lengthening a short value to a longer type preserves semantics, but shortening might or
might not preserve the semantics.
e.g. in C:
int x = 2;
double y = 3.5;
y = x * y; // in x * y, x is coerced into a double
Examples: Python
def f(): def f(): def f():
print(s) s = "World" print(s)
print(s) s = "World"
s = "Hello" print(s)
f() s = "Hello"
f() s = "Hello"
print(s) f()
print(s)
Hello World UnboundLocalError: cannot
Hello access local variable 's' where it
is not associated with a value
Here, s is global, because s is s in f is local, because s is In f, s is defined but it is used
not defined (or assigned) in f. defined in f. before it is defined, s is local.
def f(): def f(): def f(x, y):
global s s = "World" global a
print(s) print(s) a = 10
s = "World" x, y = y, x
print(s) f() b = 11
print(s) print(a, b, x, y)
s = "Hello"
f() a, b, x, y = 1, 2, 3, 4
print(s) f(5, 6)
print(a, b, x, y)
Hello name 's' is not defined 10 11 6 5
World 10 2 3 4
World
s is global. The assignment to s s is local to f, it only exists in f. a is global, b, x and y are all
in f changes the global s. local.
Principles of Programming Languages 18 Halil Özmen
Example of static scope in C:
A variable is visible within the code block it is defined, and it refers to its closest binding, going
from inner to outer scope in the program text.
int i = 7;
{
int j;
{
float i = 2.4;
j = (int) i;
}
int k = i + 1;
}
Example: What values would be printed from the program on the left if:
• static scope is used?
• dynamic scope is used?
int m = 60; Static scope:
int n = 100;
function first() In main program, n is 100
{ In second, m is 60
print("In first, n is ", n); In second, n is 1
} In first, n is 100
In first, n is 100
function second(int n)
{ --------------------
print("In second, m is ", m); Dynamic scope
print("In second, n is ", n);
first(); In main program, n is 100
} In second, m is 60
In second, n is 1
print("In main program, n is ", n); In first, n is 1
second(1); In first, n is 100
first();
Static Variables:
Static variables are bound to memory cells before program execution begins and remain bound
to the same memory cells throughout execution. Storage requirements known at compile time.
Advantages:
efficiency (direct addressing), no run-time overhead for allocation & deallocation,
history-sensitive: maintain values between successive function calls.
Disadvantage:
storage cannot be shared among variables.
Stack-Dynamic Variables:
Stack-dynamic variables are those whose storage bindings are created when their declaration
statements are elaborated.
Storage is allocated & deallocated in last-in first-out order, from the run-
time stack.
Local variables, parameters, temporary variables.
Advantages:
allows recursion,
conserves storage.
Disadvantages:
Overhead of allocation and deallocation,
Subprograms cannot be history sensitive,
Inefficient references (indirect addressing).
Heap Allocation
• The heap is finite - if too much things are put into heap, then it will run out.
• Solution: deallocate space when it is no longer necessary.
• Methods:
Manual deallocation, with e.g., free, delete (C, Pascal)
Automatic deallocation via garbage collection (Java, C#, Scheme, ML, Perl)
Semi-automatic deallocation, using destructors (C++, Ada)
Automatic because the destructor is called at certain points automatically
Manual because the programmer writes the code for the destructor
Manual deallocation:
• The programmer is in charge of deciding when heap storage can be freed. (free function in C)
• Manual deallocation is dangerous. Two types of mistakes:
storage is not freed even though it is no longer needed (memory leak)
storage is freed but referred to later (dangling pointer), programmer accidentally deallocates a
block of memory that’s still in use.
Exception Handling
• In a language without exception handling, when an exception occurs, control goes to the
operating system, where a message is displayed and the program is terminated.
• In a language with exception handling, programs are allowed to trap some exceptions, thereby
providing the possibility of fixing the problem and continuing.
Detection:
• All syntax errors and some of the semantic errors (the static semantic errors) are detected by the
compiler.
• Other semantic errors ( the dynamic semantic errors) and the logical errors cannot be detected
by the compiler, and hence they are detected only when the program is executed.
Return codes in C:
• In C, the convention is that all functions return "int" values.
• A return value of 0 indicates that the function completed successfully.
• Each negative return value generally indicates a different error.
• Values that need to be returned from the function are generally managed with "pass by
reference" mechanism.
try {
// code that might throw multiple exceptions
}
catch ([Type of Exception 1] e) { // e.g. FileNotFoundException
// what to do if exception is thrown
}
catch ([Type of Exception 2] e2) { // e.g. IOException
// what to do if exception is thrown
}
finally {
// statements here always get executed, regardless of what happens in the try block.
// Can be used to close files or to release other system resources, etc...
}
try:
- The try block
except Exception1:
- Handler for Exception1
except Exception2:
- Handler for Exception2
...
else:
- The else block (no exception is raised)
finally:
- the finally block (do it no matter what)
Pass-By-Result Pass-By-Value-Result
// C#: out specifier is used to indicate begin
// pass-by-result method. integer n;
void Fixer(out int x, out int y) procedure p(k: integer);
{ begin
x = 19; n := n+1;
y = 32; k := k+4;
} print(n);
end;
void Main(string[] args) n := 0;
{ p(n);
int a = 4, b = 7; print(n);
Fixer(a, b); end;
Console.WriteLine(a);
Console.WriteLine(b);
}
19 By-Value By-Reference By-Value-Result
32 1 5 1
1 5 4
3.2. Polymorphism
Definitions of Polymorphism:
In programming language theory, polymorphism is the provision of a single interface to entities of
different types or the use of a single symbol to represent multiple different types.
Polymorphism refers to the ability that a function or data structure can accomodate data of different
types.
Operator Overloading
Treat operators as functions
Behaviour different depending on operand type
Example in Java:
1+2 // integer addition
2.7 + 3.14159 // double (float) addition
"Hello " + "world" // string concatenation
int main() {
cout << "maximum(4, 7) -> " << maximum(4, 7) << endl;
cout << "maximum(8.4, 3.14) -> " << maximum(8.4, 3.14) << endl;
cout << "maximum('K', 'T') -> " << maximum('K', 'T') << endl;
return 0;
}
maximum(4, 7) -> 7
maximum(8.4, 3.14) -> 8.4
maximum('K', 'T') -> T
The type variable T defined in the scope of maximum is a kind of generics, which will be
substituted at the function call. The function takes two parameters (a and b) of type T and
returns a value of type T.
4.1. ML
ML (MetaLanguage) was originally designed in the 1980s by Robin Milner at the University of
Edinburgh as a metalanguage for a program verification system named Logic for Computable
Functions (LCF).
ML is primarily a functional language, but it also supports imperative programming. Unlike Lisp and
Scheme, the type of every variable and expression in ML can be determined at compile time. Types
are associated with objects rather than names. Types of names and expressions are inferred from
their context.
Unlike Lisp and Scheme, ML does not use the parenthesized functional syntax that originated with
lambda expressions. Rather, the syntax of ML resembles that of the imperative languages, such as
Java and C++.
Miranda was developed by David Turner in the early 1980s. Miranda is based partly on the languages
ML, SASL, and KRC.
Haskell is a purely functional language, having no variables and no assignment statement, and is
based in large part on Miranda. Another distinguishing characteristic of Haskell is its use of lazy
evaluation, which means that no expression is evaluated until its value is required.
Caml and its dialect that supports object-oriented programming OCaml, descended from ML and
Haskell. Finally, F# is a relatively new typed language based directly on OCaml. F# is a .NET
language with direct access to the whole .NET library.
In ML:
• Everything is an expression
• Everything evaluates to a value
• Everything has a type
Haskell is:
• Functional
Functions are first-class, that is, functions are values which can be
used in exactly the same ways as any other sort of value.
Haskell programs are centered around evaluating expressions
rather than executing instructions.
• Pure
Haskell expressions are always referentially transparent, that is:
No mutation! Everything (variables, data structures, …) is immutable.
Expressions never have “side effects” (like updating global variables or printing to the
screen).
Calling the same function with the same arguments results in the same output every time.
• Lazy Evaluation
In Haskell, expressions are not evaluated until their results are actually needed. This is a simple
decision with far-reaching consequences. Some of the consequences include:
It is easy to define a new control structure just by defining a function.
It is possible to define and work with infinite data structures.
It enables a more compositional programming style.
• Statically typed
Every Haskell expression has a type, and types are all checked at compile-time.
Programs with type errors will not even compile, much less run.
Haskell Basics:
• Basic Syntax
A Haskell program consists of function definitions followed by main body.
Do not use tab characters anywhere in the program except inside comments.
func1 ... = ... --\
... -- > function definitions
funcn ... = ... --/
main = do Main program. "do" starts a block of statements
stmt_1 --\
... -- > indentation must be equal in all statements
stmt_n --/
• Comments:
The characters "--" followed by any sequence of characters up to end of line.
The symbol "{-" followed by any sequence of characters (including new lines) up to "-}".
• Declaring Values
let keyword is used to declare values:
let n = 42
let m = n + 6
let msg = "Result= " ++ show m
Haskell show function converts to string: show 48 evaluates to "48"
Functions:
Function returns the value of the expression written after the equal sign (=):
funcname arg1 arg2 ... argn = <expr>
Examples:
double x = x + x
inc x = x + 1
in_range x min max = x >= min && x <= max
double 7 -- 14
inc 7 -- 8
in_range 4 2 7 -- True
in_range 8 2 7 -- False
Sometimes it is necessary to first define type of function and types of its parameters.
Last type is the type of the function, previous ones are types of parameters.
stringToInt :: String -> Int -- function type definition
stringToInt s = read s -- followed by function
stringToDouble :: String -> Double -- function type definition
stringToDouble s = read s -- followed by function
quadratic :: Double -> Double -> Double -> Double -> Double
quadratic x a b c = a * (x ** 2) + b * x + c
Calls of these functions:
let a = stringToInt "771" + 1 -- a will be 772
print(stringToDouble "7.71" + 3) -- 10.71
let y = quadratic 2 2 (-3) 5 -- y will be 7.0
Build-in Functions: (this is not the complete list of built-in Haskell functions)
Function Description Example
length Length of a string or list length "abcd" -- 4
mod Returns modulus of two integers mod 48 10 -- 8
div Integer division of two integers div 48 10 -- 4
even Returns True if parameter is even even 42 -- True
odd Returns True if parameter is odd odd 42 -- False
sqrt Square-root, returns double sqrt 16 -- 4.0
max Returns maximum of two values max 4 3 -- 4
min Returns minimum of two values min "good" "hi" -- "good"
gcd Returns the greatest common divisor. gcd 20 48 -- 4
lcm Returns the lowest common multiple. lcm 20 48 -- 240
head Returns the head (first element) of a list head [4,5,6,7,8] -- 4
tail Returns the tail (all except head) of a list tail [4..8] -- [5,6,7,8]
elem Element of list: True or False elem 42 [1..40] -- False
take Returns a list by taking the first n elements take 4 [8..20] -- [8,9,10,11]
Currying:
When a function has multiple arguments, the function consumes one argument at a time. This is
called currying the function.
Curried vs Uncurried:
mult3 a b c = a * b * c -- Curried Function
mult3u (a, b, c) = a * b * c -- Uncurried Function
main = do
let a = mult3 3 4 5 -- 60
let b = mult3u (3, 4, 5) -- 60
Data Types:
name :: <type> is read as: "name is of type <type>"
i :: Int
Int Machine-sized integers
let i = -78
n :: Integer
Integer Arbitrary-precision integers
let n = 12345678909876543210987340828798724
squares :: Float -> Float -> Float
Single precision floating point
Float squares x y = x*x + y*y
number
main = print (squares 2 3.8)
squares :: Double -> Double -> Double
Double precision floating point
Double squares x y = x*x + y*y
number
main = print (squares 2 3.8)
bignum n = n >= 1000000
Boolean.
Bool bignum 777
Either True or False.
False
Character :t 'a'
Char
delimited by single-quotes 'a' :: Char
String of characters
String delimited by double-quotes. length "Galaxy" -- 6
length function gives size. ['a', 'x', 'e'] is equivalent to "axe"
Principles of Programming Languages 31 Halil Özmen
Type Conversions:
Conversion from Integer to Int: fromInteger
let age = fromInteger year - byear -- Convert year from Integer to necessary type
Conversion to string: function show converts data of any type to string
let ns = show 44 -- "44"
let xs = show 3.14159 -- "3.14159"
let lst = [4, 3, 2, 4]
let lststr = show lst -- "[4,3,2,4]"
Conversion from string to integer or double: write functions based on read and specify types.
stringToInt :: String -> Int
stringToInt s = read s -- stringToInt is defined under read function
stringToDouble :: String -> Double
stringToDouble s = read s -- stringToDouble is defined under read function
let a = stringToInt "778" -- 778
let x = stringToDouble "24.7048" -- 24.7048
Data Structures:
• Lists
[e1, e2, ..., en]
All elements of a list must be of the same type.
let a = [1, 2, 1, 4]
let b = 7 : 4 : 2 : 8 : []
let p = 7 : 4 : [2, 8, 7]
let q = [] : 7 : 4 : 2 : 8 : 6 : 1 -- ERROR
let c = 8 : 2 : [1, 2, 3]
let s = ["A4", "Galaxy", "abc", "777"]
-- Usage of range operator "..":
let b = [-4..4] -- [-4,-3,-2,-1,0,1,2,3,4]
-- List comprehension:
let c = [x*x | x <- [4..10]] -- [16,25,36,49,64,81,100]
length function gives the number of elements in a list: let alen = length [...]
let a = [1, 2, 1, 4, 8, 7]
let alen = length a -- alen will be 6
Indexing: Indexes start with 0. Index operator is !!.
let a = [1, 2, 1, 4, 8, 7]
let n = a !! 3 -- n will be 4
Appending two lists: ++ operator
let aa = [...] ++ [...]
Appending an element to a list: (1) create a list containing element, (2) append two lists.
let list1 = [...]
let n = 4
let list2 = list1 ++ [n] -- append to end
let list3 = [n] ++ list1 -- append to beginning
Functions can be used to append an element to a list:
-- Append an element to the end of list:
appendEnd v lst = lst ++ [v]
-- Append an element to the beginning of list:
appendBegin v lst = [v] ++ lst
main do =
let list1 = [2, 7, 8, 2, 5, 6]
let n = 4
let list2 = appendEnd n list1 -- append to end
let list3 = appendBegin n list1 -- append to beginning
Pattern Matching
Pattern matching applies to values. It is used to sum1n n =
recognize the form of this value and lets the if n == 0 then 0
computation be guided accordingly, associating else n + sum1n (n-1)
with each pattern an expression to compute. sum1nv2 n
If the function has single argument, then pattern | n == 0 = 0
matching can be done as follows, using logical | otherwise = n + sum1nv2 (n-1)
expressions after |. Logical expressions must sum1nv3 n = case n of
cover all possible cases (must be exhaustive). 0 -> 0
sign x | x > 0 = 1 _ -> n + sum1nv3 (n-1)
| x == 0 = 0
| x < 0 = -1
If the function has one or more arguments, then pattern matching can also be done using case:
f123 x s = case x of f123a x s
1 -> "one " ++ s | x == 1 = "one " ++ s
2 -> "two " ++ s | x == 2 = "two " ++ s
3 -> "three " ++ s | x == 3 = "three " ++ s
_ -> "other " ++ s | otherwise = "other " ++ s
count [] _ = 0
count (x:xs) v
| x == v = 1 + count xs v
count lst v = case lst of | otherwise = count xs v
[] -> 0
x : xs -> count xs v + if x == v then 1 else 0
Define a Haskell function isEmpty that gets a list as argument and evaluates to True if the given
list is empty, and to False if the list is not empty. Do not use if, use pattern matching.
isEmpty lst = case lst of isEmpty [] True
[] -> True isEmpty ["xyz"] False
_ : _ -> False isEmpty [2..8] False
Define a Haskell function equal1st2nd that gets a list as argument and evaluates to true if the
first two elements of the list are equal, and false in not equal. If the list has less than two
elements, the function will evaluate to false.
equal1st2nd lst = case lst of
[] -> False
[x] -> False
x : y : _ -> if x == y then True else False
main = do
equal1st2nd [4] -- False
equal1st2nd [4, 2, 7, 2, 1, 8, 2, 5] -- False
equal1st2nd [4, 4] -- True
equal1st2nd [4, 4, 7, 2, 1, 8, 4, 5] -- True
equal1st2nd ["at", "at", "www", "in", "a", "the"] -- True
Forward Recursion
In forward recursion, the function recursively first calls on all recursive components, and then
builds the final result from the partial results.
I.e.: Wait until the whole structure has been traversed (recursively) to start building the answer.
sum1n n = sum1n 3
if n == 0 then 0 3 + sum1n 2
else n + sum1n(n-1) 2 + sum1n 1
main = do 1 + sum1n 0
let a = sum1n 3 0
print(a) 1 + 0
2+1
Call Stacks 3+3
6
While a program runs, there is a call stack of function calls that have
started but not yet returned.
• Calling a function f pushes an instance of f on the stack (with the return point in the
program),
• When a call to f finishes, it is popped from the stack.
These stack-frames store information such as the value of local variables and "what is left to do"
in the function.
Due to recursion, multiple stack-frames may be calls to the same function.
naiveSumList lst =
if (lst == []) then 0
else (head lst) + naiveSumList (tail lst)
Tail Recursion
• A recursive function is tail-recursive if all recursive calls are the last thing that the function does.
• Tail recursion generally requires extra "accumulator" arguments to pass partial results.
• May require an auxiliary function!
• The general idea is to write your recursive function such that the value returned by the recursive
call is what’s returned by your function.
• i.e., there’s no pending operation in the function waiting for the value returned by the
recursive call.
• That way, the function can say, ”Don’t bother with me anymore, just take the answer from my
recursive call as the result. You can just forget all of my state information.”
Why do we care?
Reusing the stack frame of the tail-recursive function is known as the tail call optimization.
It is an automatic optimization applied by the compilers and interpreters.
Experiment:
Write a function that takes a list, and returns the sum of the elements of this list.
sumList lst =
if lst == [] then 0
else (head lst) + sumList (tail lst)
main = do
let a = [1..100000000]
let suma = sumList a
print(suma)
ERRORS: (https://play.haskell.org/)
Main: Heap exhausted;
Main: Current maximum heap size ....
sumListTail lst acc = -- This is tail recursive!
if lst == [] then acc
else sumListTail (tail lst) (acc + (head lst))
main = do
let a = [1..100000000]
let suma = sumListTail a 0
print(suma)
Output:
5000000050000000
So, there really is a difference between tail vs. forward recursion.
Node:
Node is a recursive constructor. It has a value of type a that it receives as a parameter on
construction. The second parameter to Node is of type Seq a which is the type of Node itself,
thus it becomes a recursive data structure.
Write a function treeSum that evaluates to the sum of all values in a binary tree.
treeSum t = case t of
Nil -> 0
Node left v right -> v + treeSum left + treeSum right
Principles of Programming Languages 36 Halil Özmen
Write a function mirror that mirrors a binary tree (reverses left and right).
mirror t = case t of
Nil -> Nil
Node left v right -> Node (mirror right) v (mirror left)
Write a function contains that evaluates to true if a binary tree contains a given value.
contains t x = case t of
Nil -> False
Node left v right ->
if x == v then True else contains left x || contains right x
main = do
let t1 = Node (Node Nil 2 Nil) 4 (Node (Node Nil 6 Nil) 7 (Node Nil 8 Nil))
print(contains t1 6) -- True
print(contains t1 7) -- True
print(contains t1 5) -- False
Print Functions:
putStrLn "Hello Galaxy!"
putStrLn Prints a string then a newline
putStrLn ("Hello " ++ "Galaxy!")
putStr Prints a string, but not a newline putStr "Hello Galaxy!"
putChar Prints a character
print Prints its argument print a print(x, y, z)
While using these print functions, strings and non-string data can be printed together by
converting non-string data to string with "show" function.
Program Output
main = do Hello Galaxy!
putStrLn "Hello Galaxy!" "Hello Galaxy!"
print "Hello Galaxy!" 2
let a = 2 (2,"abba",2.71828)
print a Good work2.71828
let b = "abba" =========
let c = 2.71828 a=2, abba, e=2.71828
print (a, b, c)
putStr "Good work"
print c
putStrLn "========="
putStrLn ("a=" ++ show a ++ ", " ++ b ++ ", e=" ++ show c)
Input Functions
These functions read input from the standard input device (normally the user’s keyboard).
readLn :: Read a => IO a
getLine :: IO String
getChar :: IO Char
Major imperative programming languages: Fortran, Algol, Cobol, Pascal, C, Basic, Python, PHP, ...
OOP languages are diverse, but the most popular ones are class-based, meaning that objects are
instances of classes, which also determine their types. In these languages, computer programs are
designed by defining classes from which instances of objects are created.
Many of the OOP languages (such as C++, Java, Python, etc.) are multi-paradigm and they support
object-oriented programming to a greater or lesser degree, typically in combination with imperative
and procedural programming.
Significant object-oriented languages: Simula, Smalltalk, C++, Java, C#, JavaScript, Objective-C,
Object Pascal, Perl, PHP, Python, Visual Basic.NET.
Advantages of OO Programming:
• It reduces conceptual load:
It reduces the amount of detail the programmer must think about.
• It provides fault and change containment:
It limits the portion of a program that needs to be looked at when debugging.
It limits the portion of a program that needs to be changed when changing the behaviour of
an object without changing its interface.
• It provides independence of program components and thus facilitates code reuse.
Syntax of Inheritance:
• C++
class push_button : public widget { ... }
class DrawThread : public Thread, public Drawing { ... } // Multiple inheritance
• Java
public class push_button extends widget { ... }
• Python
class push_button(widget):
...
Dynamic Binding:
Visibility in C++:
Three visibility levels:
• Private methods/fields are visible to members of objects of the same class and to friends.
• Protected methods/fields are visible to members of objects of the same class or derived classes
and to friends.
• Public methods/fields are visible to the whole world.
Friends: A class can declare other classes and functions to be its friends, thereby providing them with
access to its private and protected members.
Any program written in a logic programming language is a set of sentences in logical form, expressing
facts and rules about some problem domain.
If H is head and B1, B2, B3, ... are the elements of the body, then a rule is written as
H :- B1, B2, …, Bn. which means: "H is true, when B1, B2, ..., Bn all are true".
The rules are written in the form of logical clauses, where head and body are present.
On the other hand, facts are like the rules, but without any body. So, an example of fact is:
H. which means "H is true".
6.1. Prolog
Prolog, which stands for PROgramming in LOGic, is a logical and declarative
programming language.
Prolog has its roots in first-order logic or first-order predicate calculus. The language
was conceived in Marseilles, France in the early 1970s by a group led by Alain Colmerauer.
A Prolog program consists of data which is based on the facts and rules (logical relationships), rather
than computing how to find a solution.
A logical relationship describes the relationships which hold for the given application (or problem).
Basics of Prolog:
• Knowledge base: The knowledge base (or database) is a collection of facts and rules.
A Prolog program is a knowledge base.
• Facts: The fact is a predicate that is true. A fact declares something to be true.
E.g. if we say, "Jane is female" or “Jane is parent of Tom”, then these are facts.
female(jane). male(tom).
parent(jane, tom). likes(tom, pizza).
• Rules: A rule states conditions for something to be true. Rules are extentions of facts that
contain conditional clauses. To satisfy a rule all conditions should be met (true).
mother(X, Y) :- female(X), parent(X, Y).
grandmother(X, Y) :- female(X), parent(X, Z), parent(Z, Y).
This implies that for X to be the grandmother of Y, X should be female and X should be parent of
Z and Z should be a parent of Y.
• Questions (queries): To run a prolog program, some questions are needed, and those
questions can be answered by the given facts and rules. The answer is yes / true or no / false.
?- grandmother(jane, ann). % is Jane grandmother of Ann?
Variables:
A variable in Prolog is a string of letters, digits, and underscores (_) beginning either with a
capital letter or with an underscore. Examples: X , First_name , Z2, Jane, _ , _the , _k48.
The variable _ is called the anonymous variable, used as a "don't-care" variable, when the
value isn't important (anything). Every occurence of _ represents a different variable.
Atoms: An atom is either:
A string of characters beginning with a lower-case letter and made up of letters, digits,
and the underscore character. E.g.: jane, goodWork, c3p0, adana_kebap.
An arbitrary sequence of characters enclosed in single quotes. E.g.: ’the’, ’Hello World’,
’2 + 2 = 4’, ' ', ’&^%&#@$ &* ’.
A string of special characters. Examples: @= ====> ; :- are all atoms. Some
atoms, such as ; and :- have a pre-defined meaning.
Functor: The word functor is used to refer to the atom at the start of a structure.
E.g.: In likes(mary, pizza) , likes is the functor.
In Prolog, predicates (functors) are not functions, and they do not
return values, they evaluate to either true or false. The values are
obtained from the parameters of functors.
Numbers: integers and floating point numbers.
Integers: …, -2, -1, 0, 1, 2, 3, …
Floating point numbers are not particularly important in typical Prolog applications.
Operators:
is: forces evaluation of arithmetic expressions. However, = do only symbolic assignment.
?- X is 2 + 4. Then: X = 6. ?- X = 2 + 4. X = 2+4
Arithmetic Operators: + - * / ** (power) // (int div) mod (modulus) sqrt max
?- A is (2 ** 8 - 12) mod 100. % Result: A 44
Identicality Operators: == \== Eg: jane \== ann (true) 4 == 2+2. (false)
Comparison Operators (numeric): =:= =\= < =< > >= Eg: 4 =:= 2+2. (true)
Comparison Operators (string): @< @=< @> @>= Eg: 'aa' @> 'ZZZZ'. (true)
Logical Operators: (see Symbols above) , (and) ; (or) not
Precedence of some operators: (0 to 1200; 0 has highest precedence.)
200 xfx **
400 yfx * / // div mod rem
500 yfx + -
700 xfx < = =.. =@= \=@= =:= =< == =\= > >= @< @=< @> @>= \= \== is
1000 xfy ,
1100 xfy ;
(See: https://www.swi-prolog.org/pldoc/man?section=operators)
Backtracking:
If an attempt to satisfy a goal fails, or if we ask for more answers, Prolog tries to re-satisfy the
goal by backtracking, as follows:
1. Move back along the search path to where the unifying clause was found.
2. Un-instantiate any variables that have been instantiated during the search process.
3. Start another search for another unifying clause from where the current unifying clause was
found (the place marked in the database).
Recursion in Prolog:
Prolog rules can be recursive.
Example:
descendant_of(X, Y) :- child_of(X, Y). % Base case
descendant_of(X, Y) :- child_of(X, Z), descendant_of(Z, Y). % Recursive case
The base case must always appear first!
Queries:
?- mother(jane, paul). ?- parent(paul, X).
?- mother(paul, ann). ?- sister(ann, X).
?- father(paul, ann). ?- sister(mary, X).
?- sister(ann, peter). ?- sister(x, peter).
?- brother(paul, mary). ?- grandmother(jane, X).
?- brother(tom, jane). ?- grandmother(X, peter).
?- grandmother(jane, ann). ?- grandmother(X, Y).
?- grandmother(tom, ann).
Trace: The "trace" command sets the debugger on, i.e. the intermediate steps are shown by the
Prolog compiler. To switch the debugger off, run "notrace" command.
?- trace.
The debugger will first creep -- showing everything (trace)
?- grandmother(jane, X).
1 1 Call: grandmother(jane,_23) ? Press Enter
2 2 Call: female(jane) ? here
2 2 Exit: female(jane) ?
3 2 Call: parent(jane,_116) ?
3 2 Exit: parent(jane,paul) ?
4 2 Call: parent(paul,_23) ?
4 2 Exit: parent(paul,ann) ?
1 1 Exit: grandmother(jane,ann) ?
a for all solutions
X = ann ? a
1 1 Redo: grandmother(jane,ann) ? ; for next solution
4 2 Redo: parent(paul,ann) ?
4 2 Exit: parent(paul,peter) ?
1 1 Exit: grandmother(jane,peter) ?
X = peter
yes
{trace}
More Exercises:
• Write a Prolog program to compute factorial. ?- factorial(4, F). → F = 24
Lists:
Enclosed in square brackets, elements separated by comma (,).
[e1, e2, ... , en] % An n-element list in Prolog
[] % Empty list
[7, 4, p, 12, q, r, 2, 8]. % List may contain mixed types.
The head and tail of a list: The head is the first element and all the rest is tail.
The vertical bar (|) separates the head and tail parts.
?- [p,q,r,s] = [Head | Tail].
Head = p
Tail = [q,r,s]
[red, green, blue, purple, white] % can be written as:
[red | [green, blue, purple, white]]
[red | REST] % REST becomes [green, blue, purple, white]
Nested lists may be created: [a, b, [c, d, e], f, [g, h]]
List Unification: two lists unify iff they have the same structure and the corresponding elements unify.
?- [jane, likes, fish] = [P,Q,R].
P = jane Q = likes R = fish
?- [a, b, c] = [X | Y]. % X is a, and Y is [b,c]
Prolog Built-in List Predicates (Functions): (this is not the complete list)
length(L, N) Number of elements of a list length([2,4,6,7,8], N). %5
member(X, L) True if X is a member of list L. member(4, [2,4,8,6,7]) % true
last(L, X) True if X is the last element of L last([2,4,8,6,7], 4) % false
last([2,4,8,6,7], X) %X=7
nth0(N, L, X) True if X is N'th element of L (index nth0(3, [2,4,8,6,7], X). %X=6
starting from 0)
nth1(N, L, X) True if X is N'th element of L (index nth1(3, [2,4,8,6,7], X). %X=8
starting from 1)
append(L1, L2, L3) Append two lists to obtain a new list. append([7,4,8,2], [a,[b,8],4], L3).
L3 = [7,4,8,2,a,[b,8],4]
reverse(L1, L2) Reverses a list reverse([2,4,7,8,4], L2).
L2 = [4,8,7,4,2]
flatten(L1, L2) Flatten a nested list flatten([a, [b, c], d, e, [f, g, h]], L2).
L2 = [a,b,c,d,e,f,g,h]
numlist(A, B, List) Make a list from numbers starting numlist(4, 8, L).
with A to B (inclusive). L = [4, 5, 6, 7, 8]
Principles of Programming Languages 49 Halil Özmen
sum_list(List, Sum) Sum of all numbers in list of sum_list([7,4,2,8,4], S).
numbers S = 25
max_list(List, Max) Largest number in a list of numbers. max_list([7,4,2,8,4], M).
Fails if list is empty. M=8
min_list(List, Min) Smallest number in a list of min_list([7,4,2,8,4], M).
numbers. Fails if list is empty. M=2
bagof(T, G, L) Binds L to the list of all instances of stu(jane). stu(tom). stu(ann).
term T satisfying the goal G. students(L) :- bagof(S, stu(S), L).
% L = [jane, tom, ann]
setof(T, G, L) Binds L to the sorted list of all stu(jane). stu(tom). stu(ann).
unique instances of term T satisfying students(L) :- setof(S, stu(S), L).
the goal G. % L = [ann, jane, tom]
findall(T, G, L) Similar to bagof, used when the goal stuGradesW(Stu, Crs, GrWList) :-
is complex (with 2 or more findall((Gr, W), (exam(Crs, Exam,
predicates) W), grade(Stu, Crs, Exam, Gr)),
GrWList).
• Append a list L2 at the end of another list L1 and put the resultant list in L3.
% If L1 is empty, resultant list will be equal to L2 (base case).
append_list([], L2, L2).
append_list([H | T], L2, [H | L3]) :- append_list(T, L2, L3).
A query for this function:
?- append_list([1,2,3,4], [8,7,2], L). % Result: L [1, 2, 3, 4, 8, 7, 2]
Another query for this function:
?- append_list(L1, L2, [a,b,c]).
L1 = []
L2 = [a,b,c] ? a a for all solutions
L1 = [a] ; for next solution
L2 = [b,c]
Principles of Programming Languages 50 Halil Özmen
L1 = [a,b]
L2 = [c]
L1 = [a,b,c]
L2 = []
• Write function getNth to get the N'th element of a list, assuming indexes start from 1.
getNth([H | _], 1, H). % Base case.
getNth([_ | T], N, X) :- N > 1, N1 is N - 1, getNth(T, N1, X).
Queries for this function:
getNth([2,4,6,7,8], 1, X). % X = 2
getNth([2,4,6,7,8], 4, X). % X = 7
getNth([2,4,6,7,8], 6, X). % no
Exercises:
• Write a Prolog program to find the last element of a list. ?- last_list([a,b,c,d], X). → X = d
• Write a Prolog program to duplicate all elements of a list. ?- dupli([a,b], L2). L2 = [a,a,b,b]
• Write a Prolog program to check iff a list is subset of another list. (Hint: Use member function.)
?- subset(L1, L2). will be true iff all elements of L1 exist in L2.
• Write a Prolog program to check iff two lists are disjoint (i.e. they have no common elements).
(Hint: Use member function.)
?- disjoint(L1, L2). will be true iff there is no common element in L1 and L2.
Set of strings that can be defined in terms of regular expressions are called
"regular languages".
Exercise:
Write a regular expression that defines the language of all decimal numbers like 3.14 -0 4722 +2.75 ...
But not numbers lacking integer part, and not numbers with a decimal point but lacking fractional part.
So, not numbers like 47. .274 . Leading and trailing zeros are allowed: 007 0.0 008.00 2.700
Basics:
Everything has a type and evaluates to a value.
OCaml definitions (statements) ends with double semi-colon ";;". let e = 2.718281;;
Comments: (* .... *)
Documentation comments: (** .... *)
Toplevel:
The toplevel is like a calculator or command-line interface to OCaml.
The toplevel is handy for trying out small pieces of code without going to the trouble of launching
the OCaml compiler. The toplevel can be called REPL, which stands for read-eval-print-loop: it
reads programmer input, evaluates it, prints the result, and then repeats.
In a terminal window, type utop to start the toplevel. Press Control-D (or enter command
"#quit;;") to exit the toplevel.
Creating, compiling, and testing large programs will require more powerful tools.
Online OCaml toplevel: https://try.ocamlpro.com/
Online OCaml IDE: https://www.tutorialspoint.com/compile_ocaml_online.php
https://coderpad.io/languages/ocaml/
https://ocaml.org/play (difficult to clear output window)
Declaration: let
Declaration is used to "bind" a value to a name. The association of a name with a value is a
"binding". Declarations are made using the let keyword. After a declaration is made, the bound
name can be used when declaring other names and in subsequent expressions.
The word "name" is deliberately used, not "variable". This is because in OCaml, once bound, the
value of a name cannot be changed.
let n = 100;; (* val n : int = 100 *)
let pi = 3.1415926535;; (* val pi : float = 3.1415926535 *)
let city = "Antalya";; (* val city : string = "Antalya" *)
let em = '!';; (* val em : char = '!' *)
if
Syntax: if e1 then e2 else e3
OCaml if is similar to (e1 ? e2 : e3) in C. If e1 is true, then it evaluates to e2, otherwise to e3.
let a = 4;; (* val a : int = 4 *)
let b = a * 2 - 7;; (* val b : int = 1 *)
if a > b then 10 else 20;; (* - : int = 10 *)
if not(a > b) then 10 else 20;; (* - : int = 20 *)
let c = if a > b then 10 else 20;; (* val c : int = 10 *)
let d = if a >= b then a else b;; (* val d : int = 4 *)
Type Conversions
Conversion Function Example Toplevel output
let x = 8.7;; val x : float = 8.7
float to int int_of_float
let n = int_of_float x;; val n : int = 8
let k = -7;; val k : int = -7
int to float float_of_int
let x = float_of_int k;; val x : float = -7.
let z = "-124";; val z : string = "-124"
string to int int_of_string
let n = int_of_string z;; val n : int = -124
let spi = "3.14159";; val spi : string = "3.14159"
string to float float_of_string
let pi = float_of_string spi;; val pi : float = 3.14159
let n = 360;; val n : int = 360
int to string string_of_int
let sn = string_of_int n;; val sn : string = "360"
let e = -2.718281;; val e : float = -2.718281
float to string string_of_float
let se = string_of_float e;; val se : string = "-2.718281"
let c = 'E';; val c : char = 'E
char to string String.make 1 ch
let sc = String.make 1 c;; val sc : string = "E"
Scope
Variable declarations in OCaml bind variables within a scope, the part of the program where the
variable stands for the value it is bound to. For example, when we write let x = e1 in e2,
the scope of the identifier x is the expression e2. Within that scope, the identifier x stands for
whatever value v the expression e1 evaluated to. Since x = v, OCaml evaluates the let
expression by rewriting it to e2, but with the value v substituted for the occurrences of x. For
example, the expression let x = 2 in x + 3 is evaluated to 2 + 3, and then the result value is 5.
Tuples
Tuples are a sequence of values that may be of different types, and they are separated by
commas. They may be enclosed in parenthesis.
4, 8, 7;; (* - : int * int * int = (4, 8, 7) *)
(4, 8, 7);; (* - : int * int * int = (4, 8, 7) *)
Principles of Programming Languages 55 Halil Özmen
4, 7.2, "good";; (* - : int * float * string = (4, 7.2, "good") *)
(4, 7.2, "good");; (* - : int * float * string = (4, 7.2, "good") *)
(4, 2.8), "nice";; (* - : (int * float) * string = ((4, 2.8), "nice") *)
((4, 2.8), "nice");; (* - : (int * float) * string = ((4, 2.8), "nice") *)
Functions
A function definition has the following form:
let f x = .... where f is the function name and x is its argument.
let f x y z = .... where f is the function name and x, y and z are arguments.
Currying:
When a function has multiple arguments, the function consumes one argument at a time. This is
called currying the function.
let mult a b = a * b;;
val mult : int -> int -> int = <fun>
let mult a = (fun b -> a * b);;
val mult : int -> int -> int = <fun>
mult 4;;
- : int -> int = <fun>
(mult 4) 3;;
- : int = 12
Principles of Programming Languages 56 Halil Özmen
mult 4 3;;
- : int = 12
Curried vs Uncurried:
let addThree a b c = a + b + c;; (* Curried *)
val addThree : int -> int -> int -> int = <fun>
addThree 4 7 5;;
- : int = 16
Functions as Arguments:
Functions can be arguments of other functions.
let add2 n = n + 2;;
val add2 : int -> int = <fun>
let thrice f x = f(f(f(x)));;
val thrice : ('a -> 'a) -> 'a -> 'a = <fun>
let thrice f x = f(f(f x));;
thrice add2 4;;
- : int = 10 (* What is 'a in this case? *)
thrice (fun s -> "Hi! " ^ s) "Jane";;
- : string = "Hi! Hi! Hi! Jane" (* What is 'a in this case? *)
Function Exercises:
Define an OCamle function halfF that gets an int and evaluates to the double of its argument.
let halfF n = float_of_int n /. 2.;;
val halfF : int -> float = <fun>
halfF 7;;
- : float = 3.5
Define an OCamle function squareF that gets a float and evaluates to the square of its
argument.
let squareF x = x *. x;;
val squareF : float -> float = <fun>
squareF 2.5;;
- : float = 6.25
Define an OCamle function max that gets two arguments (of any type), and evaluates to the
maximum of its arguments.
let max x y = if x >= y then x else y;;
val max : 'a -> 'a -> 'a = <fun>
max 4 7;;
- : int = 7
max 44.8 24.8;;
- : float = 44.8
max "Tale" "abcd";;
- : string = "abcd" (* Because 'A' ... 'Z' < 'a' ... 'z' *)
Principles of Programming Languages 57 Halil Özmen
Remark: OCaml has already built-in functions max and min that gets two arguments and
evaluates to the maximum and minimum of their two arguments.
Define an OCaml function revpair that takes a pair (tuple of two elements) and returns its
reverse.
let revpair p =
let (a, b) = p
in (b, a);;
val revpair : 'a * 'b -> 'b * 'a = <fun>
revpair (4, 7);; (* - : int * int = (7, 4) *)
revpair ("Hi", 7.4);; (* - : float * string = (7.4, "Hi") *)
Lists
An OCaml list is an ordered sequence of values all of which have the same type. They are
implemented as singly-linked lists.
[] Empty list.
[e1; e2; ...; en] Lists elements are written in [] and separated by semi-colons.
e1 :: e2 :: ... :: en :: [ ] e1, e2, ..., en are elements.
e1 is the head element and e2 is the tail (the rest).
e1 :: e2 or h :: t
Usually used in pattern matching.
Define an OCaml function isEmpty that gets a list as argument and evaluates to true if the given
list is empty, and to false if the list is not empty.
let isEmpty lst =
match lst with
| [] -> true
| _ :: _ -> false;;
val isEmpty : 'a list -> bool = <fun>
(* or: *)
let isEmpty = function
| [] -> true
| _ :: _ -> false;;
val isEmpty : 'a list -> bool = <fun>
isEmpty [];;
- : bool = true
isEmpty [7];;
- : bool = false
isEmpty [7; 4; 8; 2];;
- : bool = false
Define an OCaml function equal1st2nd that gets a list as argument and evaluates to true if the
first two elements of the list are equal, and false in not equal. If the list has less than two
elements, the function will evaluate to false.
let equal1st2nd lst =
match lst with
| [] -> false
| [x] -> false
| x :: y :: _ -> if x = y then true else false;;
val equal1st2nd : 'a list -> bool = <fun>
equal1st2nd [];;
- : bool = false
equal1st2nd [7];;
- : bool = false
equal1st2nd ["a"; "the"; "a"];;
- : bool = false
equal1st2nd [2.4; 2.4; 4.; 7.8];;
- : bool = true
(* Solution-2: *)
let equal1st2nd lst =
match lst with
| x :: y :: _ -> if x = y then true else false
| _ -> false;;
Forward Recursion
In forward recursion, the function recursively first calls on all recursive components, and then
builds the final result from the partial results.
I.e.: Wait until the whole structure has been traversed (recursively) to start building the answer.
let rec sum n = if n = 0 then 0 else n + sum1n (n-1);;
sum 3;;
sum 3
3 + sum 2
Call Stacks 2 + sum 1
While a program runs, there is a call stack of function calls that 1 + sum 0
have started but not yet returned. 0
• Calling a function f pushes an instance of f on the stack 1 + 0
2+1
(with the return point in the program), 3+3
• When a call to f finishes, it is popped from the stack. 6
These stack-frames store information such as the value of local
variables and "what is left to do" in the function.
Due to recursion, multiple stack-frames may be calls to the same function.
Tail Recursion
• A recursive function is tail-recursive if all recursive calls are the last
thing that the function does.
• Tail recursion generally requires extra "accumulator" arguments to
pass partial results.
• May require an auxiliary function!
• The general idea is to write your recursive function such that the
value returned by the recursive call is what’s returned by your
function
• i.e., there’s no pending operation in the function waiting for
the value returned by the recursive call.
• That way, the function can say, ”Don’t bother with me anymore, just
take the answer from my recursive call as the result. You can just
forget all of my state information.”
Experiment:
Write a function that takes a value x and an integer n, and returns a list of length n whose
elements are all x.
let rec makeList x n =
if n = 0 then []
else x :: makeList x (n-1);;
val makeList : 'a -> int -> 'a list = <fun>
makeList "hi" 5;;
- : string list = ["hi"; "hi"; "hi"; "hi"; "hi"]
makeList 7 4;;
- : int list = [7; 7; 7; 7]
makeList 7 1234567;;
Stack overflow during evaluation (looping recursion?).
squareF [1;2;3;4];;
- : int list = [1; 4; 9; 16]
let fib n =
let rec fibo n nm1 nm2 =
if n = 0 then nm2
else if n = 1 then nm1
else fib(n - 1) (nm1 + nm2) nm1
in fibo n 1 1;;
val fib : int -> int = <fun>
Define an OCaml function printMult that gets an integer n and a string to print the string n times
on different lines.
let rec printMult n s =
match n with
Principles of Programming Languages 62 Halil Özmen
| 0 -> ()
| _ -> print_endline s; printMult (n-1) s;;
val printMult : int -> string -> unit = <fun>
sumList [];;
- : int = 0
sumList [7; 1; 4];;
- : int = 12
Monday;
- : weekday = Monday
let today = Thursday;;
val today : weekday = Thursday
Exercise: Define a data type shape, that can be a circle, square or a triangle. Circle has a
radius, square has a side length, and triangle has three sides (all float).
type shape =
Circle of float
| Square of float
| Triangle of float * float * float;;
let area s =
match s with
| Circle r -> 3.14159 *. r *. r
| Square a -> a *. a
| Triangle (a,b,c) -> let s = (a +. b +. c) /. 2.
in sqrt(s *. (s -. a) *. (s -. b) *. (s -. b));;
val area : shape -> float = <fun>
area c;;
- : float = 162.8600256
area s;;
- : float = 36.
area (Triangle(3., 4., 5.));;
- : float = 8.48528137423857
Write a function contains that evaluates to true if a binary tree contains a given value.
let rec contains t n =
match t with
| Leaf i -> i = n
| Node (t1, t2) -> contains t1 n || contains t2 n;;
val contains : tree -> int -> bool = <fun>
contains myTree 6;;
- : bool = false
contains myTree 8;;
- : bool = true
Write a function flatten that traverse a binary tree and creates a list.
let rec flatten t =
match t with
Principles of Programming Languages 65 Halil Özmen
| Leaf n -> [n]
| Node(t1,t2) -> flatten t1 @ flatten t2;;
let myTree = Node(Node(Leaf 4, Node(Leaf 5, Leaf 8)),
Node(Leaf 9, Leaf 12));;
flatten myTree;;
- : int list = [4; 5; 8; 9; 12]
Write a function mirror that mirrors a binary tree (reverses left and right).
let rec mirror t =
match t with
| Leaf n -> Leaf n
| Node(t1, t2) -> Node(t2, t1);;
myTree;;
- : tree = Node (Node (Leaf 4, Node (Leaf 5, Leaf 8)), Node (Leaf
9, Leaf 12))
mirror myTree;;
- : tree = Node (Node (Leaf 9, Leaf 12), Node (Leaf 4, Node (Leaf
5, Leaf 8)))
Write a function treeSum that evaluates to the sum of all values in a binary tree.
let rec treeSum t =
match t with
| Leaf n -> n
| Node(t1, t2) -> treeSum t1 + treeSum t2;;
val treeSum : tree -> int = <fun>
treeSum myTree;;
- : int = 38
size intTree;;
- : int = 9
size strTree;;
- : int = 5
flatten strTree;;
- : string list = ["at"; "in"; "the"; "a"; "on"]
Write a function contains that evaluates to true if a tree contains a given value.
let rec contains t x =
match t with
| Leaf a -> a == x
| Node(a,t1,t2) -> a == x || contains t1 x || contains t2 x;;
Printing
Printing has no usefull meaning at the toplevel. It is meaningfull when an Ocaml program is run.
print_int 7;; print_string "Hello World!";; print_string "\n";;
print_float 2.7182;; print_endline "Hello World!";; print_endline "";;
print_char 'T';;
Output:
24: Jane 8.7
24: Jane 8.74
24: Jane 8.700000
0024: Jane 8.70
0824: Albert 20.65
Define an OCaml function printInt that prints an integer number and move to next line.
let printInt n = print_int n; print_endline "";;
Define an OCaml function printis that prints an integer number followed by a string.
let printis n s = print_int n; print_string s;;
(* Solution: *)
let rec print1n n s =
match n with
| 0 -> ()
| v -> print1n (v-1) s; printis v s;;
Input
Input has no usefull meaning at the toplevel. It is usefull when an Ocaml program is run.
Exercises:
1. Provide values (other than empty list) to form lists of given types.
????;; ????;;
- ; int list - ; string list list
????;; ????;;
- ; int list list - ; (int * string list) list
????;; ????;;
- ; (int * float) list - ; (int * string list) list list
????;;
- ; (int * string) list