Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save sethlivingston/10cd6d214448cacf9d764f508e377091 to your computer and use it in GitHub Desktop.
Save sethlivingston/10cd6d214448cacf9d764f508e377091 to your computer and use it in GitHub Desktop.
Functional Programming Notes

A box (or discriminated union) is just a context in which to apply functions.

.map(f) is really not about traversal, but rather applying a function or transformation within a context (of a box).

.fold(f, ...) is similar to map, except the return value is unboxed.

  • Formally, folding is the application of a combining function to reduce a structure or collection to a single value.
  • For boxes like Either, fold usually takes multiple functions, one to apply to each type of the union.

.chain(f) is similar to .map(f), but it should be used when f returns a box instead of a raw value. That way, the result of the function will be a box(a) instead of a box(box(a)).

A semigroup is a box that has a .concat() method so that its enclosed value may be concated.

  • The laws of associativity apply: a.concat(b).concat(c) === a.concat(b.concat(c)).
  • The actual implementation of concat may vary. + and && are two examples.
  • If a structure's members are all semigroups, then the structure is also a semigroup.
  • The concat method need not apply to all types in the union. For example, folktale's Validation.Failure concats but Validation.Success is the identity.
  • See http://mathworld.wolfram.com/Semigroup.html.

A monoid is a semigroup with an identity element, often named empty. See http://mathworld.wolfram.com/Monoid.html.

  • The identity element ensures failsafe concatenation: ''.concat('something').

.foldMap() is a shortcut for a map and then a fold with an initial value:

List.of(1,2,3)
.map(Sum)  // where Sum is a semigroup
.fold(Sum.empty())
// => Sum(6)

// becomes

List.of(1,2,3)
.foldMap(Sum, Sum.empty())
// => Sum(6)

A functor is a box with .map() that obeys these laws:

  • Preserves function composition: box.map(f).map(g) === box.map(f(g(x)).
  • Identity box.map(id) === id(box).

A monad is a box with .of() and .chain() (which is also known as .flatMap(), .bind(), and >>=.

An applicative functor uses the .ap() function to apply a function in one box to the contents of another box:

const add = x => y => x + y
Box(add)
  .ap(Box(2) // Box(y => 2 + y)
  .ap(Box(3) // Box(5)

Lifting and .liftA2() help clean up the above need to apply a simple function to two (thus the A2) boxes. We lift the values out of the boxes and apply the function:

liftA2(add, Box(2), Box(3))
// => Box(5)

This is made possible by the law Box(x).map(f) === Box(f).ap(Box(x)):

const liftA2 = (f, boxx, boxy) =>
  boxx.map(f).ap(boxy)

Applicative functors are great for nested looping. For example,

// No mapping or for loops
const merchandise = () =>
  List.of(x => y => z => `product:${x} size:${y} color:${z}`)
  .ap(List(['t-shirt', 'sweater'])
  .ap(List(['small', 'medium', 'large'])
  .ap(List(['black', 'white'])

Applicative functors are also great for concurrent actions. For example,

const Db = ({
  find: id =>
    new Task((rej, res) =>
      setTimeout(() => res({id: id, title: `Project ${id}`}), 100)) // fake delay
})

const reportHeader = (p1, p2) =>
  `Report: ${p1.title} compared to ${p2.title}`

Task.of(p1 => p2 => reportHeader(p1, p2))
.ap(Db.find(20))
.ap(Db.find(8)) // does not wait for previous Db.find to complete
.fork(console.error, console.log)
// => Report: Project 20 compared to Project 8

TODO: If I remove the fork call above, will any of it run?

.traverse() is used to "leap frog" or "commute" from an array of Tasks...

const readFile = futurize(fs.readFile)

const files = ['foo.json', 'bar.json']
const res = files.map(filename => readFile(filename, 'utf-8'))
// => [Task, Task]

...to a Task with an array of results:

const files = List(['foo.json', 'bar.json'])
files.traverse(Task.of, filename => readFile(filename, 'utf-8'))
.fork(console.error, console.log)
// => Task([result, result])

Note that traverse can also turn Map({key: Task}) => Task(Map({key: result}))

Map({home: '/', about: '/about'})
.traverse(Task.of, route => httpGet(route))  // httpGet returns a Task
.fork(console.error, console.log)
// => Task({home: results, about: results})

Traverse only applies to applicative functors.

A natural transformation is, naively, a function that converts one box to another, i.e. Either(x) => Task(x).

  • Formally, a natural transformation (nt) must obey nt(x).map(f) === nt(x.map(f)).

  • The two sides of this law describe the two paths (top-right vs left-bottom) that may be used to get from F(a) --> G(b):

    F(a) -- map(f) --> F(b) | | | nt | nt | | G(a) -- map(f) --> G(b)

An isomorphism can convert one type to another and back again.

from(to(x)) === to(from(x))

For example,

// String ~ [Char]
const Iso = (to, from) => ({ to, from })
const stringToCharIso = Iso(s => s.split(''), c => c.join(''))

This allows us to convert types into another type that is more conducive to what we want to do:

const truncate = str =>
  stringToCharIso.from(stringToCharIso.to(s).slice(0, 3)).concat('...')
  
truncate('hello world')
// => 'hel...'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment