raganwald / allong.es

https://leanpub.com/javascriptallongesix
484 stars 28 forks source link

allong.es

The allong.es library is a collection of functions designed to facilitate writing JavaScript and/or CoffeeScript with functions as first-class values. The emphasis in allong.es is on composing and decomposing functions using combinators and decorators. allong.es is designed to complement libraries like Underscore, not compete with them.

Currying and Partial Application

At the heart of allong.es are the functions that curry and partially apply other functions. The two most important to understand are call and apply. They work very much like the .call and .apply methods that every JavaScript function implements:

function greet (how, whom) {
  return '' + how + ', ' + whom + '!';
};

call(greet, 'Hello', 'Tom')
  //=> 'Hello, Tom!'

apply(greet, ['Hello', 'Tom'])
  //=> 'Hello, Tom!'

Their "special sauce" is that they automatically curry the supplied function, so if you provide fewer or no arguments, you get back a partially applied or curried function:

call(greet)('Hello')('Tom')
  //=> 'Hello, Tom!'

call(greet, 'Hello')('Tom')
  //=> 'Hello, Tom!'

apply(greet, [])('Hello')('Tom')
  //=> 'Hello, Tom!'

apply(greet, ['Hello'])('Tom')
  //=> 'Hello, Tom!'

immediate application

If you don't want the currying/partial application behaviour, there is an immediate application version named (appropriately), callNow (and also another named applyNow, not shown):

callNow(greet, 'Hello', 'Tom')
  //=> 'Hello, Tom!'

callNow(greet, 'Hello')
  //=> 'Hello, undefined!'

variations on the order of applying the arguments

callRight applies any arguments supplied to the right. If you supply all the arguments, it's the same as call, but if you supply fewer arguments, you get a right partial application:

callRight(greet, 'Hello', 'Tom')
  //=> 'Hello, Tom!'

callRight(greet, 'Hello')('Tom')
  //=> 'Tom, Hello!'

callFlipped applies the arguments backwards, even when curried:

callFlipped(greet, 'Hello', 'Tom')
  //=> 'Tom, Hello!'

callFlipped(greet, 'Hello')('Tom')
  //=> 'Tom, Hello!'

callFlipped(greet)('Hello')('Tom')
  //=> 'Tom, Hello!'

more partial application

callLeft is actually synonymous with call: It applies arguments given to the left. We've seen callRight above. Both are variadic: You can supply as many arguments as you want.

callFirst and callLast are just like callLeft and callRight, but they are binary functions: They accept a function and exactly one argument. This is sometimes useful when combining functions together.

callFirst and callLast both have "flipped and curried" versions (callFirstWith and callLastWith). callLastWith is especially useful for working with functions written in "collection - operation" style. Here we take advantage of the fact that they are "automatically curried" to implement the popular pluck function.

currying

allong.es does support the curry function, it is implemented as the unary form of call:

var curry = unary(call);

with

splat was present in earlier versions of allong.es but has been deprecated as being too cryptic. Instead, there is a general naming convention that works as follows. Many binary functions such as map and filter are historically written to take a noun or collection as the first argument and a verb as the second.

However, reversing and currying these functions is super-useful as it makes composeable functions out of them. That's why callFlipped is so important. But to save you the trouble of writing callFlipped map everywhere, many such functions in allong.es have a clipped version pre-defined and named with the suffix With:

map(list, function)       <=> mapWith(function, list)
filter(list, function)    <=> filterWith(function, list)
get(object, propertyName) <=> getWith(propertyName, object)
pluck(list, propertyName) <=> pluckWith(propertyName, list)

So you "map" a list, but "mapWith" a function. And of course, they are all curried. For example:

map(list)(function)       <=> mapWith(function)(list)
deepMap(list)(function)   <=> deepMapWith(function)(list)
filter(list)(function)    <=> filterWith(function)(list)
get(object)(propertyName) <=> getWith(propertyName)(object)
pluck(list)(propertyName) <=> pluckWith(propertyName)(list)

Thus if you have a collection such as:

var users = [
  { name: 'Huey' },
  { name: 'Dewey' },
  { name: 'Louie' }
]

You can get the names with either:

pluck(users, 'name')
  //=> ['Huey', 'Dewey', 'Louie']

Or:

pluckWith('name', users)
  //=> ['Huey', 'Dewey', 'Louie']

