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

https://evilsoft.github.io/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 with true as empty value
  • || forms a monoid with false as empty value
  • String#concat forms a monoid with '' as empty value.
  • Object.assign forms a monoid with {} as empty value
  • Array#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!

Read More

Benny Powers Github profile Gitlab profile