Introduction
Whenever I come across a cool concept in Computer Science, I try to think how to use it in real life scenarios.
I've recently read Kyle Simpson's highly recommended book "Functional-Light JavaScript" that manages somehow to be thorough, innovative and fun to read at the same time.
Near the end of the book Simpson discusses Monads, and demonstrates the concept with a cute example that is also a parable about the importance of humility and knowledge sharing.
While I found this lesson valuable, I tried to come up with an example that could be useful in a real project.
What is a Monad
According to Simpson, a monad is a set of behaviors that makes working with a value more predictable.
Predictable code is easier for others (and for our future selves) to understand and predict what it will do.
As a result, it is less probable to surprise us with unexpected results (== bugs).
Monads help us write predictable code by enforcing functional programming principles like immutability, pure functions and composition.
Monad building blocks
For my example I'm using the following monads as building blocks to create other monads from.
Just monad
This is a basic monad that many other monads build on
const Just = (val) => {
return {
map: (fn) => Just(fn(val)),
chain: (fn) => fn(val),
ap: (monad) => {monad.map(val)}
}
}
It's an object with a value and 3 methods:
-
map
accepts a function, calls it with the value, and creates a new Just monad that its value is the result -
chain
accepts a function, calls it with a value and returns the result as is. -
ap
function accepts a monad and executes the other monad's map function with the value.
Confused? Check the game example below to see it in action :)
Nothing Monad
This is a monad that has the same interface as Just monad, but all of the methods return a Nothing monad.
const Nothing = (val) => {
return {
map: (fn) => Nothing(),
chain: (fn) => Nothing(),
ap: (monad) => Nothing()
}
}
In the following example I will use a popular construct called 'Maybe' that switches between the Just monad and the Nothing monad to implement conditional behavior in a readable and reliable way.
Game example
This example simulates a game between two players.
I'm using the Maybe construct to make sure players score does not change after they have been removed from the game.
I add to the Just and Nothing Monads a 'get' method in order to gain access to the player's score and strikes after the game is over.
// this function is used to break down a function into successive
// chained functions that each take a single argument and return
// another function to accept the next argument.
const curry = (f) => {
return function(a) {
return function(b) {
return f(a, b);
};
};
}
// define the utility Monads
const Just = (val) => {
return {
map: (fn) => Just(fn(val)),
chain: (fn) => fn(val),
ap: (monad) => {monad.map(val)},
get: () => {return val}
}
}
const Nothing = (val) => {
return {
map: (fn) => Nothing(val),
chain: (fn) => Nothing(val),
ap: (monad) => Nothing(val),
get: () => {return val}
}
}
const Maybe = {Nothing, of: Just};
// logs the player's stats
// @param {object} p - the player object
const logPlayerSummary = (player) => {
console.log(`${player.name} won ${player.score} times and lost ${player.strikes} times`);
}
const logGameSummary = (player1, player2) => {
logPlayerSummary(player1);
logPlayerSummary(player2);
if (player1.score === player2.score) {
console.log('the game is a draw.');
} else {
const winner = player1.score > player2.score ? player1 : player2;
console.log(`${winner.name} won the game!`)
}
}
// increases the player's score
// @param {object} p - the player object
// @returns {object} the updated player after the increase
const win = (p) => {
const winner = {...p};
winner.score +=1;
console.log(`${winner.name} wins`);
return winner;
}
// increases the player's strikes
// @param {object} p - the player object
// @returns {object} the updated player after the increase
const lose = (p) => {
const loser = {...p};
loser.strikes += 1
return loser;
}
// checks if the player is still in the game
// @param {object} p - the player object
// @returns Just if true and Mothing if false
const isInGame = (p) => {
if (p.strikes < 3) {
return Maybe.of(p);
} else {
return Maybe.Nothing(p);
}
}
// @returns {number} a random number between 0 and 1
const flipCoin = () => {
return Math.random();
}
// define the players.
// For this example I'll use just 2 players,
// but it should work with any number.
let player1Monad = Just({
name: 'Salvor',
score: 0,
strikes:0
});
let player2Monad = Just({
name: 'Fara',
score: 0,
strikes:0
});
// In a real life scenario the game logic could be more complicated
// and have many stages
for (let i = 0; i < 10; i++) {
if (flipCoin() > 0.5) {
player1Monad = player1Monad.chain(isInGame).map(win);
player2Monad = player2Monad.chain(isInGame).map(lose);
} else {
player2Monad = player2Monad.chain(isInGame).map(win);
player1Monad = player1Monad.chain(isInGame).map(lose);
}
}
//now we are after the game, so we can "revive" the Nothing players
player1Monad = Just(player1Monad.get());
player2Monad = Just(player2Monad.get());
// Show final stats
player1Monad.map(curry(logGameSummary)).ap(player2Monad);
Game example explained
In this example I represent a coin flip competition between two players: Salvor and Fara.
The game has 10 rounds. In each round, if the result is greater than 0.5 Salvor wins and if lower Fara.
Whenever one player wins, the other one loses.
After 3 losses the player strikes out and its score and strikes no longer change.
At the end of the game the score and strikes of both players are logged to the console.
When a player wins or loses, there is an intermediate stage:
player1Monad = player1Monad.chain(isInGame).map(win);
isInGame
function is called with player1Monad's value (using chain
) and if the player didn't exceed the allowed number of strikes it returns a new just monad with the same value.
Then the function 'win' is called with player1Monad and returns a new Monad with the updated score (using 'map').
If the player just striked out, isInGame
returns a Nothing Monad, so the 'map' function also returns a nothing Monad with unchanged value.
In future iterations, the striked out player will also get a Nothing Monad, because both 'chain' and 'map' will always return Nothing.
Pay attention that while I store the number of strikes on the player object, it would work just as well if the striking out were an event that wasn't stored, e.g. the game was a dice instead of a coin flip and the first player to get one was removed from the game.
After the player would get 1, she would have become nothing, and no further checks would have been required.
At the end of the game I need to extract the players stats in order to display the total score.
This could be a problem if the players are nothing.
In order to overcome this problem, I use the get
method to create new Just monads with the players' score.
The logGameSummary
is a function that takes the stats of both players and displays the game summary.
player1Monad.map(curry(logGameSummary)).ap(player2Monad);
In order to work with the values of both monads, I'm using the method ap
that executes the value of one monad with the value of the other monad and returns a new monad.
For ap
method to work, the value of one of the monads must be a function.
To accomplish this I'm using the curry
function.
It is a very useful function in FP, and if you don't know it I recommend looking it up.
It breaks down logGameSummary
into successive chained functions, that one of them takes the first player's stats and returns another function to accept the next player's stats.
This function calls logGameSummary
with both players' stats.
Summary
In this post I've contrived a usage example of the Maybe Monad that could be integrated into a game app.
While there are many ways to implement this simple game, this approach has some advantages.
- It's immutable
- It's relatively short
- You don't have to maintain state of which players are active and check it all the time.
I've learned a lot from writing this example, and now I humbly share it with you.
I hope you enjoy it, and will be happy to read your thoughts on the subject.
Top comments (0)