Tacit Ramda
Some programming languages, especially among those which haven't gained great popularity, are puzzles. James Hague (Puzzle Languages)
Functional programming in JavaScript took a foothold with Underscore, which as of today March 7th 2015, is the most depended package on npm, fallowed by Lodash as #3. Both Jeremy Ashkenas and John-David Dalton did a great job bringing functional flavor to the JavaScript world, for that I’m very grateful.
Lately I've been playing a lot with Ramda, JavaScript library that promises purer functional style wrapped in a practical package that could be used in your daily projects. Plus I have a bitter sweet relationship with its authors, who never gave me what I've asked, but they always delivered what I need.
Ramda arguments are flipped for the reason best presented by Brian Lonsdorf talk Hey Underscore, You’re Doing It Wrong! and its functions are partially applied by default. Together with lack of optional arguments and no dispatching on type Ramda enables writing code in tacit aka pointfree style. Those things though not new, remained in the domain of the experimental and academic libraries.
You could find Ramda at http://ramdajs.com/ which for convenience has an online REPL that enables you to try the below code examples at http://ramdajs.com/repl/ . OK enough talking, lets start coding.
Curry
Ramda functions are automatically curried, thus enabling you to create new functions by omitting arguments.
var add5 = R.add(5); // http://ramdajs.com/docs/#add
add5(4) //=> 9var double = R.multiply(2)
double(6);//=> 12
And if you need your own functions to be curryed use R.curry
// http://en.wikipedia.org/wiki/Heron%27s_formula
var heron = R.curry(function heron(a,b,c){
var s= (a+b+c)/2;
return Math.sqrt(s*(s-a)*(s-b)*(s-c));
});
R.map(heron(3,4),[5,7,9])//=> [6,0,NaN]
The first issue with currying comes with anti-commutative functions, such as divide, subtract & modulo. In curried form its usually the first argument that we want to omit. For such uses Ramda provides R.__ placeholder
var minus5 = R.subtract(R.__,5);
minus5(9) //=> 4var half= R.divide(R.__,2);
half(6); //=> 3var isOdd = R.modulo(R.__,2);
isOdd(3) //=> 1
Though its usage is consistent throughout the library this R.__ placeholder introduces some kind of ugliness thus having a version with flipped arguments looks better for this trio of functions.
For this purpose Ramda provides R.flip which returns a new function much like the supplied one, except that the first two arguments’ order is reversed.
var subtractBy = R.flip(R.subtract)
var minus5 = subtractBy(5)
minus5(9) //=> 4var divideBy = R.flip(R.divide)
var half = divideBy(2)
half(6); //=> 3var moduloBy = R.flip(R.modulo)
var isOdd = moduloBy(2)
isOdd(3) //=> 1
Ramda actually had above three functions in the previous versions but they were omitted to keep the library leaner.
Classics
The most well known higher order functions map, filter & reduce are curried as well in Ramda.
// double elements
var doubleAll = R.map(R.multiply(2))
doubleAll([1,2,3]) //=> [2,4,6]var odds = R.filter(R.modulo(R.__,2))
odds([1,2,3,4]) //=> [1,3]var flatten = R.reduce(R.concat,[]);
flatten([[1,2],[3,4]])//=>[1,2,3,4]
Compose
Now that we covered the basics lets get to the part where we are creating more interesting functions. Our first tool is compose which creates a new function that runs each of the functions supplied as parameters in turn, passing the return value of each function invocation to the next function invocation, beginning with whatever arguments were passed to the initial invocation. Yes I've copy pasted the documentation but example will clear the concept.
var triplePlusOneAbs = R.compose(Math.abs,R.add(1),R.multiply(3))
triplePlusOneAbs(-2) //=> 5
// which is more or less
function triplePlusOneAbs(x){
return Math.abs(R.add(1,R.multiply(3,x)));
}
triplePlusOneAbs(-2) //=> 5
Note that rightmost function could take several arguments, the rest are limited to one.
var rectanglePerimeter = R.compose(R.multiply(2),R.add)
rectanglePerimeter(2,3);//=>10
One nice technique when using pure functions is being able to memoize the results.
var oneToN = R.compose(R.range(1),R.add(1))
oneToN(5) //=> [1,2,3,4,5]var factorial = R.memoize(R.compose(R.product,oneToN))
factorial(5) //=> 120
How about splitting the string into uppercase tokens
var tokenize = R.compose(R.split(' '),R.toUpper);
tokenize("Mary had a little lamb")
//=>['MARY','HAD','A','LITTLE','LAMB']
Converge
Converge also known as monadic fork in the J world is probably the most well known of the higher order forms
var avg = R.converge(R.divide,R.sum,R.length)
avg([1,2,3,4,5,6,7])//=> 4var isDuplicate = R.converge(R.not(R.eq),R.indexOf,R.lastIndexOf)
isDuplicate(1,[2,1,3,1,6]) //=> true
isDuplicate(2,[2,1,3,1,6]) //=> false
The best explanation how it works is probably to see how its implemented
var converge = R.curry(function(f,g,h,x,y) {
return f(g(x,y),h(x,y));
});
The other higher order form I’ve found quite useful is monadic noun fork
var monadNounFork = R.curry(function monadicNounFork(f, g,x, y){
return f(g(x),y);
});var listPlusN = monadNounFork(R.map,R.add);
listPlusN(10, [1,2,3,4,5]);//=> [11,12,13,14,15]
But let’s try it on accessing data. Here’s an example I’ve shamelessly ripped of the Hardcore Functional Programming in JavaScript. We have an array containing articles. We want to create a function that extracts all the author names and another one that checks is someone an author.
var monadNounFork = R.curry(function monadNounFork(f, g,x, y){
return f(g(x),y);
});var articles = [
{
title: 'Everything Sucks',
url: 'http://do.wn/sucks.html',
author: {
name: 'Debbie Downer',
email: 'debbie@do.wn'
}
},
{
title: 'If You Please',
url: 'http://www.geocities.com/milq',
author: {
name: 'Caspar Milquetoast',
email: 'hello@me.com'
}
}
];
var authors = R.map(R.compose(R.prop('name'), R.prop('author')));
authors(articles);
// =>['Debbie Downer','Caspar Milquetoast']
var isAuthor = R.flip(monadNounFork(R.flip(R.contains), authors));
isAuthor('New Guy', articles)
isAuthor('Caspar Milquetoast', articles)
The higher form worked and we achieved our goal without using parameters. However all those flips are sign of a code smell. We are trying to fit tacit style into the problem where the tacit solution isn’t obvious. That makes it a puzzle.
As a parting gift I’ll left with you with an exercise for the reader.
We have a list of elements, some are duplicates. We’re trying out how to find the duplicate elements and increase a counter value by 1 for each instance of the element found. The list consists of lists with two elements, the first being the incremental counter, the second being the string sought.
http://www.nsl.com/papers/kisntlisp.htm
var example= [[1,"one"],[1,"two"],[1,"three"],[1,"one"],[1,"four"],[1,"two"]];
var brunberg = R.compose(R.mapObj(R.length),R.groupBy(R.nth(1)))
brunberg(example)
// =>"{"one":2,"two":2,"three":1,"four":1}"
Your solution should produce
var sought = [[2,"one"],[2,"two"],[1."three"],[1,"four"]];
Update:
As suggested by Scott Sauyer @CrossEye there is a simpler definition for isAuthor using R.useWith :
var isAuthor = R.useWith(R.contains, R.identity, authors);
Here’s my solution for the final exercise:
var example= [[1,”one”],[1,”two”],[1,”three”],[1,”one”],[1,”four”],[1,”two”]];
var brunberg = R.compose(R.map(R.reverse),R.toPairs,R.mapObj(R.length),R.groupBy(R.nth(1)))
JSON.stringify(brunberg(example));
// => [[2,"one"],[2,"two"],[1,"three"],[1,"four"]]