I'm just learning functional programming in JavaScript using Ramda.
I took this tutorial and implemented the server core.js component using Ramda.
The idea is that we'll have a collection of things to vote from: Movies, songs, programming languages, Horse JS quotes, anything. The app will put them against each other in pairs, so that on each round people can vote for their favorite of the pair. When there's just one thing left, that's the winner.
I attempted to use the point-free style as much as possible as I found this really forced me to avoid writing imperative code. I also avoided making any changes to the data structure defined by the tutorial though I felt that doing so could have potentially simplified some things quite a bit. In real projects we can't always change an API.
Here is the data structure used:
(source: teropa.info)
Here's my code:
const R = require('ramda');
// overCalculatedValue :: Lens s a -> ( s -> (a -> a)) -> ( s -> s)
const overCalculatedValue = R.curry( (lens, func) => {
return R.converge( R.over(lens), [func, R.identity]);
});
// setCalculatedValue :: Lens s a -> ( s -> a) -> ( s -> s)
const setCalculatedValue = R.curry( (lens, func) => {
return R.converge( R.set(lens), [func, R.identity]);
});
// createEntryTallyLens :: string -> Lens s a
const createEntryTallyLens = R.useWith( R.lensPath, [R.flip(R.append)(['vote', 'tally'])] );
// isVotingOnLastCandidates :: State -> boolean
const isVotingOnLastCandidates = R.compose( R.equals(0), R.length, R.prop('entries'));
// pickWinner :: [[string],number] -> [ string, number] -> [[string],number]
const pickWinner = R.curry( ( result, candidate ) => {
return result[1] === candidate[1] ? [ R.append(candidate[0], result[0] ), result[1]] :
result[1] < candidate[1] ? [ [candidate[0]], candidate[1] ] : result;
});
const NoWinner = [[],-Infinity];
// getWinnerList :: State -> [string]
const getWinnerList = R.compose( R.nth(0), R.reduce(pickWinner, NoWinner), R.toPairs(), R.path(['vote', 'tally']));
// isWinnerDetermined :: State -> boolean
const isWinnerDetermined = R.compose( R.equals(1), R.length, getWinnerList);
// getOverallWinner :: State -> string
const getOverallWinner = R.compose(R.nth(0), getWinnerList);
// getNextCandidates :: State -> [string]
const getNextCandidates = R.compose(R.take(2), R.prop('entries'));
// addWinnerToList :: State -> ([string]->[string])
const addWinnerToList = R.compose(R.flip(R.concat()), getWinnerList);
// createNewVote :: State -> State
const createNewVote = R.compose( R.over(R.lensProp('entries'), R.drop(2)), setCalculatedValue(R.lensPath(['vote', 'pair']), getNextCandidates), R.omit('vote'));
// addWinnerBackToEntries :: State -> State
const addWinnerBackToEntries = overCalculatedValue( R.lensProp('entries'), addWinnerToList);
// doesWinnerExist :: State -> boolean
const doesWinnerExist = R.allPass([isVotingOnLastCandidates, isWinnerDetermined]);
// declareWinner :: State -> State
const declareWinner = R.compose( R.omit(['entries', 'vote']), setCalculatedValue( R.lensProp('winner'), getOverallWinner));
// setupNextElection :: State -> State
const setupNextElection = R.compose( createNewVote, addWinnerBackToEntries);
// setEntries :: State -> [string] -> State
exports.setEntries = (state, entries) => {
return R.set(R.lensProp('entries'), entries, state);
};
// vote :: State -> string -> State
exports.vote = ( state, entry ) => {
return R.over( createEntryTallyLens(entry), R.compose(R.inc, R.defaultTo(0)), state);
};
// startNextVote :: State -> State
exports.startNextVote = R.ifElse(doesWinnerExist, declareWinner, setupNextElection);
My questions:
- Is converge with set or over the correct way to update a value in an object that must be calculated from other values in the object?
- Any other areas where this could be simplified or improved?