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
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'
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
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);
###