"A monad is just a monoid in the category of endofunctors..."
Because FP "laws" and tools enables us to write a more:
In general, a more reliable & maintainable code in the long term
But the journey has its bumps
We'll go over some "mandatory" concepts:
Some "mandatory" concepts:
The majority of patterns and tools around FP requires functions to be treated as first-class citizens
Which means they can:
// anonymous functions
const aFunction = function () {
console.log('hello fp');
};
// or named functions
const aFunction = function aFunctionName() {
console.log('hello fp');
};
// or arrow functions
const aFunction = () => console.log('hello fp');
// or even borrowed methods
const aFunction = someObj.someOtherFunction;
// With objects
const obj = {
methodAnon: function() { },
methodNamed: function aFunctionName() { },
methodArrow: () => { },
methodBorrowed: otherObj.someOtherFunction;
};
// Or with arrays
const arr = [
function() { },
function aFunctionName() { },
() => { },
otherObj.someOtherFunction
];
const hello = () => {
console.log('hello fp');
};
const callFn = fn => fn();
// ...
callFn(hello); // hello fp
const getHello = () => {
return () => {
console.log('hello fp');
};
};
// or the shorter
const getHello = () => () => console.log('hello fp');
// ...
const hello = getHello();
hello(); // hello fp
// or in one go
getHello()(); // hello fp
Some "mandatory" concepts:
A high-order function is a function that does at least one of the following:
const highOrderSecret = (fnArg) => {
const secret = 'FP rulez!';
fnArg(secret);
};
const logSecret = (secret) => console.log(secret);
const saveSecret = (secret) => secretStorage.add(secret);
// ...
highOrderSecret(logSecret); // FP rulez!
highOrderSecret(saveSecret);
Useful to separate concerns and abstract/decouple logic
const makeSecret = () => {
const secret = 'FP rulez!';
return () => secret; // Btw, this is a closure
};
const getSecret = makeSecret();
console.log(getSecret()); // FP rulez!
Useful to "hide" state (achieve privacy), persist state to be processed/used later and compose/add behaviour to other functions
A closure is a function that refers to "free variables" (variables defined in parent scopes)
In other words, it's a function that "remembers" the state/environment where it was created
// global scope
const makeSecret = () => {
// scope 0
const secret = 'FP rulez';
// following will log undefined because parent a scope
// does not have access to child scopes
console.log(secretSuffix); // ReferenceError: secretSuffix is not defined
return () => {
// scope 1
const secretSuffix = '!!!!!';
return secret + secretSuffix;
};
};
console.log(secret); // ReferenceError: secret is not defined
const getSecret = makeSecret();
// It remembers its own scope plus parent scopes
console.log(getSecret()); // FP rulez!!!!!
Some "mandatory" concepts:
A function is considered pure if it does not break the following "laws":
const add = (a, b) => a + b;
const getCircleArea = r => Math.PI * r * r;
const getFullName = (first, last) => `${first} ${last}`;
const logUserIn = user => Object.assign(
{},
user,
{ loggedIn: true }
);
// I/O operation
const logMsg = msg => console.log(msg);
// Different outputs, same input
const getRandom = (max) => Math.random() * max;
// depends on mutable state
const getFullName = (first, last) =>
`${globalNamePrefix} ${first} ${last}`;
// Mutating object state
const logUserIn = user => user.loggedIn = true;
A program without any observable side effect is also a program that accomplishes nothing useful
but, side effects should be avoided where possible
as they make programs hard to follow/read, hard to test and hard to maintain
most of a program codebase should be composed of small, single-purpose and pure functions
Some "mandatory" concepts:
Question: what's the difference between arguments and parameters?
// firstName, middleName and lastName are parameters
const getFullName = (firstName, middleName, lastName) =>
`${firstName} ${middleName} ${lastName}`;
// All strings passed into getFullName() call are arguments
getFullName('Allan', 'Marques', 'Baptista');
// arguments < parameters - perfectly valid in JS
getFullName('Emperor', 'Palpatine');
// arguments > parameters - also valid
getFullName('Some', 'Big', 'Ass', 'Freaking', 'Name');
Parameter is the variable which is part of the function signature
Argument is the value/variable/reference/expression being passed in during a function call
The number of parameters a function expects in its signature is called arity
const double = n => n * 2; // arity = 1 (unary)
// arity = 3 (ternary)
const getFullName = (firstName, middleName, lastName) =>
`${firstName} ${middleName} ${lastName}`;
It's possible to get a function's arity through the Function.prototype.length
property
const double = n => n * 2;
console.log(double.length); // 1
By combining the power of high-order functions (HoF), knowledge of function arity and loose arguments application, we can build powerful abstractions
Sometimes we need to ensure a function that expects more than one parameter to receive only one argument
const strArr = ['1', '2', '3', '4', '5'];
const mumArr = strArr.map(parseInt);
console.log(numArr); // [1, NaN, NaN, NaN, NaN]
That happens because parseInt's signature is:parseInt(str [, radix])
And Array.prototype.map
calls any function passed in with the arguments:fn(item, index, arr)
We can fix that with a utility HoF usually called unary
That can be implemented in JS like so:
const unary = fn =>
param => fn(param);
And used like this:
const strArr = ['1', '2', '3', '4', '5'];
const mumArr = strArr.map(unary(parseInt));
console.log(numArr); // [1, 2, 3, 4, 5]
Calling a function and passing some arguments to it like:foo(bar, baz);
can also be described as applying function foo to the arguments bar and baz
Means fixing/binding a number of arguments to a function producing another function with smaller arity
It's useful when we know some of the arguments that'll be applied to a function ahead of time
But the rest of the arguments we'll only know at a later point in execution time.
A partial function application utility can easily be implemented like so:
const partial = (fn, ...eagerArgs) =>
(...lazyArgs) => fn(...eagerArgs, ...lazyArgs);
And it's used like this:
const fullName = (preferedTreatment, firstName, lastName) =>
`${preferedTreatment} ${lastName}, ${firstName}`;
const maleName = partial(fullName, 'Sir');
const femaleName = partial(fullName, 'Ma\'am');
maleName('Allan', 'Baptista'); // Sir Baptista, Allan
femaleName('Nadia', 'Carvalho'); // Ma'am Carvalho, Nadia
It's also possible to implement a utility that partially applies the final arguments like so:
const partialRight = (fn, ...rightArgs) =>
(...leftArgs) => fn(...leftArgs, ...rightArgs);
That can be used like this:
const fullName = (preferedTreatment, firstName, lastName) =>
`${preferedTreatment} ${lastName}, ${firstName}`;
const kirk = partialRight(fullName, 'James', 'Kirk');
kirk('Sir'); // Sir Kirk, James
kirk('Captain'); // Captain Kirk, James
It's creating a function that only executes its actual logic once it has gathered all parameters it expects
When a curried function is applied to less arguments than its arity it returns another function
And it keeps returning another function until all arguments are provided
const curriedFullName = preferedTreatment =>
firstName =>
lastName =>
`${preferedTreatment} ${lastName}, ${firstName}`;
const getName = curriedFullName('Mr'); // preferedTreatment = 'Mr'
const getLastName = getName('James'); // firstName = 'James'
getLastName('Bond'); // Mr. Bond, James
// or in one go
curriedFullName('Sir')('Leonard')('Nimoy'); // Sir Nimoy, Leonard
In Haskell all functions are curried by default, but in javascript we need to write a utility function to achieve the same
const autoCurry = (fn, arity = fn.length) =>
(...args) =>
args.length >= arity ?
fn(...args) :
autoCurry(partial(fn, ...args), arity - args.length);
const curriedFullName = autoCurry(
(preferedTreatment, firstName, lastName) =>
`${preferedTreatment} ${lastName}, ${firstName}`
);
const getName = curriedFullName('Mr'); // preferedTreatment = 'Mr'
const getLastName = getName('James'); // firstName = 'James'
getLastName('Bond'); // Mr. Bond, James
// or
curriedFullName('Sir')('Leonard')('Nimoy'); // Sir Nimoy, Leonard
// or
curriedFullName('Sir')('Rowan', 'Atkinson'); // Sir Atkinson, Rowan
// or
curriedFullName('Mr', 'Mickey', 'Mouse'); // Mr Mouse, Mickey
Note that the strict implementation of currying produces only unary functions after each call
So the implementation showed here should be called loose currying, which is often more useful
Some "mandatory" concepts:
When a program/application is well split into simple, single-purpose and pure functions a repeating pattern starts to come up:
const outputData = freeze(enhance(escape(inputData)));
And to avoid repetition, it's common to create composed abstractions:
const transformData = data => freeze(enhance(escape(data)));
// later somewhere...
const outputData = transformData(inputData);
// and even later...
const dataToPersist = transformData(inputData);
What if there was a way to achieve the same thing in a declarative way?
const transformData = compose(freeze, enhance, escape);
// later somewhere...
const outputData = transformData(inputData);
compose(...fns)
takes a list of functions
and returns another function that applies each function from right to left, so:
// This
const transformData = compose(freeze, enhance, escape);
transformData(...args);
// is the same as this
const escaped = escape(...args);
const enhanced = enhance(escaped);
const outputData = freeze(enhanced);
// or this
const outputData = freeze(enhance(escape(...args)));
One can implement compose
in JS like so:
const compose = (...fns) =>
(...args) => fns
.slice(0, -1)
.reduceRight(
(res, fn) => fn(res),
fns[fns.length - 1](...args)
);
Note that all functions besides the first one to be applied are expected to be unary
as it's not possible to return more the one value from a function
By combining the concepts of function composition, arity and input management one can build very complex logic in a very declarative way
Sometimes reading the flow of data from right to left can be counter-intuitive
to fix that, we can build a variation of compose
that applies each function from left to right
that variation is usually called pipe or pipeline
const transformData = pipe(escape, enhance, freeze);
// later somewhere...
const outputData = transformData(inputData);
pipe
can be implemented like so:
const pipe = (firstFn, ...restFns) =>
(...args) => restFns.reduce(
(res, fn) => fn(res),
firstFn(...args)
);
Other than the difference on how data flows compose
and pipe
works in the same way
(Except this implementation o pipe
is a little bit more performant than compose
's implementation showed before)
Some "mandatory" concepts:
In javascript (and the majority of hybrid/OO languages) immutability is usually not natively enforced on objects
Some may naively think assigning objects with the const
keyword prevents objects from being mutated
const config = { cannotChange: 'Never changed' };
config.cannotChange = 'Chaos';
console.log(config); // { cannotChange: 'Chaos' }
// but the following throws a TypeError
config = { cannotChange: 'Invalid' };
But in fact, const
only prevents the variable from being re-assigned
So, if immutability is not enforced natively by the language, how do we achieve it?
// Very bad
const logIn = user => {
user.loggedIn = true;
return user;
};
const loggedUser = logIn(anonymousUser);
console.log(loggedUser.loggedIn); // true
console.log(anonymousUser.loggedIn); // true
// Good
const logIn = user => {
const userCopy = Object.assign({}, user);
userCopy.loggedIn = true;
return userCopy;
};
const loggedUser = logIn(anonymousUser);
console.log(loggedUser.loggedIn); // true
console.log(anonymousUser.loggedIn); // false
Pattern: copy objects and mutate the copy
// Very bad
const addTask = (taskList, task) => {
taskList.push(add);
return taskList;
};
const newTaskList = addTask(taskList, task);
console.log(newTaskList.length); // 10
console.log(taskList.length); // 10
// Good
const addTask = (taskList, task) => {
// or [...taskList, task];
return taskList.concat(task);
};
const newTaskList = addTask(taskList, task);
console.log(newTaskList.length); // 10
console.log(taskList.length); // 9
Pattern: avoid mutable methods (push, pop, shift, unshift, splice, sort, fill, reverse)
instead use immutable methods (concat, slice, map, filter) or the spread notation
Object.freeze
freezes an object, preventing it from being mutated (works w/ arrays as well)
const user = Object.freeze({ name: 'Elza' });
user.name = 'Evil'; // throws if in 'strict mode'
console.log(user.name); // Elza
Pattern: combine Object.freeze
with other immutable patterns to achieve full immutability
Note that Object.freeze
only freezes objects shallowly
const user = Object.freeze({
name: {
first: 'Elza',
last: 'Arendelle'
}
});
user.name.first = 'Evil';
// { first: 'Evil', last: 'Arendelle' }
console.log(user.name);
So to achieve full immutability all child objects also need to be frozen
Note that these patterns are much less performant than its mutable counterpart
Even more if we're dealing with deep nested objects
If you need immutability as well as performance maybe it's time to bring a library in
But if performance is still an issue, you should think about replacing parts of your code with mutable patterns
but remember:
"Premature optimization is the root of all evil"
- Donald Knuth
Some "mandatory" concepts:
const activeItems = [];
for (let i = 0; i < arr.length; i++) {
if (arr[i].active === true) {
activeItems.push(arr[i]);
}
}
// vs
const activeItems = arr.filter(item => item.active === true);
Array methods are usually better because:
Loops are better when:
map()
Array.prototype.map
is a HoF that traverses the list applying the provided operator function to each item
and produces a new array with the values returned from each operator call
const bananas = ['🍌', '🍌', '🍌', '🍌', '🍌', '🍌'];
const mix = bananas.map((banana, index) => (
index % 2 === 0 ? '🍎' : banana
));
console.log(mix); // ['🍎', '🍌', '🍎', '🍌', '🍎', '🍌']
map()
Source: https://github.com/getify/Functional-Light-JS/blob/master/ch8.md
In FP terminology, a Functor is a wrapper object that has a utility method for applying an operator function to its wrapped value
returning a new Functor wrapping the new value produced by the operator
If the wrapped value is compound the Functor applies the operator to each indidual value instead
All this is just a fancy way of saying that Functor is just an object that has a map method
filter()
Array.prototype.filter
is a HoF that traverses the list applying the provided predicate function to each item
and produces a new array with the values of which the predicate function returned truthy
const badDiet = ['🍌', '🍫', '🍎', '🍫', '🥕', '🍫', '🍉', '🍫'];
const goodDiet = badDiet.filter(food => !food.includes('🍫'));
console.log(goodDiet); // ['🍌', '🍎', '🥕', '🍉']
filter()
Source: https://github.com/getify/Functional-Light-JS/blob/master/ch8.md
reduce()
Array.prototype.reduce
is a HoF that traverses the list applying the provided reducer function to the previous returned value and current value
And produces whatever the last reducer call returns
const people = ['👩', '👧', '👨', '👦'];
const family = people.reduce((str, person) => (
str === '' ?
person :
str + '\u200D' + person
), '' /* <- initial value */);
console.log(family); // '👩👧👨👦'
reduce()
Source: https://github.com/getify/Functional-Light-JS/blob/master/ch8.md
Some "mandatory" concepts:
Recursion is when a function calls itself until a base condition is satisfied
const fib = n =>
n <= 1 ?
n :
fib(n - 2) + fib(n - 1);
fib(10); // 55
Although it may be less performant, expressing repetition with recursion is usually more readable because of its declarative nature
const sum = (...values) => {
let total = 0;
for(let i = 0; i < values.length; i++) {
total += values[i];
}
return total;
};
// vs
const sum = (firstValue, ...otherValues) =>
otherValues.length === 0 ?
firstValue :
firstValue + sum(...otherValues);
"Loops may achieve a performance gain for your program. Recursion may achieve a performance gain for your programmer. Choose which is more important in your situation!"
- Leigh Caldwell
An common strategy to apply when creating a recursive functions is taking the divide and conquer approach:
Iterative:
const map = (arr, fn) => {
const newArr = [];
for (let i = 0; i < arr.length; i++) {
newArr.push(fn(arr[i]));
}
return newArr;
};
Recursive:
const map = ([firstVal, ...rest], fn) =>
firstVal === undefined ?
[] :
[fn(firstVal), ...map(rest, fn)];
Note that this technique doesn't only work with lists. Lists are just easier to wrap you head around the concept
This approach can be applied to almost anything
const iterativeRangeSum = (start, end) => {
let result = start;
for (let i = start; i < end; i++) {
result += i;
}
return result;
}
// vs
const recursiveRangeSum = (start, end) =>
start === end ?
0 :
start + recursiveRangeSum(start, end - 1);
Stack Overflow
The function stack is a limited resource
If the base condition of a recursive function is not met until the environment runs out of stack frames
The program/application will crash and burn
// If list has something like 200 items or more,
// Stack Overflow!
const values = recursiveMap(bigList, item => item.value);
Always ensure the base condition will be satisfied before that or refactor the function to use the benefits of tail call optimization
Tail call is when a function call is the very last thing evaluated inside a function
const foo = () => {
const value = 'tail call';
return bar(value); // <- bar is being tail called
};
When this happens the compiler can optimize the runtime by reusing the last stack frame
By refactoring the recursive map utility from:
const map = ([firstVal, ...rest], fn) =>
firstVal === undefined ?
[] :
[fn(firstVal), ...map(rest, fn)];
To use TCO:
const map = ([firstVal, ...rest], fn, result = []) =>
firstVal === undefined ?
result :
map(rest, fn, [...result, fn(firstVal)]);
map()
will now support mapping over lists of any size
Some "mandatory" concepts:
But first, some boring topics that won't be covered:
(but you should at some point)
Some people say these fundamental topics are mandatory to start FPing
I disaggree. But when you feel ready to dive into the theory behind FP, it's' recommended you do so
Meet something and nothing:
const something = (value) => ({
map: fn => something(fn(value))
});
const nothing = () => ({
map: nothing
});
Note: the names of this Functor implementations vary a lot
(Some/None, Just/Nothing, Something/Empty...)
That happens because Functors, like any other FP type, is like a loose interface
Implementations must respect the type laws but their names are not enforced
something is useful
const getUser = userId => {
const user = repository.findUserById(userId);
return user ? something(user) : nothing();
}
// now we can write
// beautiful, idiomatic, declarative code
getUser(existingId) // User exists
.map(attachPermissions(permissions))
.map(attachOrders(orders))
.map(showProfile(template));
nothing is useful
const getUser = userId => {
const user = repository.findUserById(userId);
return user ? something(user) : nothing();
}
// now we can write
// beautiful, idiomatic, declarative code
getUser(nonExistantId) // User is not found
.map(attachPermissions(permissions))
.map(attachOrders(orders))
.map(showProfile(profileTemplate));
It's the same exact code, but nothing happens. Not even an exception!
Maybe?
When we use the Something and Nothing Functors together in a function like this, we're actually implementing the Maybe Functor.
It allows easier and safer null/empty checks without sacrificing readability.
handling branches/errors
const success = (value) => ({
map: fn => something(fn(value)),
catch: nothing
});
const error = (e) => ({
map: () => error(e),
catch: fn => something(fn(e))
});
handling branches/errors
const getUser = userId => {
const user = repository.findUserById(userId);
return user ? success(user) : error(new Error('User not found'));
}
// now we can write
// beautiful, idiomatic, declarative code
getUser(nonExistantId) // User is not found
.map(attachPermissions(permissions))
.map(attachOrders(orders))
.map(showProfile(profileTemplate))
// error branch executed when is error
.catch(showError(errorTemplate));
Either?
When we use the Error and Success together in a function like this, we're actually implementing the Either Functor.
Its strict implementation is more complex, but the core concept is the same.
In the strict implementation of the Either Functor, the Error and Success Functors are often called Left and Right respectively.
Containing containers
const attachPermissions = permissions => user =>
permissions ?
something(user.setPermissions(permissions)) :
nothing();
const attachOrders = orders => user =>
orders ?
something(user.setOrders(orders)) :
nothing();
getUser(nonExistantId)
.map(attachPermissions(permissions)) // something(something(user))
.map(attachOrders(orders))// Error: setOrders is not a function
.map(showProfile(profileTemplate))
.catch(showError(errorTemplate));
Much like the Functor, the Monad has a utility method for applying an operator function to its wrapped value
But unlike the Functors map()
utility, it does not wrap the output value in a new monad
Because it expects the value to be already wrapped in a monad
This utility function has many names: bind()
, chain()
and flatMap()
Unboxing a box
const something = (value) => ({
map: fn => something(fn(value)),
flatMap: fn => fn(value)
});
const nothing = () => ({
map: nothing,
flatMap: nothing
});
Unboxing a box
const success = (value) => ({
map: fn => something(fn(value)),
flatMap: fn => fn(value)
catch: nothing
});
const error = (e) => ({
map: () => error(e),
flatMap: () => error(e),
catch: fn => something(fn(e))
});
Unboxing a box
const attachPermissions = permissions => user =>
permissions ?
something(user.setPermissions(permissions)) :
nothing();
const attachOrders = orders => user =>
orders ?
something(user.setOrders(orders)) :
nothing();
getUser(nonExistantId)
.flatMap(attachPermissions(permissions))// something(user)
.flatMap(attachOrders(orders))// something(user)
.map(showProfile(profileTemplate))
.catch(showError(errorTemplate));
Let's create a fictional monad that wraps a future value
const future = futureValue => ({
map: fn => future(fn(futureValue)),
flatMap: fn => fn(futureValue)
});
That can be used like this:
future(asyncGetUser(userId))
.flatMap(asyncAttatchPermissions(userId))
.flatMap(asyncAttatchOrders(userId))
.map(showProfile(profileTemplate));
But map()
and flatMap()
are not very meaningful when dealing with future values
What if we "merged" and renamed them?
const future = futureValue => ({
then: fn => {
const nextFutureValue = fn(futureValue);
const isFutureMonad = (
nextFurureValue &&
typeof nextFutureValue.then === 'function'
);
return isFutureMonad ?
nextFutureValue :
future(nextFutureValue);
}
});
Now it reads a lot better:
future(asyncGetUser(userId))
.then(asyncAttatchPermissions(userId))
.then(asyncAttatchOrders(userId))
.then(showProfile(profileTemplate));
Feeling a déjà vu?
Promise.resolve(asyncGetUser(userId))
.then(asyncAttatchPermissions(userId))
.then(asyncAttatchOrders(userId))
.then(showProfile(profileTemplate));
Yes! Promise is a Monad!
const words = [
'The', 'quick', 'brown', 'fox,', 'jumps', 'over',
'the', 'lazy', 'dog.', '- It', 'was', 'a', 'german',
'shepherd!'
];
let result = false;
for (let i = 0; i < words.length; i++) {
words[i] = words[i].replace(/[ -_:;.,!\?]/g, '');
if (words[i].length >= 3 && words[i].length <= 6) {
words[i] = words[i].toUpperCase()
.split('').reverse().join('');
if (words[i] === 'GOD') {
result = true;
break;
}
}
}
if (result) console.log('Found GOD...');
if (words[0] !== 'The') console.log('...and the devil');
const words = [
'The', 'quick', 'brown', 'fox,', 'jumps', 'over',
'the', 'lazy', 'dog.', '- It', 'was', 'a', 'german',
'shepherd!'
];
const removeInvalidChars = word => word.replace(/[ -_:;.,!\?]/g, '');
const enoughChars = word => word.length >= 3;
const canContainGod = word => word.length >= 3 && word.length <= 6;
const toUpperCase = word => word.toUpperCase();
const reverseStr = word => word.split('').reverse().join('');
const toBibleCode = compose(reverseStr, toUpperCase);
const isGod = word => word === 'GOD';
const logIf = (condition, str) = condition && console.log(str);
const result = words
.map(removeInvalidChars)
.filter(canContainGod)
.map(toBibleCode)
.some(isGod);
logIf(result === true, 'Found GOD...');
logIf(words[0] !== 'The', '...and the devil');
Challenge: Can you spot a difference between both outputs?
I'm out