Overview: Differences Between OOP and Functional Programming
- Only a few functions work on common data.
- Uses an inheritance model.
- Object state can be modified.
- Functions use side effects.
- Sub-classes might only need (or perhaps should only use) one method, yet they still absorb all methods from the parent classes.
- There is potential fragility if changes to a super-class has unforeseen effects on a sub-class.
- The state of an object and function side-effects can become difficult to manage as projects get larger.
Functional Programming uses composition, via an assembly-line style combining of many simple functions to act on fixed data. This structures code around "what something has". Unlike OOP's "box" approach, this is more like putting data in one end of a pipe and then the processed data comes out on the other end.
Basic Qualities- Many functions work on fixed data.
- No modification of state.
- Pure functions - no side effects.
- More declarative - about what needs to be done.
Object Oriented Programming
Benefits of OOP
- Clear and understandable.
- Easy to extend.
- Easy to maintain.
- Memory efficient.
- Adheres to "D.R.Y." philosophy (Don't Repeat Yourself).
Four Pillars of OOP
- Encapsulation - neat & clean self-contained packages that can interact with each other
- Abstraction - Hiding the complexity to make interaction with the code basic, intuitive and complete.
- Inheritance - Avoiding duplicated code by sharing methods and properties.
- Polymorphism - Ability to call a single method on different objects and have the method function different based on the object or situation.
Using
const elf = {
name: 'Orwell',
weapon: 'bow',
attack() {
return 'attack with ' + elf.weapon
}
}
const elf2 = {
name: 'Sam',
weapon: 'Sword',
attack() {
return 'attack with ' + elf.weapon
}
}
The problem with the objects above is they repeat code and, presumably, there will be more of these. Duplicating this many times is time-consuming and inefficient at runtime.
The concept of "Factory Functions", or functions that build objects, can make the objects above cleaner and more efficient to build.
// Factory function - build elf objects
function createElf(name, weapon) {
return {
name: name,
weapon: weapon,
attack() {
return 'attack with ' + weapon
}
}
}
const peter = createElf('Peter', 'Stones');
console.log(peter.attack()); // RESULT: "attack with Stones"
const sam = createElf('Sam', 'Fire');
console.log(sam.attack()); // RESULT: "attack with Fire"
This is better, but still will house common code, like attack() in different places in memory for each object. To prevent this duplication, object Inheritance can allow each object to reference, through prototype chaining, a core Elf object. Constructor Functions were originally used for this purpose. Constructor Functions use the
// Constructor functions
// A capitol first letter is common to indicate a Constructor function.
function Elf(name, weapon) {
this.name = name;
this.weapon = weapon;
}
// Setup the prototype chain
Elf.prototype.attack = function() {
return 'attack with ' + this.weapon
}
// the new keyword is required for using Constructor functions to point "this" to the object being created.
const peter = new Elf('Peter', 'Stones');
console.log(peter.attack()); // RESULT: "attack with Stones"
const sam = new Elf('Sam', 'Fire');
console.log(sam.attack()); // RESULT: "attack with Fire"
A step better than Constructor Functions,
// Factory function - build elf objects
const elfFunctionsStore = {
attack() {
return 'attack with ' + this.weapon
}
}
function createElf(name, weapon) {
// Set up the prototype chain
let newElf = Object.create(elfFunctionsStore);
newElf.name = name;
newElf.weapon = weapon;
return newElf;
}
const peter = createElf('Peter', 'Stones');
console.log(peter.attack()); // RESULT: "attack with Stones"
const sam = createElf('Sam', 'Fire');
console.log(sam.attack()); // RESULT: "attack with Fire"
Everything in the constructor section of a class gets created as a new property on the new object. Everything outside of the constructor section, like functions, will be referenced by the new object and not duplicated. This makes non-unique bits of code, like functions, take up less memory.
// ES6 Class
class Elf {
constructor(name, weapon) {
this.name = name;
this.weapon = weapon;
}
attack() {
return 'attack with ' + this.weapon
}
}
const peter = new Elf('Peter', 'Stones');
console.log(peter.attack()); // RESULT: "attack with Stones"
const sam = new Elf('Sam', 'Fire');
console.log(sam.attack()); // RESULT: "attack with Fire"
Creating a new object that is a subset of a main object is thought of as creating an instance of a class. This process is called instantiation. For this, using
// ES6 Class
class Elf {
constructor(name, weapon) {
this.name = name;
this.weapon = weapon;
}
attack() {
return 'attack with ' + this.weapon
}
}
const peter = new Elf('Peter', 'Stones');
console.log(peter.attack()); // RESULT: "attack with Stones"
const sam = new Elf('Sam', 'Fire');
console.log(sam.attack()); // RESULT: "attack with Fire"
// Check for instanceOf
console.log('Is Peter and instance of Elf?', peter instanceof Elf);
console.log('Is Sam and instance of Elf?', sam instanceof Elf);
The above accomplished an OOP-style of programming (even though JavaScript technically is still just using prototypal inheritance under the hood), but this can be improved to a more complete OOP approach. Using a super-class and then extending this to create sub-classes allows for sharing of common functions between all objects created, while each sub-class object can share functions and properties unique to their group
For this, you need to call the super-class' constructor and pass the needed variables. This is done through the
// Set the super-class
class Character {
constructor(name, weapon) {
this.name = name;
this.weapon = weapon;
}
attack() {
return 'attack with ' + this.weapon
}
}
// Extend the super-class
class Elf extends Character {
constructor(name, weapon, type) {
super(name, weapon, type);
// Add properties unique to this sub-class, but also unique variations for each instance of this sub-class
this.type = type
}
}
class Ogre extends Character {
constructor(name, weapon, color) {
super(name, weapon);
// Add properties unique to this sub-class, but also unique variations for each instance of this sub-class
this.color = color;
}
// Add functions unique to this sub-class and shared by all in the sub-class
makeFort() {
return 'The strongest fort in the world has been made'
}
}
const peter = new Elf('Peter', 'Stones', 'Wood Elf');
console.log(peter.name); // RESULT: "Peter"
console.log(peter.type); // RESULT: "Wood Elf"
console.log(peter.attack()); // RESULT: "attack with Stones"
const sam = new Elf('Sam', 'Fire', 'House Elf');
console.log(sam.name); // RESULT: "Sam"
console.log(sam.type); // RESULT: "House Elf"
console.log(sam.attack()); // RESULT: "attack with Fire"
const shrek = new Ogre('Shrek', 'club', 'green');
console.log(shrek.name); // RESULT: "Shrek"
console.log(shrek.attack()); // RESULT: "attack with club"
console.log(shrek.makeFort()); // RESULT: "The strongest fort in the world has been made"
// Check for instanceOf
console.log('Is Peter an instance of Elf?', peter instanceof Elf); // RESULT: true
console.log('Is Sam and instance of Elf?', sam instanceof Elf); // RESULT: true
console.log('Is Shrek an instance of Elf?', shrek instanceof Elf); // RESULT: false
console.log('Is Shrek an instance of Character?', shrek instanceof Character); // RESULT: true
console.log('Is Shrek an instance of Ogre?', shrek instanceof Ogre); // RESULT: true
// Under the hood, JS is just creating prototype chains
console.log('Is Ogre a prototype of Shrek? ', Ogre.prototype.isPrototypeOf(shrek));
console.log('Is Character a prototype of Ogre? ', Character.prototype.isPrototypeOf(Ogre.prototype));
Four ways to bind this
Creating with the new Keyword
function Person(name, age) {
this.name = name;
this.age = age;
}
// this becomes bound to the object created with the new keyword
const person1 = new Person(Xavier ', 55);
Implicit Binding
// this will reference an object, even without directly declaring it.
person2 {
name: 'Karen',
age: 40,
hi() {
console.log('hi' + this.name)
}
}
const person1 = new Person(Xavier ', 55);
Explicit Binding
const person3 = {
name: 'Karen',
age: 40,
hi: function() {
// this is explicitly bound to the window object here
console.log('hi' + this.setTimeout)
}.bind(window)
}
person3.hi();
Arrow Function
const person4 = {
name: 'Karen',
age: 40,
hi: function() {
// this is bound to the lexical environment with an arrow function, not to where it is invoked like a regular function
var innerFunction = () = & gt; {
console.log('hi' + this.name)
}
return innerFunction
}
}
person4.hi();
Functional Programming
- Functional programming was introduced in LISP over 60 years ago (1958).
- Current popular functional programming languages include Pascal, Scala, and Clojure.
- Functional programming works really well for distributed computing (multiple machines interacting with data) and parallelism (machines working on the same data at the same time).
- A strong focus on simplicity concerning data and functions is a characteristic of Functional Programming languages.
- The separation of concerns - data and functions - creates clarity.
- Pure functions are a staple of functional programming.
- All objects created in functional programming are immutable and sharing state is not allowed.
Main goals are to achieve code that is:
- Clear and understandable.
- Easy to extend.
- Easy to maintain.
- Memory efficient.
- D.R.Y.
Pure Functions
Rules for Pure Functions- Referential Transparency:Functions that always return the same output given the same input.
- Side Effects:Cannot modify anything outside of the functions.
- Only one task.
- Return statement.
- No shared state.
- Immutable state.
- Composable.
- Predictable.
Can the average program only have pure functions? No. There will always be a certain amount of interactions/side effects in any program. The key, as far as Functional Programming is concerned, is to organize the code in a way that isolates these side effects.
// These are NOT Pure function as each will alter the array every time it is called, and the order of calling these makes a difference.
const array = [1, 2, 3];
function mutateArray(arr) {
arr.pop();
}
function mutateArray2(arr) {
arr.push()
}
// Rewritten as Pure functions
const array = [1, 2, 3];
function removeLastItem(arr) {
const newArray = [].concat(arr);
newArray.pop();
return newArray
}
function addToArray(arr) {
const newArray = [].concat(arr);
newArray.push(1)
return newArray
}
console.log(removeLastItem(array)); // RESULT: [ 1, 2 ]
console.log(addToArray(array)); // RESULT: [ 1, 2, 3, 1 ]
console.log(array); // RESULT: [ 1, 2, 3] (has not been altered)
// Another example of a Pure function
function multiplyBy2(arr) {
return arr.map(item = & gt; item * 2)
}
console.log(multiplyBy2(array)); // RESULT: [ 2, 4, 6 ]
console.log(array); // RESULT: [ 1, 2, 3] (has not been altered)
Idempotence
This is the concept of a function doing exactly what is expected every time it is called. The function may or may not be Pure (it might have side effects) but it still performs the same action and returns the same result every time it is called with the same variables. In addition, an idempotent function can call itself repeatedly and still return the same output as if it did not call itself
This concept is especially valuable for parallel and distributed computing in that it makes the code predictable and consistent
// Not Idempotent
function notIdempotent(num) {
Math.random(num)
}
notIdempotent(5) // RESULT: (This returns a different number every time, because the function is not idempotent)
// An idempotent function that is not Pure becasue it interacts outside its environment
function antIdempotentOne(num) {
console.log(num);
}
antIdempotentOne(5) // RESULT: 5
// An example of being able to call itself repeatedly and still output the the same as if not calling itself
Math.abs(Math.abs(Math.abs(-50)));
Imperative vs Declarative
Imperative: Telling the machine what to do and how to do it.
Declarative: Telling the machine what to do and what you want to have happen, but not how to do it. This will generally still be compiled by something more imperative, but the engineers interaction with the code can be less complex with declarative functions
Functional Programming helps us be more declarative.
// Imperative command - Details the actions to be taken to produce a result
for (let i = 0; i & lt; = 3; i++) {
console.log(i);
}
// Decalrative - No instruction on how to perform this, only what should happen
[1, 2, 3].forEach(item = & gt; console.log(item))
Currying
Currying is the technique of reducing functions to a very basic utility with no more than one argument that can then be linked together to perform larger operations. This can reduce system demands and control complexity.
// Without currying
const multiply = (a, b) => a * b;
multiply(3, 4); // RESULT: 12
// With currying
const curriedMultiply = (a) => (b) => a * b;
curriedMultiply(3)(4); // RESULT: 12
// Other possibilities for currying
const multBy5 = curriedMultiply(5);
const multBy7 = curriedMultiply(7);
const multBy250 = curriedMultiply(255);
multBy5(3); // RESULT: 15
multBy7(3); // RESULT: 21
multBy250(3); // RESULT: 750
multBy5(multBy7(3)); // RESULT: 75
multBy5(multBy5(3)); // RESULT: 105
Partial Application
Partial Application is the technique of partially filling a function's variables/ actions so that the function can be reused with less system demand because it is partially saved in memory. The function would expect the rest of the variables on the second call. This can also create cleaner, more obvious code while eliminating repeated identical variable input..
// Without Partial Application
const multiply = (a, b, c) => a * b * c;
multiply(3, 4, 10); // RESULT: 120
const partMultBy5 = multiply.bind(null, 5);
partMultBy5(4, 10) // RESULT: 200
Memoization (Cache)
Memoization is the act of storing the output of a function in memory to be read multiple times. This keeps the data available while avoiding running the same function and parameters multiple times.
// Using closure wraps the variable and keeps it away from the Global scope.
function memoizeAddTo80(n) {
let cache = {};
return function(n) {
if (n in cache) {
return cache[n];
} else {
// THhs console.log simulates the longer calculation that might be taking place
console.log('long time');
const answer = n + 80;
cache[n] = answer;
return answer;
}
}
}
const memoized = memoizeAddTo80();
console.log(1, memoized(7)) // RESULT: long time 87
console.log(2, memoized(7)) // RESULT: 83
Compose & Pipe
Composability is a system design principle that handles different components directly interacting. Essentially, it's a process for combining different functions and running them in various combinations. This is somewhat like a function assembly line, where the output of each function feeds into the next one, and so on.
Pipe is the same as compose, but it runs the functions in reverse order. The first function fires first with the output feeding the next function, whose output feeds the next function, etc.
// Create a compose function
// This runs the last parameter first (g) and
// the return from that is fed into the first parameter (f)
const compose = (f, g) => (data) => f(g(data));
// Create a pipe function
// This runs the first function (g) and
// the output from that is fed into the next parameter (f)
const pip = (f, g) => (data) => f(g(data));
const multiplyBy3 = (num) => num * 3;
const makePositive = (num) => Math.abs(num);
// Compose it all in a littel assembly line
const multiplyBy3andAbsolut = compose(multiplyBy3, makePositive);
// Run it
multiplyBy3andAbsolut(-50); // RESULT: 150
// Pipe it all in a littel assembly line
const multiplyBy3andAbsolut = pipe(multiplyBy3, makePositive);
// Run it
multiplyBy3andAbsolut(-50); // RESULT: 150
Arity
Arity is the number of parameters a function accepts. In Functional Programming, an arity of one or two is desired.
// This has an arity of 2
const compose = (f, g) => (data) => f(g(data));
// This has an arity of 4
const funct2 = (f, g, h, i) => console.log(f, g, h, i);
Bringing All of This Together
Below uses the principles of Functional Programming to create a working shopping cart.
// Setup the user
const user = {
name: 'Kim',
active: true,
cart: [],
purchases: []
}
// The history1 variable will store user each step to keep a record
const history1 = [];
// Setup compose functionality
const compose = (f, g) = & gt;
(...args) = & gt;
f(g(...args))
// Use compose to set up the assembly line
const purchaseItem = (...fns) = & gt;
fns.reduce(compose);
// Fill the assembly line
purchaseItem(
emptyUserCart,
buyItem,
applyTaxToItems,
addItemToCart
)(user, {
name: 'laptop',
price: 50
});
// Record the initial user data,
// update the cart and then return a copy
// of the user object with the updated info
function addItemToCart(user, item) {
history1.push(user)
const updatedCart = user.cart.concat(item)
return Object.assign({}, user, {
cart: updatedCart
});
}
// Record the initial user data and
// then return the cart with the updated amount
function applyTaxToItems(user) {
history1.push(user)
const {
cart
} = user;
const taxRate = 1.3;
const updatedCart = cart.map(item = & gt; {
return {
name: item.name,
price: item.price * taxRate
}
})
return Object.assign({}, user, {
cart: updatedCart
});
}
// Record the initial user data and
// then return a copy of the object with the item added to the cart
function buyItem(user) {
history1.push(user)
const itemsInCart = user.cart;
return Object.assign({}, user, {
purchases: itemsInCart
});
}
// Record the initial user data and
// then return an empty copy of the object.
function emptyUserCart(user) {
history1.push(user)
return Object.assign({}, user, {
cart: []
});
}



















