Starting Functional JavaScript
(HJKL Arrow Keys to Navigate)
Pure Functions
Functions that have no side effects are said to be pure.
Which of these functions is pure?
const add = (x, y) => x + y
const trace = (tag, x) => console.log(tag, x) ?? x
const getTime = () => Date.now()
const resolveUser = ({ token }, models) =>
models.user.isValidToken(token)
? () => models.user.fetch(token)
: async () => null
Pure functions are useless, easily tested , and referentially transparent.
const add = (x, y) => x + y
add(2, 3) === 5;
source.replace('add(2, 3)', '5');
Currying
and
Partial Application
Curried Functions
Take their arguments one at a time.
const add = x => y => x + y
function add(x) {
return function addToX(y) {
return x + y;
}
}
Partial Application
Use closure to defer computation.
const add2 = add(2)
add2(3) === 5
add2(4) === 6
Partial application has many uses e.g. generic functions or fluent computations.
Function Composition
Combines many functions into one by applying each function to the result of the previous one. In other words, makes one function from two (or more) others.
// binary compose
const compose =
(f, g) =>
x => f(g(x))
const addTwice2 = compose(add(2), add(2))
const deepSerialClone = compose(JSON.parse, JSON.stringify)
const isAdmin = compose(
x => x === 'admin',
({ role }) => role
)
Use Case: Class Mixins
JavaScript class mixins are a way to share behaviour among classes
const DraggableMixin = superclass => class extends superclass {
onDragstart(event) { /* ... */ }
onDrop(event) { /* ... */ }
}
@customElement('draggable-image')
class DraggableImage extends DraggableMixin(LitElement) {
@property({ type: String, reflect: true }) src = '';
@property({ type: String, reflect: true }) alt = '';
render() { return html`<img src="${this.src}" alt="${this.alt}">` }
}
Pointfree Style
Functions which reference their own data are said to be "pointed". Likewise, functions which do not reference their parameters are said to be "pointfree". With curried and point-free functions, always take your data as the last parameter.
const map = f => xs => xs.map(f)
const filter = p => xs => xs.filter(p)
const assign = a => o => ({ ...o, ...a })
const handleAsJson = resp => resp.json()
fetch('/users')
.then(handleAsJson)
.then(filter(isAdmin))
.then(map(assign({admin: true}))
Functors
A functor is a container for some value, like an envelope. Functors can map
from some value x
in a category to another value y
in that same category.
Array
and Promise
are both functors.
const inc = x => x + 1
[1].map(inc) // [2]
Promise.resolve(2)
.then(inc) // Promise 3
Crocks
Crocks Helpers
import propOr from 'crocks/helpers/propOr'
import propPathOr from 'crocks/helpers/propPathOr'
// propOr :: A -> String -> {[String]: A} -> A
propOr(null, 'name', { name: 'ftr' }) // 'ftr'
propOr(null, 'name', { date: '2019' }) // null
// helpers are curried by default
const getName = propOr(null, 'name')
getName({ name: 'Forter' }) // 'Forter'
const getFirstCourseId = propPathOr(null, ['courses', '0', 'id'])
getFirstCourseId({ courses: [{ id: 1 }] }) // 1
getFirstCourseId({ courses: 'blammo!' }) // null
Crocks Logic
import { propOr, compose, isTruthy, not, and, or } from 'crocks'
const isUser = compose(isTruthy, propOr(null, 'token'))
const isRich = compose(x => x > 1000, propOr(0, 'balance'))
const hasFriends = compose(x => x.length, propOr([], 'friends'))
const isFriendly = and(isUser, hasFriends)
const isUnfriendly = not(isFriendly)
const shouldFriend = or(isFriendly, isRich)
Crocks Curry
Crocks' curry is very flexible.
import curry from 'crocks/helpers/curry'
const wellCurried = curry(
(x, y) => z => (foo, bar) => 'baz'
)
wellCurried(1) // curry((y, z, foo, bar) => 'baz')
wellCurried(1, 2, 3) // curry((foo, bar) => 'baz')
Monoids
A monoid is a type which has an 'empty' value, and an operation to combine values. Combining a value with the 'empty' value always produces the same value.
- What is the empty value for numbers under addition?
- Under multiplication?
You're already using monoids!
&&
forms a monoid withtrue
as empty value||
forms a monoid withfalse
as empty valueString#concat
forms a monoid with''
as empty value.Object.assign
forms a monoid with{}
as empty valueArray#concat
forms a monoid with[]
as empty value
mreduceMap
Folds an array under a monoid of your choice, first mapping your monoid constructor over it.
import { All, Any } from 'crocks'
import { compose, mreduceMap } from 'crocks/helpers'
import isNil from 'crocks/predicates/isNil'
import not from 'crocks/logic/not'
const hasToken = compose(not(isNil), propOr(null, 'token'))
const isFraud = compose(greaterThan(0.5), propOr(0, 'score'))
const allAreUsers = mreduceMap(All, hasToken)
const anyAreFraud = mreduceMap(Any, isFraud)
Monad
Like a functor, Monads can map over their contents.
Monads have the added ability to unwrap their self-similar
contents. This power is called chain
, bind
, or flatMap
[1]
.map(x => [x + 1]) // [ [ 2 ] ]
[1]
.flatMap(x => [x + 1]) // [2]
Maybe Monad
The Maybe monad wraps a value which may not exist. It has two instances: Just a
and Nothing
. Mapping over a Just
works as expected. Mapping over a
Nothing
skips execution.
import { ifElse, isNumber, Just, Nothing, chain } from 'crocks'
const safe = p => ifElse(p, Just, Nothing)
const gt10 = x => x > 10
const safeNumber = safe(isNumber)
const maybeBig = safe(gt10)
const bigNumber = compose(
chain(maybeBig),
safeNumber
)
Maybe Monad - Binary
import { Maybe, safe, isNumber } from 'crocks'
const safeNumber = safe(isNumber)
const safeAdd = (x, y) => {
const maybeX = safeNumber(x)
const maybeY = safeNumber(y)
return maybeX.chain(
x_ => maybeY.map(y_ => x_ + y_)
)
}
safeAdd(1, 2) // Maybe 3
safeAdd(1, null) // Nothing
safeAdd('רק', 'ביבי') // Nothing
Maybe Monad - Binary
Wait!! People code like that?
👉 NOPE 👈
Monads are also applicatives, which means we can lift any function into
'monadic space' with lift
import { liftA2 } from 'crocks';
const add = (x, y) => x + y;
const safeAdd = (x, y) =>
liftA2(add, safeNumber(x), safeNumber(y))
Thanks For
Watching!