Table of Contents #
- Functional Programming
- Functional Programming
- Concepts of functional programming
- Function composition
- Downsides
- References
Functional Programming #
Functional Programming #
What it is #
- Programming paradigm
- It's a way to think about structural code
- Tech savvy dialect
Little bit of history #
- Invented by some mathematicians in the 60's
- First programming language was Lisp
- One of the first languages to have garbage collector
Changing of paradigms of programming languages #
- With ES6, JS have introduced a lot of functional programming conceps to it
.map
.reduce
.filter
- React: influencing a whole new field of libraries and developers to be functional
- Some programming languages started adopting some methods to be multi-paradigm
- Java
- Rust
- Kotlin
- Swift
Concepts of functional programming #
Everything is a function #
- No classes, only functions
- Why? Easier to extend and easier to fix: small functions, small contexts, small responsabilities
- Everything should receive a data and return a data: input -> output
Declarative code #
What's de diference between these methods in JS?
every()
filter()
flatMap()
map()
indexOf()
reduce()
reduceRigth()
reverse()
slice()
some()
splice()
We could implement all these methods with a for
loop or the forEach()
method,
they all seem so redundant…
Examples #
-
Imperative
const dataModified = [ { name: 'bar', value: 100 }, { name: 'bar', value: 101 } ] for (const item of dataModified) { item.id = `${item.name}-${item.value}` } console.log(dataModified)
-
Functional
const dataModified = [ { name: 'bar', value: 100 }, { name: 'bar', value: 101 } ] const result = dataModified.map(item => ({ ...item, id: `${item.name}-${item.value}` })) console.log(result)
Immutability #
- You should always treat data as immutable, once created, it cannot be changed
- Reduces risk of side effects
- Allows easier parallelization on the same data
Examples #
-
With side effects
const proPlayer = { name: 'Flash', game: 'Starcraft: Brood War', race: 'Terran', matches: { wins: 779, losses: 297 } } function winRate(player) { return new Promise(resolve => { setTimeout(() => { const games = player.matches.wins + player.matches.losses const winRate = player.matches.wins / games console.log(`Player ${player.name} has a win rate of ${winRate * 100}`) resolve() }, 200) }) } function lostMatch(player) { return new Promise(resolve => { setTimeout(() => { console.log('Player has lost a match') player.matches.losses += 1 resolve() }, 100) }) } winRate(proPlayer) .then(() => Promise.all([ winRate(proPlayer), lostMatch(proPlayer), ]))
-
With Immutability
const proPlayer = { name: 'Flash', game: 'Starcraft: Brood War', race: 'Terran', matches: { wins: 779, losses: 297 } } function winRate(player) { return new Promise(resolve => { setTimeout(() => { const games = player.matches.wins + player.matches.losses const winRate = player.matches.wins / games console.log(`Player ${player.name} has a win rate of ${winRate * 100}`) resolve() }, 200) }) } function lostMatch(player) { return new Promise(resolve => { setTimeout(() => { console.log('Player has lost a match') const newData = { ...player, matches: { ...player.matches, losses: player.matches.losses + 1 } } resolve(newData) }, 100) }) } winRate(proPlayer) .then(() => Promise.all([ winRate(proPlayer), lostMatch(proPlayer), ]))
Lazy evaluation #
- It's a mechanism that delays the evaluation of an expression until its value is needed
- With this, functional languages are able to create infinite lists, which would not normally be available in an imperative language
Examples #
const rand = function * () {
while (true) {
yield Math.random()
}
}
const randIter = rand()
console.log(randIter.next())
console.log(randIter.next())
console.log(randIter.next())
Recursion #
See Sergio's presentation about it
- Functional programming languages doesn't have loops: for, while, do while
- They iterate using recursion
- That's way their garbage collector is totally diferent from JS and that's why it needs to be lazy loaded
Example #
function factorial(number) {
if (number === 0) return 1
return number * factorial(number - 1)
}
console.log(factorial(3))
Pure functions #
- Functions shouldn't have any reference outside of it's scope
- The function should always have the same output for the same input
Examples #
-
Impure
const newBand = { name: 'Black Country, New Road', members: ['Tyler', 'Lewis', 'Georgia', 'May', 'Charlie', 'Luke', 'Isaac'], albums: ['Ants from Up There'] } const newBand = { name: 'Black Country, New Road', members: ['Tyler', 'Lewis', 'Georgia', 'May', 'Charlie', 'Luke', 'Isaac'], albums: ['Ants from Up There'] } function releaseRecord(recordName) { newBand.albums.push(recordName) } function removeMember(memberName) { const index = newBand.members.indexOf(memberName) newBand.members.splice(index, 1) } removeMember('Isaac') console.log(newBand) releaseRecord('Live at Bush Hall') console.log(newBand)
-
Pure
const newBand = { name: 'Black Country, New Road', members: ['Tyler', 'Lewis', 'Georgia', 'May', 'Charlie', 'Luke', 'Isaac'], albums: ['Ants from Up There'] } const releaseRecord = (band, recordName) => ({ ...band, albums: [...band.albums, recordName] }) const removeMember = (band, memberName) => ({ ...band, members: band.members.filter(member => member !== memberName) }) const bandWithoutIsaac = removeMember(newBand, 'Isaac') console.log(bandWithoutIsaac) const bandWithANewRecord = releaseRecord(bandWithoutIsaac, 'Live at Bush Hall') console.log(newBand) console.log(bandWithANewRecord)
High-order functions #
See Vandre's presentation about closure
- Function that return a function to be executed later on
Example #
const kingGizzard = {
name: 'King Gizzard and the Lizard Wizard',
genres: [
'garage rock',
'rock',
'psychedelic rock',
'jazz fusion',
'heavy psychedelic rock',
'progressive rock',
'syth pop',
'thrash metal'
]
}
const kingGizzard = {
name: 'King Gizzard and the Lizard Wizard',
genres: [
'garage rock',
'rock',
'psychedelic rock',
'jazz fusion',
'heavy psychedelic rock',
'progressive rock',
'syth pop',
'thrash metal'
]
}
function validateBandGenre(band) {
return function(genre) {
return band.genres.includes(genre)
}
}
// or using arrow function ===> const validateBandGenre = band => genre => band.genres.includes(genre)
const validateKingGizzardGenres = validateBandGenre(kingGizzard)
console.log(
validateKingGizzardGenres('rock')
)
console.log(
validateKingGizzardGenres('pop')
)
Function composition #
- Function that compose based on another function to create a new behavior
- There's a lot of utility libraries for JS:
- Lodash FP module: doesn't have a lot of utilities
- Ramda JS: has tons of functions and is focused in functional programming
Pipe #
-
Combine functions to be executed in order
-
The output of one function is the input to the next until there's all functions are executed
-
High-order function that returns a function that has the initial param to be of the first function and returns the output of the last function
function pipe(...fns) { return function(value) { return fns.reduce((acc, fn) => fn(acc), value) } }
Examples #
-
Without pipe
const newBand = { name: 'Black Country, New Road', members: ['Tyler', 'Lewis', 'Georgia', 'May', 'Charlie', 'Luke', 'Isaac'], albums: ['Ants from Up There'] } const releaseRecord = recordName => band => ({ ...band, albums: [...band.albums, recordName] }) const removeMember = memberName => band => ({ ...band, members: band.members.filter(member => member !== memberName) }) const bandWithoutIsaac = removeMember('Isaac')(newBand) console.log(bandWithoutIsaac) const bandWithANewRecord = releaseRecord('Live at Bush Hall')(bandWithoutIsaac) console.log(newBand) console.log(bandWithANewRecord)
-
With pipe
function pipe(...fns) { return function(value) { return fns.reduce((acc, fn) => fn(acc), value) } } const newBand = { name: 'Black Country, New Road', members: ['Tyler', 'Lewis', 'Georgia', 'May', 'Charlie', 'Luke', 'Isaac'], albums: ['Ants from Up There'] } const releaseRecord = recordName => band => ({ ...band, albums: [...band.albums, recordName] }) const removeMember = memberName => band => ({ ...band, members: band.members.filter(member => member !== memberName) }) const finalLineup = pipe(removeMember('Isaac'), releaseRecord('Live at Bush Hall')) console.log( finalLineup(newBand) )
Compose #
-
Same as
pipe
, but the order of execution is from right to leftfunction pipe(...fns) { return function(value) { return fns.reduce((acc, fn) => fn(acc), value) } } const newBand = { name: 'Black Country, New Road', members: ['Tyler', 'Lewis', 'Georgia', 'May', 'Charlie', 'Luke', 'Isaac'], albums: ['Ants from Up There'] }
function compose(...fns) { return pipe(...fns.reverse()) }
const releaseRecord = recordName => band => ({ ...band, albums: [...band.albums, recordName] })
const removeMember = memberName => band => ({ ...band, members: band.members.filter(member => member !== memberName) })
const finalLineup = compose(releaseRecord('Live at Bush Hall'), removeMember('Isaac'))
console.log( finalLineup(newBand) )
Curry #
-
Makes that the function can be invoked with only some of the arguments
-
It is finally executed only when it receives all the arguments from the curried function
function curry(fn, arity) { arity ||= fn.length return function(...args) { if (args.length < arity) { return curry( (...lefts) => fn(...args, ...lefts), arity - args.length ) } return fn(...args) } }
Examples #
-
Without curry
const kingGizzard = { name: 'King Gizzard and the Lizard Wizard', genres: [ 'garage rock', 'rock', 'psychedelic rock', 'jazz fusion', 'heavy psychedelic rock', 'progressive rock', 'syth pop', 'thrash metal' ] } function validateBand(band, field, data) { if (field === 'name') { return band.name === data } if (field === 'genres') { return band.genres.includes(data) } } console.log( validateBand(kingGizzard, 'genres', 'rock') ) console.log( validateBand(kingGizzard, 'genres', 'psychedelic rock') )
-
With curry
const kingGizzard = { name: 'King Gizzard and the Lizard Wizard', genres: [ 'garage rock', 'rock', 'psychedelic rock', 'jazz fusion', 'heavy psychedelic rock', 'progressive rock', 'syth pop', 'thrash metal' ] } function curry(fn, arity) { arity ||= fn.length return function(...args) { if (args.length < arity) { return curry( (...lefts) => fn(...args, ...lefts), arity - args.length ) } return fn(...args) } } function validateBand(band, field, data) { if (field === 'name') { return band.name === data } if (field === 'genres') { return band.genres.includes(data) } } const curriedValidateBand = curry(validateBand) const validateKingGizzardGenres = curriedValidateBand(kingGizzard, 'genres') console.log( validateKingGizzardGenres('rock') ) console.log( validateKingGizzardGenres('psychedelic rock') )
Downsides #
- Memory consumption
- Type system can become complex over time
- Harder to onboard