Lecture 9: Programming Paradigms
Video 🔒
-
What are some examples of programming paradigms?
-
Is Java object-oriented, or functional? Why? What about JavaScript?
-
Why are most mainstream programming languages multi-paradigm?
Object-Oriented vs. Functional Programming
Here are two snippets of JavaScript implementing the same behavior:
-
What is this code doing?
-
How are the two versions different? How did we go from one to the other?
-
Which version is written in an object-oriented style and which is written in a functional style? Why?
-
Which parts of which version are better? Why?
-
How can we combine these versions to have the best of both worlds?
-
Suppose that we want to extend the game with
Lizard
andSpock
. Is it better to do that in an object-oriented or in a functional style? -
Suppose that we want to extend the game with a feature to randomly choose an option for the player. Is it better to do that in an object-oriented or in a functional style?
Key Points
Object-Oriented Programming
-
It couples the kinds of data with the actions you can perform on them, but breaks apart the parts of the actions.
-
It’s good for when you want to be able to come up with new kinds of data.
-
It isn’t as good for when you want to come up with new actions on the data.
-
The canonical use case for object-oriented programming is building user interfaces. That’s because we commonly want to change what components are part of the interface, but we rarely want to assign them new actions. For example, we may want add or remove a button from the interface, but we rarely want to change the kinds of actions a button supports—it can be clicked, hovered, and so forth, but that’s it.
Funcional Programming
-
It decouples the kinds of data from the actions you can perform on them, but keeps the parts of the actions in one place.
-
It isn’t good for when you want to be able to come up with new kinds of data.
-
It’s as good for when you want to come up with new actions on the data.
-
The canonical use case for functional programming is manipulating mathematical equations. You rarely want to change the kinds of equations your program can handle, because it’s rare that someone comes up with a new kind of operator or a new kind of number. But you commonly want to add new actions on those equations: finding the solution for a given variable, taking the derivative, and so forth.
Object-Oriented Programming in Depth
Typically people learn about object orientation in languages like Java, but there’s a lot more to it in other languages. We’ll continue to use JavaScript in this section, but many ideas here apply to other languages like Ruby, Python, Scala, and so forth.
Duck Typing
Consider our extension for choosing an option randomly:
class Rock {
toString() {
return "💎";
}
beats(otherPlayerChoice) {
return otherPlayerChoice instanceof Scissors;
}
}
class Paper {
toString() {
return "📄";
}
beats(otherPlayerChoice) {
return otherPlayerChoice instanceof Rock;
}
}
class Scissors {
toString() {
return "✂️";
}
beats(otherPlayerChoice) {
return otherPlayerChoice instanceof Paper;
}
}
function randomChoice() {
const randomNumber = Math.random();
if (randomNumber < 1 / 3) return new Rock();
if (randomNumber < 2 / 3) return new Paper();
if (randomNumber < 3 / 3) return new Scissors();
}
const player1Choice = randomChoice();
const player2Choice = randomChoice();
console.log(`Player 1 chose ${player1Choice}`);
console.log(`Player 2 chose ${player2Choice}`);
if (player1Choice.beats(player2Choice)) console.log("Player 1 wins!");
else if (player2Choice.beats(player1Choice)) console.log("Player 2 wins!");
else console.log("It’s a draw!");
-
If this were written in Java, what could be the type of
player1Choice
? -
How would this type affect the uses of
player1Choice
, for example, inplayer1Choice.beats(player2Choice)
? -
Why isn’t this a problem in JavaScript?
-
What are the advantages and disadvantages of duck typing?
Monkey Patching
For example:
class Rock {
toString() {
return "💎";
}
}
const rock = new Rock();
Rock.prototype.toAscii = function() {
return "rock";
};
rock.toString = function() {
return "🐒";
};
console.log(`This ${rock.toAscii()}s!`);
console.log(`This ${rock}s!?`);
And:
Array.prototype.sample = function() {
return this[Math.floor(Math.random() * this.length)];
};
console.log(["💎", "📄", "✂️"].sample());
-
Why would you want to monkey patch something?
-
Why is it better to avoid monkey patching?
Objects That Aren’t Instances of Classes
For example:
const rock = {
toString() {
return "💎";
}
};
console.log(`This ${rock}s!`);
-
How would you build something equivalent to this in Java?
-
Why would you want an object like this?
A Class is Just a Function That Constructs an Object
For example:
function Rock() {
return {
toString() {
return "💎";
}
};
}
console.log(`This ${Rock()}s!`);
-
The
Rock()
function acts as the constructor of theRock
class we had in the first snippet on this page. -
Why would you write code like this?
Classes are Values (Because They Really Are Just Functions, and Functions Are Values)
For example:
class Rock {}
class Paper {}
class Scissors {}
function randomChoice() {
const randomNumber = Math.random();
let ClassToInstantiate;
if (randomNumber < 1 / 3) ClassToInstantiate = Rock;
else if (randomNumber < 2 / 3) ClassToInstantiate = Paper;
else if (randomNumber < 3 / 3) ClassToInstantiate = Scissors;
return new ClassToInstantiate();
}
const player1Choice = randomChoice();
Mixins
For example:
class Rock {
toString() {
return "💎";
}
}
function toDoubleStringMixin(Base) {
return class extends Base {
toDoubleString() {
return `${this}${this}`;
}
};
}
const DoubleRock = toDoubleStringMixin(Rock);
const player1Choice = new DoubleRock();
console.log(player1Choice.toDoubleString());
-
Don’t get mixed up: If you look for mixins in JavaScript on the web, you’ll find two different explanations. We’re going for the one you find at the MDN, not the one using
Object.assign()
. Confusingly enough, in the React community mixins take this latter meaning, and what we’re calling mixins they call higher-order components (HOC). -
Why is multiple inheritance disallowed in most programming languages?
-
How do mixins solve this problem?
-
A mixin is like an abstract subclass, as opposed to an abstract superclass like those we have in Java. It’s as if we had written
abstract toString()
in the class returned bytoDoubleStringMixin()
. -
Mixins allow us to simulate multiple inheritance, just use something like
myOtherMixin(toDoubleStringMixin(Rock))
. -
Mixins allows us to exercise one of the most important principles of object-oriented programming: favor composition over inheritance (here’s React’s take on this principle, which is a really good explanation).
Classes Don’t Have Parents; Objects Have Prototypes
We didn’t cover this in lecture.
class Rock {
toString() {
return "💎";
}
}
class Paper {
toString() {
return "📄";
}
}
const rock = new Rock();
rock.__proto__ = Paper.prototype;
console.log(`This ${rock}s!?`);
-
This is the so-called prototype-based inheritance system in JavaScript.
-
Check these articles at the MDN: Inheritance and the prototype chain and Inheritance in JavaScript.
Functional Programming in Depth
We didn’t cover this in lecture.
Functions Are Values
-
The so-called higher-order functions, or lambdas.
-
Where did we use functions as values? In Java? In JavaScript?
Favor Immutability
-
In JavaScript, that is favor
const
instead oflet
. In Java, pretend variables arefinal
. -
Also, use immutable data structures and functional updates, for example:
const someChoices = ["💎"]; console.log(`Original ‘someChoices’: ${someChoices}`); someChoices.push("📄"); console.log(`‘someChoices’ after ‘push()’: ${someChoices}`); const allChoices = [...someChoices, "✂️"]; console.log(`‘someChoices’ after creating ‘allChoices’: ${someChoices}`); console.log(`And ‘allChoices’ itself: ${allChoices}`);
-
push()
is mutating, while[...someChoices, "✂️"]
is performing a functional update. -
Why is immutability good?
Avoid Side-Effects
For example:
class Rock {}
class Paper {}
class Scissors {}
function beats(thisPlayerChoice, otherPlayerChoice) {
if (thisPlayerChoice instanceof Rock)
return otherPlayerChoice instanceof Scissors;
if (thisPlayerChoice instanceof Paper)
return otherPlayerChoice instanceof Rock;
if (thisPlayerChoice instanceof Scissors)
return otherPlayerChoice instanceof Paper;
}
let beaten = false;
function beatsViaSideEffects(thisPlayerChoice, otherPlayerChoice) {
if (thisPlayerChoice instanceof Rock)
beaten = otherPlayerChoice instanceof Scissors;
else if (thisPlayerChoice instanceof Paper)
beaten = otherPlayerChoice instanceof Rock;
else if (thisPlayerChoice instanceof Scissors)
beaten = otherPlayerChoice instanceof Paper;
}
-
Why is
beatsViaSideEffects()
worse thanbeats()
? -
So mutation is an example of side-effects. What are other examples?
-
When are side-effects bad? When are they desired?
Idempotence
-
If side-effects are inevitable, try to make functions safe to run multiple times, that is, make them idempotent.
-
For example, see how we use
IF NOT EXISTS
when creating a table on the database. -
Why is this a good idea?
Other Paradigms
Different programming paradigms exist to help you communicate different ideas more expressively. My favorite uncommon paradigm is relational programming.