All Articles

Notes on Hardcore Functional Programming in Javascript - Brian Lonsdorf

Functions

What are functions

 

Functions are a single-valued collection of pairs. They must be deterministic and not take the same input and give different results.

  One input, one output
  
  (-2,-1)
  (0 , 0)
  (2 , 1)

The left side is called teh domain and the right the range

 

Functions abide by always being

  1. Total
  2. Deterministic
  3. No Observable Side-Effects

 

const inc = i => {                                  const inc = i => {
  if(i === 0) return 1                                return i + 1
  if(i === 1) return 2                              }
  if(i === 2) return 3
}                                                  

 

A total function means that for every input there is a corresponding output. The left is an example of a function that is not total and on the right is a total function.

 

// This is not deterministic as the output will always change with the comment it is given
const timeSince = comment => {
    const now = new Date()
    const then = new Date(comment.createdAt)
    return getDifference(now, then)
}

// This function will always give the same result if given the same input
const getDifference = (now, then) => {
    const days = Math.abs(now.getDate() - then.getDate())
    const hours = Math.abs(now.getHours() - then.getHours())
    return {days, hours}
}

Deterministic means always having the same output for a given input.

 

// Changing the outside world in an observable ways
const add = (x, y) => {
  console.log(`Adding ${x} ${y}`)
  return x + y
}