The latter is interesting because pluck and pluckWith are both automatically curried (like almost everything that isn't named "now"). Thus, we could also write:

var namesOf = pluckWith('name');

// ...
namesOf(users)
  //=> ['Huey', 'Dewey', 'Louie']

Arity Function Decorators

variadic

Makes a function into a variadic (accepts any number of arguments). The last named parameter will be given an array of arguments.

var variadic = require('allong.es').allong.es.variadic;

var fn = variadic(function (a) { return a })

fn()
  //=> []
fn(1, 2, 3)
  //=> [1,2,3]

fn = variadic(function (a,b) { return { a: a, b: b } })

fn()
  //=> { a: undefined, b: [] }
fn(1)
  //=> { a: 1, b: [] }
fn(1,2,3)
  //=> { a: 1, b: [2, 3] }

variadic, part ii

When given just the function, variadic returns a function with an arity of zero. This is consistent with JavaScript programming practice. There are times when you wish to report an arity, meaning that you want the returned function to have its length getibute set.

You do this by prefacing the function argument with a length:

fn = variadic(function (a,b) { return { a: a, b: b } });

fn.length
  //=> 0

fn2 = variadic(1, function (a,b) { return { a: a, b: b } }); 

fn2.length
  //=> 1

unary, binary, and ternary

Sometimes, you have a function that takes multiple arguments, but you only want it to accept one, or two, or maybe three arguments and ignore the rest. For example, parseInt takes a radix as an optional second parameter. And that is havoc if you try to use it with Array.map:

['1', '2', '3', '4', '5'].map(parseInt)
  //=> [ 1,
  //     NaN,
  //     NaN,
  //     NaN,
  //     NaN ]

Use unary(parseInt) to solve the problem:

['1', '2', '3', '4', '5'].map(unary(parseInt))
  //=> [ 1, 2, 3, 4, 5 ]

binary has similar uses when working with Array.reduce and its habit of passing three parameters to your supplied function.

Miscellaneous Combinators

bound

var bound = require('allong.es').allong.es.bound;

bound(fn, args...)(obj)
  //=> fn.bind(obj, args...)

getWith

var getWith = require('allong.es').allong.es.getWith;

array.map(getWith('property'))
  //=> array.map(function (element) {
  //               return element['property']
  //             })

Functional Composition

var compose = require('allong.es').allong.es.compose,
    sequence = require('allong.es').allong.es.sequence;

compose(a, b, c)
  //=> function (x) {
  //     return a(b(c(x)))
  //   }

sequence(a, b, c)
  //=> function (x) {
  //     return c(b(a(x)))
  //   }

List Combinators

mapWith and deepMapWith

var mapWith = require('allong.es').allong.es.mapWith,
    deepMapWith = require('allong.es').allong.es.deepMapWith;

var squareList = mapWith(function (x) { return x * x })

squareList([1, 2, 3, 4])
  //=> [1, 4, 9, 16]

var squareTree = deepMapWith(function (x) { return x * x })

squareTree([1, 2, [3, 4]])
  //=> [1, 4, [9, 16]]

Function/Method Decorators

maybe

var maybe = require('allong.es').allong.es.maybe;

var safeFirst = maybe(function (arr) { return arr[0] })

safeFirst([1, 2, 3])
  //=> 1
safeFirst(null)
  //=> null

tap

var tap = require('allong.es').allong.es.tap;

tap([1, 2, 3, 4, 5], send('pop'))
  //=> [1, 2, 3, 4]

fluent

var fluent = require('allong.es').allong.es.fluent;

Role = function () {}

Role.prototype.set = fluent( function (property, name) { 
  this[property] = name 
})

var doomed = new Role()
  .set('name', "Fredo")
  .set('relationship', 'brother')
  .set('parts', ['I', 'II'])

once

var once = require('allong.es').allong.es.once;

var message = once( function () { console.log("Hello, it's me") })

message()
  //=> "Hello, it's me"
message()
  //=>
message()
  //=>
message()
  //=>

Decorating Classes/Constructors

var mixin = require('allong.es').allong.es.mixin,
    classDecorator = require('allong.es').allong.es.classDecorator;

function Todo (name) {
  var self = this instanceof Todo
             ? this
             : new Todo();
  self.name = name || 'Untitled';
  self.done = false;
};

Todo.prototype.do = fluent( function () {
  this.done = true;
});

Todo.prototype.undo = fluent( function () {
  this.done = false;
});

var AddLocation = mixin({
      setLocation: fluent( function (location) {
        this.location = location;
      }),
      getLocation: function () { return this.location; }
    });

AddLocation.call(Todo.prototype);
// Or use AddLocation(Todo.prototype)

new Todo("Vacuum").setLocation('Home');
  //=> { name: 'Vacuum',
  //     done: false,
  //     location: 'Home' }

var AndColourCoded = classDecorator({
  setColourRGB: fluent( function (r, g, b) {
    this.colourCode = { r: r, g: g, b: b };
  }),
  getColourRGB: function () {
    return this.colourCode;
  }
});

var ColourTodo = AndColourCoded(Todo);

new ColourTodo('Use More Decorators').setColourRGB(0, 255, 0);
  //=> { name: 'Use More Decorators',
  //     done: false,
  //     colourCode: { r: 0, g: 255, b: 0 } }

Note: classDecorator works with JavaScript constructors that have a default implementation (they work properly with no arguments), and are new-agnostic (they can be called with new or as a normal function). Todo above has both properties.

Functional Iterators

Functional iterators are stateful functions that "iterate over" the values in some ordered data set. You call the iterator repeatedly to obtain the values, and it will either never stop returning values (an infinite data set) or return undefined when there are no more values to return.

The functional iterators utilities are all namespaced:

var iterators = require('allong.es').allong.es.iterators;

FlatArrayIterator and RecursiveArrayIterator

Making functional iterators from arrays:

var FlatArrayIterator = iterators.FlatArrayIterator,
    RecursiveArrayIterator = iterators.RecursiveArrayIterator;

var i = FlatArrayIterator([1, 2, 3, 4, 5]);

i();
  //=> 1
i();
  //=> 2
i();
  //=> 3
i();
  //=> 4
i();
  //=> 5
i();
  //=> undefined

var i = FlatArrayIterator([1, [2, 3, 4], 5]);

i();
  //=> 1
i();
  //=> [2, 3, 4]
i();
  //=> 5
i();
  //=> undefined

var i = RecursiveArrayIterator([1, [2, 3, 4], 5]);

i();
  //=> 1
i();
  //=> 2
i();
  //=> 3
i();
  //=> 4
i();
  //=> 5
i();
  //=> undefined

range and numbers

var range = iterators.range,
    numbers = iterators.numbers;

var i = range(1, 5);

i();
  //=> 1
i();
  //=> 2
i();
  //=> 3
i();
  //=> 4
i();
  //=> 5
i();
  //=> undefined

var i = range(1, 5, 2);

i();
  //=> 1
i();
  //=> 3
i();
  //=> 5
i();
  //=> undefined

var i = range(5, 1);

i();
  //=> 5
i();
  //=> 4
i();
  //=> 3
i();
  //=> 2
i();
  //=> 1
i();
  //=> undefined

var i = range(1);

i();
  //=> 1
i();
  //=> 2
i();
  //=> 3
// ...

var i = numbers();

i();
  //=> 1
i();
  //=> 2
i();
  //=> 3
// ...

var i = numbers(0);

i();
  //=> 0
i();
  //=> 1
i();
  //=> 2
i();
  //=> 3
// ...

unfold and unfoldWithReturn

Unfold makes an iterator out of a seed by successively applying a function to the seed value. Here's an example duplicating the "numbers" feature:

var unfold = iterators.unfold,
    unfoldWithReturn = iterators.unfoldWithReturn;

var i = unfold(1, function (n) { return n + 1; });

i();
  //=> 1
i();
  //=> 2
i();
  //=> 3
// ...

var i = unfoldWithReturn(1, function (n) { 
  return [n + 1, n + n]; 
});

i();
  //=> 2
i();
  //=> 4
i();
  //=> 6
// ...

A richer example of unfoldWithReturn:

var cards = ['A', 2, 3, 4, 5, 6, 7, 8, 9, '10', 'J', 'Q', 'K'];

function pickCard (deck) {
  var position;

  if (deck.length === 0) {
    return [[], void 0];
  }
  else {
    position = Math.floor(Math.random() * deck.length);
    return [
      deck.slice(0, position).concat(deck.slice(position + 1)),
      deck[position]
    ];
  }
};

var i = unfoldWithReturn(cards, pickCard);

i();
  //=> 5
i();
  //=> 4
i();
  //=> 2
i();
  //=> J

// ...

map

Stateless mapping of an iterator to another iterator:

var map = iterators.map;

var squares = map(numbers, function (n) { return n * n; });

squares();
  //=> 1
squares();
  //=> 4
squares();
  //=> 9
// ...

accumulate

Accumulating an iterator to another iterator, a/k/a stateful mapping, with an optional seed:

var accumulate = iterators.accumulate;

var runningTotal = accumulate(numbers, function (accumulation, n) { 
      return accumulation + n; 
    });

runningTotal();
  //=> 1
runningTotal();
  //=> 3
runningTotal();
  //=> 6
runningTotal();
  //=> 10
runningTotal();
  //=> 15
// ...

var runningTotal = accumulate(numbers, function (accumulation, n) { 
      return accumulation + n; 
    }, 5);

runningTotal();
  //=> 6
runningTotal();
  //=> 8
runningTotal();
  //=> 11
runningTotal();
  //=> 15
runningTotal();
  //=> 20
// ...

accumulateWithReturn

This code transforms filters duplicates out of an iterator of numbers by turning them into "false." It consumes space proportional to the time it runs and the size of the set of possible numbers in its iterator.

var accumulateWithReturn = iterators.accumulateWithReturn;

var randomNumbers = function () {
  return Math.floor(Math.random() * 10);
};

randomNumbers();
  //=> 7
randomNumbers();
  //=> 0
randomNumbers();
  //=> 1
randomNumbers();
  //=> 1
randomNumbers();
  //=> 6
// ...

var uniques = accumulateWithReturn(randomNumbers, function (alreadySeen, number) {
  var key = number.toString();

  if (alreadySeen[key]) {
    return [alreadySeen, false];
  }
  else {
    alreadySeen[key] = true;
    return [alreadySeen, number];
  }
}, {});

uniques();
  //=> 7
uniques();
  //=> 5
uniques();
  //=> 1
uniques();
  //=> false
uniques();
  //=> 9
uniques();
  //=> 4
uniques();
  //=> false
// ...

select and reject

var select = iterators.select,
    reject = iterators.reject;

function isEven (number) {
  return number === 0 || !isEven(number - 1);
};

var evens = select(randomNumbers, isEven);

evens();
  //=> 0
evens();
  //=> 6
evens();
  //=> 0
evens();
  //=> 2
evens();
  //=> 4
// ...

var odds = reject(randomNumbers, isEven);

odds();
  //=> 3
odds();
  //=> 1
odds();
  //=> 7
odds();
  //=> 9
odds();
  //=> 9
// ...

Note: select and reject will enter an "infinite loop" if the iterator does not terminate and also does not have any elements matching the condition.

slice

var slice = iterators.slice,
    numbers = unfold(1, function (n) { return n + 1; });

var i = slice(numbers, 3);

i();
  //=> 4
i();
  //=> 5
i();
  //=> 6

i = slice(numbers, 3, 2);

i();
  //=> 10
i();
  //=> 11
i();
  //=> undefined

take

var take = iterators.take,
    numbers = unfold(1, function (n) { return n + 1; });

var i = take(numbers);

i();
  //=> 1
i();
  //=> undefined

var i = take(numbers);

i();
  //=> 2
i();
  //=> undefined

var i = take(numbers, 3);

i();
  //=> 3
i();
  //=> 4
i();
  //=> 5
i();
  //=> undefined
// ...

drop

var drop = iterators.drop,
    numbers = unfold(1, function (n) { return n + 1; });

drop(numbers);

numbers();
  //=> 2
numbers();
  //=> 3
numbers();
  //=> 4

drop(numbers);

numbers();
  //=> 6
numbers();
  //=> 7

drop(numbers, 3);

numbers();
  //=> 11
numbers();
  //=> 12
// ...

Trampolining

var trampoline = require('allong.es').allong.es.trampoline,
    tailCall = require('allong.es').allong.es.tailCall;

function factorial (n) {
  var _factorial = trampoline( function myself (acc, n) {
    return n > 0
      ? tailCall(myself, acc * n, n - 1)
      : acc
  });

  return _factorial(1, n);
};

factorial(10);
  //=> 3628800