Skip to content

Latest commit

 

History

History
366 lines (262 loc) · 9.41 KB

5.coffee.md

File metadata and controls

366 lines (262 loc) · 9.41 KB

Function-Building Functions

In which we discuss how and why one build functions on the fly and explore various ways to facilitate function composition.

_ = require 'underscore'
{ok, deepEqual} = require 'assert'
eq = deepEqual

The Essence of Functional Composition

In which we discuss how to use existing parts in well-known ways to build up new behaviors that can later serve as behaviors themselves.

We'll need a few of our previously defined functions for what follows.

fail = (msg) -> throw new Error msg

callIf = (cond, f) -> f() if cond

invoker = (method) ->
  (obj, args...) ->
    fail 'Must provide a target object' unless obj?
    _method = obj[method.name]
    kosher = _method? and method is _method
    callIf kosher, -> obj[method.name](args...)

The following dispatch function takes one or more functions as arguments. It attempts to invoke each in turn, until a defined value is returned.

This is an example of a polymorphic function: a function that exhibits different behaviors based on its argument.

dispatch = (functions...) ->
  (target, args...) ->
    for f in functions
      result = f(target, args...)
      return result if result?

str = dispatch(
  invoker(Array::toString)
  invoker(String::toString)
)

ok str('beta') is 'beta'
ok str([1..4]) is '1,2,3,4'

stringReverse = (s) ->
  return undefined if not _.isString s
  s.split('')
   .reverse()
   .join('')

ok stringReverse('xyz') is 'zyx'
ok stringReverse(10000) is undefined

reverse = dispatch(
  invoker(Array::reverse)
  stringReverse
)

eq reverse([1, 2, 3]), [3, 2, 1]
ok reverse('a b c d') is 'd c b a'

we can exploit the contract of dispatch to compose a terminating function that provides some default behavior.

rev = dispatch(
  reverse,        # a func created by dispatch can also be an arg 
  -> 'DEFAULT'    # our terminating function w/ default behavior
)

eq rev([1, 2, 3]), [3, 2, 1]
ok rev('a b c d') is 'd c b a'
ok rev(1) is 'DEFAULT'

Manual dispatch is often handled with a switch statement. For example, the following perform function uses a switch statement to look at the type attribute on a command object, using it to dispatch the relevant code.

warn = (msg) -> 'WARNING: ' + msg
explode = (msg) -> msg + '!!!'

perform = (command) ->
  switch command.type
    when 'warning' then warn(command.message)
    when 'bomb' then explode(command.message)
    else 'DEFAULT'

warning = 
  type: 'warning'
  message: 'be careful!'

explosion = 
  type: 'bomb'
  message: 'BOOM'

nothing = 
  type: 'unknown'
  message: 'none'

ok perform(warning)   is 'WARNING: be careful!'
ok perform(explosion) is 'BOOM!!!'
ok perform(nothing)   is 'DEFAULT'

Now consider how this could be handled by dispatch.

isa = (type, action) ->
  (obj) ->              # returns undefined if type doesn't match
    action(obj.message) if type is obj.type

perform = dispatch(
  isa('warning', warn)  # try next sub-function if undefined
  isa('bomb', explode)
  -> 'DEFAULT'
)

ok perform(warning)   is 'WARNING: be careful!'
ok perform(explosion) is 'BOOM!!!'
ok perform(nothing)   is 'DEFAULT'

Note that we'd have to go in and change the actual switch statement in the hardcoded version, but we can simply extend our dispatch version.

performBetter = dispatch(
  isa('unknown', -> '???')
  isa('silent',  -> '')
  perform
)

ok performBetter(warning)         is 'WARNING: be careful!'
ok performBetter(explosion)       is 'BOOM!!!'
ok performBetter(nothing)         is '???'
ok performBetter(type: 'silent')  is ''
ok performBetter(type: null)      is 'DEFAULT'

Currying

A curried function is one that returns a new function for every logical argument that it takes.

divBy = (d) ->
  (n) -> n / d

divByTen = divBy 10

ok divByTen(200) is 20

Auto Currying Parameters

The following curry function takes a function and returns a function expecting one parameter.

curry = (f) ->
  (arg) -> f arg

We can use curry to make parseInt mappable by constraining it to a single parameter, forcing it to receive only one argument on each call.

int = curry(parseInt) 

eq ['1', '2', '3'].map(int), [1, 2, 3]

Now consider curry2 which takes a function and curries it up to two parameters deep.

curry2 = (f) ->
  (b) -> 
    (a) ->
      f(a, b)

div = (n, d) -> n / d

###
var div10 = curry2(div)(10);

div10(50);
//=> 5

var parseBinaryString = curry2(parseInt)(2);

parseBinaryString("111");
//=> 7