const add = (x, y) => { 
  return { result: x + y, log: `Adding ${x} ${y}`
}

No observable(within the confines of reason) side-effects besides computing a value.

 

What are pure functions

 

  // Not a function
  var xs = [1,2,3,4,5]
  xs.splice(0,)3il
  // => [1,2,3]
  xs.splice(0,3)
  // => [4,5]
  // This will mutate the array and not keep the input pure, because of the implementation of splice
  
  // Function
  xs.slice(0,3)
  // => [1,2,3]
  xs.slice(0,3)
  // => [1,2,3]
  // This is a pure function that doesn't modify the original input and returns the same output given the same input

  // Not a function
  // Doesn't allow us to see what is going on 
  const signUp = (attrs) => {
    let user = saveUser(attrs)
    welcomeUser(user)
  }
  
  // Function
  // Allows us to at least see that the function occured by return and maintains the purity as well with the input
  const signUp = (attrs) => {
    return () => {
      let user = saveUser(attrs)
      welcomeUser(user)
    }
  }

 

Why use pure functions?

  1. Reliable
  2. Portable
  3. Reusable
  4. Testable
  5. Composable
  6. Properties/Contract

 

Properties Arguments Currying

 

 // Associative 
 add(add(x,y), z) == add(x, add(y,z))

 // commutative
 add(x,y) == add(y,x)

 // identity
 add(x,0) == x

 // distributive
 add(multiply(x, y), multiply(x, z) == multiply(x, add(y,z))

 

This allows us to think of the multiple ways we could create a function and still get the same result.

 

  const add = (x,y) => x + y
  
  const curry = f => 
    x => y => f(x,y)
  
  const curriedAdd = curry(add)

  const increment = curriedAdd(5)
  const curryResult = increment(10)
  
  // Curry result is 15

Currying allows us to take arguments one at a time and produce an output based on it. The function remembers it’s argument. Think about design/reusability when currying, do you really need it?

 

Currying vs Partial Application

  • Currying always produces nested unary functions. The transformed function is still largely the same as the original.
  • Partial application produces functions of arbitrary number of arguments. The function that is returned takes less arguments than the original.

 

// Curry Example in mathematical terms
Currying takes a function
f : X x Y -> R
and turns it into a function
f: X->(Y->R)
Uncurried Function is invoked as f(3,5)
Curred Function is invoked as f(3)(5)

Normal Function Version
const add = (x,y) => x + y

Curried Function Version
const curriedAdd = f => x => => y => x + y

Will be invoked as
curriedAdd(add)(10)(2) // 12

// Algorithm for currying
curry: (X x Y -> R)->(X->(Y->R))

Curry takes a binary function that returns a unary function
const curry = f => x => y => f(x,y)

// Partial Example in mathematical terms
Partial Application takes a function
f: X x Y -> R

and a fixed value for x for the first argument to produce a new function
f: Y -> R

You only have to fill in the second parameter which is why it is one less than the arity
of the initial f. One says that the first argument is bound to x. 

Normal Function Version
const add = (x,y) => x + y

Partial Application Function Version
const partialApplication = (f,x) => y => f(x,y)

Will be invoked as
const add5 = partialApplication(add, 5)
add5(10) // 15

// Algorithm for partial application
partApply: ((X x Y -> R) x X)->(Y -> R)

partApply takes a binary function and a value and produces a unary functrion
const partApply = (f,x) => y => f(x,y)

 

Compose

Composing is a general concept in mathematics where you combine two or more functions into a brand new function, in the form of f(g(x)) “f of g of x”. This allows us to reduce code and to providing a more covenient reusable piece of code. Compose is also associative.

const toUpper = str => str.toUpperCase()

const exclaim = str => str + '!'

const compose = (f,g) => x => f(g(x))

const applyCompose = compose(toUpper, exclaim)

const applyCompose('compose') // COMPOSE!

Example of compose with a library for example RamdaJS

// Composition is dot chaining 

// Using Compose
const doStuff = _.compose(join(''),_.filter(x => x.length > 3), reverse, _.map(trim), split(' '), toLowerCase)

// Using dot chaining
const doStuff = str => str.toLowerCase().split(' ').map(c => c.trim()).reverse().filter(x => x.length > 3).join(' ')

// Logging in compose
const log = curry((tag,x) => (Console.log(tag, x), x))

// If needing to map multiple functions  Map comes from category theory of mapping
compose(map(f) , map(g)) == map(compose(f, g))

// Example of function compositions

// Normal Function
const availablePrice = cars => {
    const available_cars = _.filter(.prop('in_stock'), cars);
    
    return available_cars.map(x => formatMoney(x._dollar_value)).join(', ');
}

// Compose Function
const availablePrices = _.compose(_.join(''),_.map(_.compose(formatmoney, _.prop('dollar_value')), _.filter(_.prop('in_stock'))
                                                   
// Compose Function with Subcomposition
const formatDollarValue = _.compose(formatMoney, _.prop('dollar_value'))
const availablePrices = _.compose(_.join(', '),_.map(formatDollarValue),_.filter(_.prop('in_stock'))

// What if you wanted to maintain the array 
const formatDollarValue = _.compose(formatMoney,_.prop('dollar_value'))
const availablePriceArray = _.compose(_.map(formatDollarValue),_.filter(_.prop('in_stock')))
                                  
const availablePrices = _.compose(_.join(', '), availablePriceArray)

// Usage of flip
const append = _.flip(_.concat)
const fastestCar = _compose(append('is the fastest'),_.prop('name'),_.last,_sortBy(_.prop('horsepower'))

                            

 

Identity Functor

Maps each object and morphism of C to itself C: C -> C .

// Takes data and works on it, then will return a value, it is a functor because it has a map method
const Box = x => {
  {
    map: f => Box(f(x)),
    fold: f => f(x),
    toString: `Box${x}s`
  }
}

// Original Function
const nextCharForNumberString = str => {
    const trimmed = str.trim()
    const number = parseInt(trimmed)
    const nextNumber = new number(number + 1)
    
    return String.fromCharCode(nextNumber)
}


// Example using Box identity functor
const nexCharForNumberString = str =>
    // Clean and Concise, no extra declarations needed
    Box(str)
    .map(x => x.trim())
    .map(trimmed => parseInt(trimmed,10))
    .map(number => new Number(number + 1))
    .fold(String.fromCharCode)
          

nextCharForNumberString('64') // A

// Example of flatmap

const Box = x => ({
  map: f => Box(f(x)),
  chain: f => f(x),
  fold: f => f(x),
  toString: () => `Box(${x})`
})


const applyDiscount = (price, discount) =>
      // Box(Box(x))
      Box(moneyToFloat(price))
      .chain(cents =>
        Box(percentToFloat(discount))
           .map(savings => cents - (cents * savings))) 
              .fold(x => x)

applyDiscount('$5.00', '20%') // 4

 

Monad

In functional programming a monad is an abstraction that allows structuring programs generically(meaning we don’t care about the type).This allows us to interact with all different data representations and perform some type of operation. A monad achieves their own data type(a particular type for each type of monad), which represents a form of computation, along with two procedures.

  • One to wrap values of any basic type within the monad (yielding a monadic value)
  • Another to compose functions that output monadic values (called monadic functions)

Monads allow you to simplify a wide range of problems, like handling potential undefined values ( with the Maybe monad), or keeping values within a flexible, well-formed list(using the List monad). With a mona we can turn a sequence of complicated functions into a succint pipelin that abstracts state management, control flow, or side-effects.

The concept of a monad and the term originally come from category theory, where a monad is defined as a functor with additional structure.

 

Either Monad

The Either monad is a type called Either.

// Example that will return undefined
const findColor = name => 
  ({red: '#ff4444', blue: '#3b5998', yellow: '#fff68f'}[name])

const res = findColor('redd').toUpperCase()


console.log(res)

// How to deal with null / undefined

// Think of these as two sub-classes of Either
const Right = x => 
({
  
  chain: f => f(x),
  map: f => Right(f(x)),
  fold: (f, g) => g(x),
  toString: `Right(${x})`
})

const Left = x =>
({
  chain: f => Left(x),
  map: f => Left(x),
  fold: (f, g) => f(x),
  toString: `Left(${x})`
})



const findColor = name => {
  const found = {red: '#ff4444', blue: '#3b5998', yellow: '#fff68f'}[name]
  
  return found ? Right(found) : Left('missing')
}
// Now we are checking and if not found received a message with info vs undefined or null

// Bubbling up error to receive error handling with Left

const res = findColor('redd').map(x => x.toUpperCase()).fold(() => 'no color!', color => color)


console.log(res)


// What if we were to use fromNullable

const fromNullable = x => 
  x != null ? Right(x) : Left()

// Refactored to return a Left | Right and less state management
const findColor = name => {
    fromNullable({red: '#ff4444', blue: '#3b5998', ye llow: '#fff68f'}[name])
  
}


// Refactoring using the Either Monad

// Impure function that will be transformed into a mathematical function that we will deal with on the calling side, instead of trying to throw an error

const fs = require('fs')

// This trycatch when this function is called. When this blows up I get an error and when it doesn't, I get a Right 
const tryCatch = f => {
  try {
    return Right(f())
  } catch(e) {
    return Left(e)
  }
  
}


const getPort_ = () => {

  try {
    const str = fs.readFileSync('config.json')
    const config = JSON.part(str)
    return config.port
  } catch(e) {
    return 3000
  }
  
}

// This was to always have try/catch on it 
const readFileSync = path => tryCatch(() => fs.readFileSync(path))

// Helper for parsing
const parseJSON = contents =>
  tryCatch(() => JSON.parse(contents))

const getPort = () => 
  // This isn't using purity yet. The I/O handling will happen later in the notes

 
// Flattens as it maps flatmap
  readFileSync('config.json') // Right('')
  .chain(contents => parseJson(contents)) // Right(Right({}))
  .map(config => config.port)
  .fold(() => 8080, x => x)

const result = getPort()

console.log(result) // 8080 on success or failure

 

Task Monad

Task monad is essentially promise in Javascript.

const Box = f => ({
  map: g => Box(compose(f, g)),
  fold: f
)}

Box(() => 2).map(two => two + 1).fold() // 2           

Task.of(2).map(two => two + 1) // Task(2)

const t1 = Task((rej,res) => res((2).map(two => two + 1).map(three => three * 2)

// Fork runs the function, lazy approach that makes none of this code run when declared except at t1.fork
t1.fork(console.error, console.log)
                

// Refactoring node.io with task

// Task will handle all out io there is a writer monad that does that as well but it doesn't handle async
                
// Original
const fs = require('fs')

const app () => 
  fs.readFile('config.json', 'utf-8', (err, contents) => {
    console.log(err, contents)
    if (err) throw err
    
    const newContents = contents.replace(/3/g,'6')
    
    
  fs.writeFile('config1.json', newContents, (err,_) => {
    if (err) throw err
    console.log('success!)
  })
}


// refactored
// lazy promise
// Example to show how it works Task has methods to make promises out of callbacks
               
const readFile = (path, enc) => 
  Task((rej, res) =>
       fs.readFile(path, enc, (err, contents) =>
                   err ? rej(err) : res(contents)
                   )
       )
    

const writeFile = (path, contents) =>
  Task((rej, res) => 
       fs.write(parth, contents, (err, contents) =>
                err ? rej(err) : res(contents)
                ))
              
                
const app_ = () =>
  readFile('config.json', 'utf-8').map(contents => contents.replace(/3/g, '6').chain(newContents => writeFile('config1.json', newContents))
                                       

// We could write this as promises, but promises were inspired by functional programming and they aren't lazy we could before fork apply any logic to the app like running another app or something in parallel and it wouldn't affect the log or ru
app().fork(console.error, () => console.log('success!))

 

Natural Transformation

// nt(a.map(f)) == nt(a).map(f)
// F a -> T a - Type Transformation
const eitherToTask = e =>
  e.fold(Task.rejected, Task.of)

// Original Example
const fake = id => 
  ({ id: id, name: 'user1', best_friend_id: id + 1})

const Db = ({
  find: id =>
    Task((rej,res) =>
         setTimeout(() =>
                      res(id > 2 ? Right(fake(id)) : Left('not found')),100))
})

const send = (code, json) => 
  console.log(`sending ${code}: ${JSON.stringify(json)}`)
a
DB.find(3) // Task(Either(user))
.chain(eu =>
       eu.fold(e => Task.of(eu),
               u => Db.find(u.best_friend_id)))
    .fork(error => send(500, {error}),
          eu => eu.fold(error => send(404, {error}),
                        x => send(200,x)))


// Refactored
Db.find(3)
.chain(eitherToTask) // Task(User)
.chain(u => Db.find(u.best_friend_id))
.chain(eitherToTask)
.fork(error => send(500, {error}),
      u => send(200, u))