Programming Paradigms
Programming Languages
Programming languages are used to describe the behavior of a computer (includes mobile phones, tablets, embedded devices and servers) to perform a specific task. There are many programming languages and each of them has its own strengths and weaknesses. Some of the programming languages are general purpose and some of them are domain specific. In this chapter, we will look different programming languages and their paradigms. We will try to classify them with different criteria such as typing, execution model, and paradigm.
A programming language is a formal specification of programs that can be executed by a computer. For this reason, it is important to know the difference between a programming language and implementation of a programming language.
Programming Language | Implementation(s) |
---|---|
C | GCC, Clang |
Java | OpenJDK, Oracle |
Python | CPython, PyPy |
Ruby | MRI, JRuby |
Javascript | V8, SpiderMonkey |
PHP | Zend Engine |
Go | gc, gccgo |
Rust | rustc |
Swift | Swift |
The syntax of a programming language describes the valid "sentences" that can be written in the language. On the other hand, the semantics of a programming language describes the meaning of the "sentences" that can be written in the language.
Programming languages often categorized by their paradigms, execution model and typing.
Compiled and Interpreted Languages
Most programming languages can be categorized as compiled or interpreted.
Programming Language | Interpreted | Compiled |
---|---|---|
Javascript | Yes | No |
Typescript | Yes | Yes |
Python | Yes | No |
Java | No | Yes |
C# | No | Yes |
PHP | Yes | No |
Go | No | Yes |
Imperative Programming Languages
Declarative Programming Languages
Object Oriented Programming
Object orient programming is a programming paradigm that organizes code into objects that combines data and behavior in a single unit. Commonly a unit of data stored in an object is called attribute and the a unit of behavior is called method. Object oriented programming is one of the most popular programming paradigms in the world. Most modern programming languages support object oriented programming today.
Why we need to combine data and behavior in a single unit? Lets examine a simple counter example. Suppose that we want to write a program that counts the number of times a button is clicked. We want to print the number to the console when a user clicks a button. We can write a simple program like this:
// Create a simple variable to store the count
var count = 0;
// Create a function to increment the count
function inc() {
// Increment the counter
count = count + 1;
// Print the counter to the console
console.log(count);
}
// Create a button element
// If you do not know javascript, just know that this block of triggers
// the inc function when the button is clicked
var button = document.createElement('button');
button.textContent = 'Click me';
button.onclick = () => inc();
document.body.appendChild(button);
This program will work as expected. However there are some problems with this program. The first one is that the
count
variable is a global variable. This means that any part of the program can change the value of the count
variable. This can lead to bugs that are hard to find. The second one is related to creating multiple counters.
If we want to create multiple counters, we need to create multiple count
variables and inc
functions. This
can lead to code duplication and hard to maintain code.
// Create a simple variable to store the count1
var count1 = 0;
// Create a function to increment the count1
function inc1() {
// Increment the counter
count1 = count1 + 1;
// Print the counter to the console
console.log(count1);
}
// Create a button element
// If you do not know javascript, just know that this block of triggers
// the inc1 function when the button is clicked
var button1 = document.createElement('button');
button1.textContent = 'Click me';
button1.onclick = () => inc1();
document.body.appendChild(button1);
// Create a simple variable to store the count2
var count2 = 0;
// Create a function to increment the count2
function inc2() {
// Increment the counter
count2 = count2 + 1;
// Print the counter to the console
console.log(count2);
}
// Create a button element
// If you do not know javascript, just know that this block of triggers
// the inc2 function when the button is clicked
var button2 = document.createElement('button');
button2.textContent = 'Click me';
button2.onclick = () => inc2();
document.body.appendChild(button2);
This code is too dirty and hard to maintain. Object oriented programming solves these problems by introducing a method to combine data and behavior in a single unit. This unit is called an object. Lets rewrite the counter example using objects.
// Create counter1 object
var counter1 = {
count: 0, // <-- Attribute
inc: function () {
counter1.count = counter1.count + 1;
console.log(counter1.count);
}, // <-- Method
};
// Create counter2 object
var counter2 = {
count: 0, // <-- Attribute
inc: function () {
counter1.count = counter1.count + 1;
console.log(counter1.count);
}, // <-- Method
};
// Create a button element
var button1 = document.createElement('button');
button1.textContent = 'Click me';
button1.onclick = () => counter1.inc();
document.body.appendChild(button1);
// Create a button element
var button2 = document.createElement('button');
button2.textContent = 'Click me';
button2.onclick = () => counter2.inc();
document.body.appendChild(button2);
This code is much cleaner compared to previos one. However as you can notice, the inc
method contains the same code
except the object name. In order to remove code duplication, we can receive the object the we interested in as a parameter
to the inc
method.
// Create counter1 object
var counter1 = {
count: 0, // <-- Attribute
};
// Create counter2 object
var counter2 = {
count: 0, // <-- Attribute
};
function inc(counter) {
// <-- Receive the object as a parameter (again it is a method)
counter.count = counter.count + 1;
console.log(counter.count);
}
// Create a button element
var button1 = document.createElement('button');
button1.textContent = 'Click me';
button1.onclick = () => inc(counter1); // <-- Pass counter1 object to the inc function
document.body.appendChild(button1);
// Create a button element
var button2 = document.createElement('button');
button2.textContent = 'Click me';
button2.onclick = () => inc(counter2); // <-- Pass counter2 object to the inc function
document.body.appendChild(button2);
By passing the object to the inc
method, we removed the code duplication. This is the basic idea of object oriented
programming. Instead of passing the object that we interested in as an explicit parameter, we can use a special keyword
called this
which is passed to the function implicitly. Lets rewrite the counter example using this
keyword.
var counter1 = {
count: 0,
inc: function () {
this.count = this.count + 1;
console.log(this.count);
},
};
var counter2 = {
count: 0,
inc: function () {
this.count = this.count + 1;
console.log(this.count);
},
};
var button1 = document.createElement('button');
button1.textContent = 'Click me';
button1.onclick = () => counter1.inc(); // <-- No need to pass the object explicitly
document.body.appendChild(button1);
var button2 = document.createElement('button');
button2.textContent = 'Click me';
button2.onclick = () => counter2.inc(); // <-- No need to pass the object explicitly
document.body.appendChild(button2);
However instead of specifying each attribute's initial value one by one we can use a function that will create the object for us. This function is called a constructor. Lets rewrite the counter example using a constructor.
function Counter() {
// <-- Constructor function
return {
count: 0,
inc: function () {
this.count = this.count + 1;
console.log(this.count);
},
};
}
var counter1 = Counter(); // <-- Create counter1 object
var counter2 = Counter(); // <-- Create counter2 object
var button1 = document.createElement('button');
button1.textContent = 'Click me';
button1.onclick = () => counter1.inc();
document.body.appendChild(button1);
var button2 = document.createElement('button');
button2.textContent = 'Click me';
button2.onclick = () => counter2.inc();
document.body.appendChild(button2);
This example shows the basic idea of object oriented programming and covers attribute
, method
, and constructor
concepts. Although there are some improvements that can be made, this example is enough to understand the basic idea
of object oriented programming and we will cover these improvements in the following sections.
Classes
A class is a blueprint for an object. It defines common attributes and methods that objects created from the class will have. In the previous examples, we created objects using object literals and constructor functions. However modern programming languages provide a special syntax to define classes. Lets rewrite the counter example using classes.
class Counter {
constructor() {
this.count = 0;
}
inc() {
this.count = this.count + 1;
console.log(this.count);
}
}
var counter1 = new Counter();
var counter2 = new Counter();
A class is a type in a typed programming language. This means that we can create a variable that has a type of a class. In the previous examples, we created objects using object literals and constructor functions. Lets examine an example for Java programming language.
class Counter {
int count = 0;
void inc() {
count = count + 1;
System.out.println(count);
}
}
public class Main {
public static void main(String[] args) {
Counter counter1 = new Counter(); // <-- Type of counter1 is Counter
Counter counter2 = new Counter(); // <-- Type of counter2 is Counter
}
}
Therefore, you should notice that a class is not a concrete object. It is just a blueprint for an object. When you create
an instance of a class, it becomes an object. In the previous examples, Counter
is a class and counter1
and counter2
are objects.
Encapsulation
Encapsulation is a concept that restricts direct access to some of the object's attrubutes and methods. Encapsulation is used to protect the object's state from being modified by other objects. In object oriented programming, we can restrict access to an object's attributes and methods by using access modifiers.
For instance, in our counter example, the count
attribute is public which means that any part of the program can
change the value of the count
attribute. This can lead to bugs that are hard to find. We can restrict access to the
count
attribute by using access modifiers. Access modifiers are language specific and they can be different from
language to language. Lets examine an example for Typescript programming language.
class Counter {
private count = 0; // <-- Private access modifier
private printToConsole() {
// <-- Private access modifier
console.log(this.count);
}
inc() {
// <-- Public access modifier by default
this.count = this.count + 1; // <-- Access to the private attribute
this.printToConsole(); // <-- Access to the private method
}
}
var counter1 = new Counter();
var counter2 = new Counter();
counter1.inc(); // <-- Access to the public method
counter2.inc(); // <-- Access to the public method
In this example, the count
attribute is private which means that only the methods of the Counter
class can access
the count
attribute directly. The printToConsole
method is also private which means that only the methods of the
Counter
class can access the printToConsole
method directly. The inc
method is public by default which means
that any part of the program can access the inc
method.
Inheritance
Inheritance is a concept that allows a class to inherit attributes and methods from another class. The class that inherits attributes and methods is called a subclass and the class that provides attributes and methods is called a superclass. Lets examine an example for Typescript programming language.
class Counter {
private count = 0;
private printToConsole() {
console.log(this.count);
}
inc() {
this.count = this.count + 1;
this.printToConsole();
}
}
class ResettableCounter extends Counter {
reset() {
this.count = 0;
this.printToConsole();
}
}
var counter1 = new ResettableCounter();
var counter2 = new Counter();
counter1.inc(); // <-- Access to the public method of the Counter class
counter1.reset(); // <-- Access to the public method of the ResettableCounter class
counter2.inc(); // <-- Access to the public method of the Counter class
In this example, the ResettableCounter
class is a subclass of the Counter
class. The ResettableCounter
class
inherits the count
attribute and the printToConsole
method from the Counter
class. The ResettableCounter
class
also has a reset
method that is not available in the Counter
class. The ResettableCounter
class can access the
count
attribute and the printToConsole
method directly. The ResettableCounter
class can also access the inc
method of the Counter
class.
Also, it is possible to override the methods of the superclass in the subclass. Lets examine an example for Typescript programming language.
class Counter {
private count = 0;
private printToConsole() {
console.log(this.count);
}
inc() {
this.count = this.count + 1;
this.printToConsole();
}
}
class ResettableCounter extends Counter {
private printToConsole() {
console.log('ResettableCounter printToConsole method');
console.log(this.count);
}
reset() {
this.count = 0;
this.printToConsole();
}
}
var counter1 = new ResettableCounter();
var counter2 = new Counter();
counter1.inc();
counter1.reset();
counter1.inc();
counter2.inc();
Therefore, by using inheritance, we can remove code duplication and create a hierarchy of classes.
Polymorphism
Work in Progress
Abstraction
Abstraction is a concept that hides the implementation details of an object and only shows the necessary details to the outside world. Abstraction is used to simplify the complexity of an object. In object oriented programming, we can use abstract classes and interfaces to achieve abstraction.
An interface is a specification of a class. It defines a set of methods that a class must implement. Lets examine an example for Typescript programming language.
interface Counter {
count: number;
inc(): void;
}
class InMemoryCounter implements Counter {
count = 0;
inc() {
this.count = this.count + 1;
console.log(this.count);
}
}
class LocalStorageCounter implements Counter {
count = 0;
constructor() {
this.count = parseInt(localStorage.getItem('count') || '0');
}
inc() {
this.count = this.count + 1;
localStorage.setItem('count', this.count.toString());
console.log(this.count);
}
}
// Gets an instance of the Counter interface and increments the counter n times
function countNTimes(counter: Counter, n: number) {
for (var i = 0; i < n; i++) {
counter.inc(); // <-- We do not know which concrete class will be used here
}
}
var counter1 = new InMemoryCounter();
var counter2 = new LocalStorageCounter();
counter1.inc();
counter2.inc();
// Refresh the page and run the program again
When you refresh the page and run the program again, you will see that the count
attribute of the LocalStorageCounter
class is persisted in the local storage. This is an example of abstraction. The countNTimes
function does not know
which concrete class will be used. It only knows that the class will have a count
attribute and an inc
method.
It does not care about the implementation details of the class.
This way of calling a method without knowing the implementation details is called dynamic dispatch. Dynamic dispatch
is a mechanism that determines which method to call at runtime. In the previous example, the countNTimes
function
uses dynamic dispatch to call the inc
method of the Counter
interface. On the other hand, counter1.inc()
and
counter2.inc()
calls the inc
method of the InMemoryCounter
and LocalStorageCounter
classes respectively. This
is called static dispatch. Static dispatch is a mechanism that determines which method to call at compile time.
Prototype Chains (JavaScript Specific, Optional)
Generally, an object oriented programming language has two choices for implementing object oriented programming. The first one is class-based and the second one is prototype-based. In class-based object oriented programming, a class is a blueprint for an object and an object is an instance of a class.
Work in Progress
Functional Programming
Functional programming is a programming paradigm that treats computation as the evaluation of mathematical functions and avoids changing state and mutable data. It is a declarative programming paradigm, which means that programming is done with expressions or declarations instead of statements.
Some programming languages support functional programming paradigm in addition to other paradigms. For instance, JavaScript supports functional programming paradigm in addition to object-oriented programming paradigm. On the other hand, some programming languages are designed to support only functional programming paradigm. For instance, Haskell is a purely functional programming language.
We will look at some of the core concepts of functional programming in this chapter.
Pure Functions
There is a difference between functions in mathematics and functions in programming. In mathematics, a function is a relation between a set of inputs and a set of possible outputs. Therefore, a function satisfies these two properties:
- A function should return the same output for the same input.
- A function should not have side effects.
However, in programming, a function can have side effects and it can return different outputs for the same input. For instance, a random number generator function is a function that returns different outputs for the same input. It does not take any arguments but it returns a different output each time you call it. (If it would be a function in a mathematical sense, it should return only one value because it does not take any arguments.).
A function that prints something to the screen is a function that has side effects. It does not return anything but it changes the state of the screen. Or a function that send a request to a server is a function that has side effects because it manipulates the "outside world" by using electrical signals. A function that reads a file from the disk is also a function that has side effects because it causes the disk to spin and read the file that is located on a specific location on the disk.
Lets look at some examples;
// Some pure functions
(x, y) => x + y;
(person) => (person.age > 18 ? 'Adult' : 'Child');
(str) => JSON.parse(str);
// Some impure functions
(x, y) => x + y + Math.random();
(x, y) => console.log(x + y);
() => fs.readFileSync('file.txt');
(x, y) => fs.writeFileSync('file.txt', x + y);
(x, y) => {
console.log(x + y);
return x + y;
};
In some functional programming languages, all functions must be pure functions (such as Haskell). Pure functions are very useful because they are easy to test. You can test a pure function by giving it an input and checking the output. If the output is the same as you expected, then the function is working correctly. Also, pure functions are easy to understand and most importantly, they are easy to compose. Therefore, building complex systems with pure functions is much easier than building them with impure functions. Because, they are guaranteed to work correctly for each individual function and composing them will not result any unexpected behavior.
First Class Functions
Immutablity
One of the most bug-prone parts of a code is mutable variables (or variables). Because, you can change the value of a
variable at any time from any place. This can cause a lot of bugs. In order to avoid these bugs, you should use immutable
variables as much as possible. Instead of using let
and var
, you should use const
in order to make your variables
immutable. Also, you should avoid mutating objects and arrays. You should use map
, filter
, reduce
functions to
manipulate arrays.
Also you should be careful about scoping. Because scopes can restrict the usage of a variable. If you are defining a variable in a global scope, you can use it from anywhere. This can cause a lot of bugs. For this reason, please try to use local variables as much as possible and keep your scopes as small as possible.
It may be seem to be impossible to avoid mutable variables. However, you can use some techniques to avoid them. For
instance, you can use Map, Filter and Reduce functions can be used. They are higher order functions which means that they
take a function as an argument and they return a new array. These functions are very useful and they are reduces the
need for loops and mutable variables. You should use these functions as much as possible. Also there are some other
functions in javascript that you can use to manipulate arrays. For instance, forEach
, some
, every
, find
, findIndex
etc. You should use these functions as much as possible. Also, you can use lodash
library to manipulate objects and
arrays.
Also, you should return results of a function with return
statement. Mutating a global variable and then reading it
from another function is a bad practice. Instead of that, you should return the result of a function with return
statement. This will make your code much more clear and easy to understand.
If you want to learn more about immutability, you can check Functional Programming Paradigm.
Common Higher Order Functions
Side Effects with Pure Functions
You may wonder that how is it possible to write a program only with pure functions. Because, in real world, you need to interact with the outside world. You need to read files, send requests to servers, print something to the screen etc. However, this is possible in Haskell. At the end of the day nothing is pure because the nature of the problem that you want to solve is not pure. Lets look at an example from Haskell.
-- You can run this program with `runhaskell` command in your terminal but you need to have ghc installed on your system.
-- putStrLn is a function that takes a string and returns an IO action that DESCRIBES how to print the given string to
-- the screen. Type signiture of putStrLn is:
-- putStrLn :: String -> IO ()
-- main is a DESCRIPTION of the program. It is an IO action that DESCRIBES how to run the program.
-- It is not a function it is just a value that describes how to run the program.
main:: IO ()
main = putStrLn "Hello, World!" -- In haskell, we call functions with space instead of parentheses.
You should think IO
as a recipe that describes what a computer should do. Although, executing instructions in a computer
is not a pure operation, the recipe that includes these instructions itself is a completely pure value. Lets look at
another example.
-- This is a pure function that takes a string and returns a string
greet :: String -> String
greet name = "Hello, " ++ name ++ "!"
main:: IO ()
main = putStrLn (greet "John")
Lets examine the code above. The haskell compiler first looks at the main
function. It sees that main
is an IO action
that describes how to run the program. However in order to get a complete list of instructions, it needs to evaluate the
putStrLn (greet "John")
expression. In order to evaluate this expression, it needs to evaluate the greet "John"
expression. Therefore it evaluates the greet
function with the argument "John"
. The greet
function returns a string
that is "Hello, John!"
. Then the compiler evaluates the putStrLn
function with the argument "Hello, John!"
. The
putStrLn
function returns an IO action that describes how to print the given string to the screen. We can see its like
this:
main = `Print "Hello, John!" to the screen`
After that, the compiler runs the instructions that are described in the main
function. It prints the string "Hello, John!"
to the screen. Lets look at a more complex example.
greet :: String -> String
greet name = "Hello, " ++ name ++ "!"
-- Lets write our main function that will ask the name from the user and greet the user
main :: IO ()
main = putStrLn "What is your name?"