parseBinaryString("10");
//=> 2

var plays = [{artist: "Burial", track: "Archangel"},
            {artist: "Ben Frost", track: "Stomp"},
            {artist: "Ben Frost", track: "Stomp"},
            {artist: "Burial", track: "Archangel"},
            {artist: "Emeralds", track: "Snores"},
            {artist: "Burial", track: "Archangel"}];

_.countBy(plays, function(song) {
  return [song.artist, song.track].join(" - ");
});

//=> {"Ben Frost - Stomp": 2,
//    "Burial - Archangel": 3,
//    "Emeralds - Snores": 1}

function songToString(song) {
  return [song.artist, song.track].join(" - ");
}

var songCount = curry2(_.countBy)(songToString);

songCount(plays);
//=> {"Ben Frost - Stomp": 2,
//    "Burial - Archangel": 3,
//    "Emeralds - Snores": 1}

function curry3(fun) {
  return function(last) {
    return function(middle) {
      return function(first) {
        return fun(first, middle, last);
      };
    };
  };
};

var songsPlayed = curry3(_.uniq)(false)(songToString);

songsPlayed(plays);

//=> [{artist: "Burial", track: "Archangel"},
//    {artist: "Ben Frost", track: "Stomp"},
//    {artist: "Emeralds", track: "Snores"}]

function toHex(n) {
  var hex = n.toString(16);
  return (hex.length < 2) ? [0, hex].join(''): hex;
}

function rgbToHexString(r, g, b) {
  return ["#", toHex(r), toHex(g), toHex(b)].join('');
}

rgbToHexString(255, 255, 255);
//=> "#ffffff"

var blueGreenish = curry3(rgbToHexString)(255)(200);

blueGreenish(0);
//=> "#00c8ff"

var greaterThan = curry2(function (lhs, rhs) { return lhs > rhs });
var lessThan    = curry2(function (lhs, rhs) { return lhs < rhs });

var withinRange = checker(
  validator("arg must be greater than 10", greaterThan(10)),
  validator("arg must be less than 20", lessThan(20)));

function divPart(n) {
  return function(d) {
    return n / d;
  };
}

var over10Part = divPart(10);
over10Part(2);
//=> 5

function partial1(fun, arg1) {
  return function(<args>) {
    var args = construct(arg1, arguments);
    return fun.apply(fun, args);
  };
}

function partial2(fun, arg1, arg2) {
  return function(<args>) {
    var args = cat([arg1, arg2], arguments);
    return fun.apply(fun, args);
  };
}

var div10By2 = partial2(div, 10, 2)

div10By2()
//=> 5

function partial(fun, <pargs>) {
  var pargs = _.rest(arguments);

  return function(<arguments>) {
    var args = cat(pargs, _.toArray(arguments));
    return fun.apply(fun, args);
  };
}

var zero = validator("cannot be zero", function(n) { return 0 === n
});
var number = validator("arg must be a number", _.isNumber);

function sqr(n) {
  if (!number(n)) throw new Error(number.message);
  if (zero(n))    throw new Error(zero.message);

  return n * n;
}

function condition1(<validators>) {
  var validators = _.toArray(arguments);

  return function(fun, arg) {
    var errors = mapcat(function(isValid) {
      return isValid(arg) ? [] : [isValid.message];
    }, validators);

    if (!_.isEmpty(errors))
      throw new Error(errors.join(", "));

    return fun(arg);
  };
}

var sqrPre = condition1(
  validator("arg must not be zero", complement(zero)),
  validator("arg must be a number", _.isNumber));

function uncheckedSqr(n) { return n * n };

uncheckedSqr('');
//=> 0

var checkedSqr = partial1(sqrPre, uncheckedSqr);

var sillySquare = partial1(
  condition1(validator("should be even", isEven)),
  checkedSqr);

var validateCommand = condition1(
  validator("arg must be a map", _.isObject),
  validator("arg must have the correct keys", hasKeys('msg',
'type')));

var createCommand = partial(validateCommand, _.identity);

var createLaunchCommand = partial1(
  condition1(
    validator("arg must have the count down", hasKeys('countDown'))),
  createCommand);

var isntString = _.compose(function(x) { return !x }, _.isString);

isntString([]);
//=> true

function not(x) { return !x }

var composedMapcat = _.compose(splat(cat), _.map);

composedMapcat([[1,2],[3,4],[5]], _.identity);
//=> [1, 2, 3, 4, 5]

var sqrPost = condition1(
  validator("result should be a number", _.isNumber),
  validator("result should not be zero", complement(zero)),
  validator("result should be positive", greaterThan(0)));

var megaCheckedSqr = _.compose(partial(sqrPost, _.identity),
checkedSqr);

###