Functional programming is a tricky beast but I am finding it unexpectedly liberating. Here's an example that might help you make progress too.
It has a different sense of reasoning and problem solving concepts from how most of us learnt to code. I've gone through the process of developing (essentially) vanilla JavaScript apps with jQuery (fine until you have to maintain your code). And (fortunately?) only started to consider React once Hooks arrived (previously React seemed neither one thing nor another).
Suddenly the benefits of functional programming (FP) seemed well worth investigating properly.
Learning FP
I read a couple books (this one and another), plus a few Medium and DEV articles. Then took a more direct step and went through this whole Udemy course. I must admit to skipping some of the later exercises - I find it really tough to focus sufficiently on problems for their own sake. Need a real issue to implement!
So it was lucky that I was in the latter stages of developing an app which has a very data hungry modal dialog (not yet in production release, will try to update later with a link). So I built this using the framework applied in the Udemy course - and it was very liberating. The framework isn't React but has similarities, with virtual DOM, data driven state, protection from mutation, clean event loops and complex UI that is easy to update or extend.
But
After the reading and the course, did I really think I'd "got" it? By which I mean the fundamental reasoning and problem solving concepts - things like:
- no data mutation
- no
for
loops - pure functions
Somewhat...
Understanding these in principle is fine, but it takes practice, as for any code reasoning, to re-configure your problem solving processes.
The data mutation thing I got first, although it is tightly linked to the concept of pure functions:
- Keep functions simple
- Think in terms of individual functional steps rather than integrated bundles of logic
- Return new versions of your data
- Make sure to avoid library functions which mutate the passed-in data (e.g.
Array.slice()
rather thanArray.splice()
)
I can't say my code is perfect in these regards but I think I'm doing OK at knowing when there is a problem and whether I care.
Drop the loop
Dropping for
loops has been the trickiest. Recursion has always filled me with dread and examples of map()
and reduce()
are typically quite simple. They give you the technical syntax but not really the reasoning process you need in order to actually use them.
Finally 'getting it'
This morning I did a rare thing and shouted "Yes! OMG it actually worked!" when a piece of refactored code worked the first time. A joyous occasion. I won't claim that the refactor is perfect FP but for me the diff is instructive - hope you find it so as well.
The code below is training a neural network (using Brain.js) and returning some test results to a web page. In the try
block, the CSV data file is processed (get the training and test data) followed by returning the results via the res
object. I used a forEach
loop to iterate through the CSV data, responding according to need with some nested if/else control logic.
So I started with this procedural tangle:
exports.start = function (res) { // res is the Express response object, being the server package I used | |
let training = true | |
let trainingDataSet = [] | |
let testResult = [] | |
const network = new NN.NeuralNetwork(); | |
let dataTypes = [] | |
let listOfTrainingData = [] | |
let listOfTestData = [] | |
let trainingDataRaw = [] | |
let testDataRaw = [] | |
var parser = csv({ delimiter: ',' }, function (err, data) { | |
if (err) { | |
return null | |
} else { | |
data.forEach(function (element, index) { // data[][] represents elements of each row in the CSV | |
if (index === 0) { | |
// header row | |
element.forEach(function (header) { | |
dataTypes.push(header) | |
}) | |
} else { | |
if(element[0] === 'TEST') { | |
// flag start of test data | |
training = false | |
} else { // last 3 rows (elements) of data[][] are test data | |
if(training) { | |
listOfTrainingData.push(element[0]) // first element is a unique designator, not data (is for user feedback) | |
trainingDataRaw.push(element.slice(1)) | |
} else { | |
listOfTestData.push(element[0]) // designator | |
testDataRaw.push(element.slice(1)) | |
} | |
} | |
} | |
}) | |
trainingDataRaw.forEach((data) => { | |
const input = data.slice(0, data.length - 1) | |
const output = data.slice(data.length - 1) | |
trainingDataSet.push({ input: input, output: output }) | |
}) | |
network.train(trainingDataSet, { /* some config settings */ }) | |
testDataRaw.forEach((dataPoint) => { | |
const input = dataPoint.slice(0, dataPoint.length - 1) | |
const output = dataPoint.slice(dataPoint.length - 1) | |
testResult.push({ actual: network.run(input), desired: output }) | |
}) | |
} | |
}) | |
try { | |
const stream = fs.createReadStream(path.join(__dirname, 'data.csv')) | |
stream.pipe(parser) | |
stream.on('close', () => { | |
res.send(testResult) | |
}) | |
stream.on('error', (error) => { | |
throw(error) | |
}) | |
} catch (err) { | |
res.send(err) | |
} | |
} |
Which I turned into the following, whilst also adding some additional logic to analyse the neural network (a bit). The long
if
statement in the parser()
function has been simplified to a few lines, kicking off a recursive function. I also replaced the two forEach
loops which processed the raw training and test data with functions definedTrainingDataset()
and generatedTestResults()
using map()
. Finally, the new functionality identifySignificantInputs()
I added to process the network's weights uses a reduce()
within a map()
to iterate (or map) through the hidden nodes and sum up (or reduce) each node's set of weights.exports.start = function (res) { // res being the Express response object, being the server package I used | |
const network = new NN.NeuralNetwork(); | |
let dataTypes = [] | |
let listOfTrainingData = [] | |
let listOfTestData = [] | |
let trainingDataRaw = [] | |
let testDataRaw = [] | |
const trainingOptions = { /* config options */ } | |
var parser = csv({ delimiter: ',' }, function (err, data) { // in general, I find processing of arbitrary data files hard to do with pure functions - so I took some short cuts... | |
if (err || data.length === 0) { | |
return null | |
} else { | |
dataTypes = data[0].slice(0) // not very pure | |
parseTrainingData(data.slice(1)) // this should normally return the completed data - see in-function comments below | |
} | |
}) | |
try { // not really pure either, but I don't know a better way of handling contact with the file system. At least the event-driven code is all in one place | |
const stream = fs.createReadStream(path.join(__dirname, 'data.csv')) | |
stream.pipe(parser) | |
stream.on('close', () => { | |
network.train(definedTrainingDataset(trainingDataRaw), trainingOptions) | |
const nnConfig = network.toJSON() | |
const significantInputs = identifySignificantInputs(nnConfig) | |
fs.writeFileSync('savedNN.json', JSON.stringify(nnConfig, null, 2)) | |
res.send({ msg: '', result: generatedTestResults(testData), keyInputData: significantInputs, testedData: listOfTestData }) | |
}) | |
stream.on('error', (error) => { | |
throw(error) | |
}) | |
} catch (err) { | |
res.send({ msg: err }) | |
} | |
function parseTrainingData (data) { | |
if (data[0][0] === 'TEST') { | |
return parseTestData(data.slice(1)) | |
} else if (data.length === 0) { | |
return // normally a recursive function would return the completed data, but see next comment for why not in this case | |
} else { | |
listOfTrainingData.push(data[0][0]) // not very pure but I'm OK with it for now. Hard to return two new arrays and then two more test data arrays | |
trainingDataRaw.push(data[0].slice(1)) | |
return parseTrainingData(data.slice(1)) | |
} | |
} | |
function parseTestData (data) { | |
if (data.length === 0) { | |
return // see above regarding return values | |
} else { | |
listOfTestData.push(data[0][0]) // not very pure either! | |
testDataRaw.push(data[0].slice(1)) | |
return parseTestData(data.slice(1)) | |
} | |
} | |
function identifySignificantInputs (nnConfigData) { | |
const listOfHiddenNodes = Object.values(nnConfigData.layers[1]) | |
const summedWeights = listOfHiddenNodes.map((node) => { | |
return Object.values(node.weights).reduce(function sumWeights (sum, weight) { | |
return sum + weight | |
}) | |
}) | |
const numWeights = summedWeights.length > 10 ? 10 : summedWeights.length | |
const topTen = summedWeights.sort().slice(summedWeights.length - numWeights) | |
return topTen.map((summedWeight) => { // what is actually wanted are the indexes of the highest 10 summedWeights values | |
return dataTypes[summedWeights.indexOf(summedWeight)] // then match these to the original data (column) names | |
}) | |
} | |
function definedTrainingDataset(rawData) { | |
return rawData.map((data) => { | |
const input = data.slice(0, data.length - 1) | |
const output = data.slice(data.length - 1) // the CSV defines the desired neural network output as the last item in a row | |
return { input, output } // Brain.js specifies this type of object for the training set | |
}) | |
} | |
function generatedTestResults(dataToTest) { | |
return dataToTest.map((dataPoint) => { | |
const input = dataPoint.slice(0, dataPoint.length - 1) | |
const output = dataPoint.slice(dataPoint.length - 1) // output is last item in a row | |
return { actual: network.run(input), desired: output } // not pure, as running the test function here - probably should segregate this better | |
}) | |
} | |
} |
Now I just need to write it this way first time rather than with a re-factor!
For reference, here is the (edited for brevity) neural network definition object that contains the weights.
{ | |
"sizes": [ | |
86, | |
43, | |
1 | |
], | |
"layers": [ | |
{ | |
"0": {}, | |
"1": {}, | |
"2": {}, | |
... | |
"86": {} | |
}, | |
{ | |
"0": { | |
"bias": -0.13822075724601746, | |
"weights": { | |
"0": 0.01626661792397499, | |
"1": -0.014252233318984509, | |
"2": 0.14316223561763763, | |
"3": -0.005704993382096291, | |
"4": 0.08960812538862228, | |
"5": -0.07037261873483658, | |
"6": 0.1102287769317627, | |
"7": -0.17131270468235016, | |
"8": 0.1448451578617096, | |
"9": 0.18123319745063782, | |
"10": -0.21595078706741333, | |
"11": 0.16244560480117798, | |
"12": 0.18404613435268402, | |
"13": 0.012663952074944973, | |
"14": 0.09024368226528168, | |
"15": 0.12085451930761337, | |
"16": 0.0732698142528534, | |
"17": 0.08153338730335236, | |
"18": 0.1552974134683609, | |
"19": -0.13225726783275604, | |
"20": -0.08330784738063812, | |
"21": 0.09186286479234695, | |
"22": -0.009232715703547001, | |
"23": -0.056838080286979675, | |
"24": -0.10421989113092422, | |
"25": 0.1351219266653061, | |
"26": 0.12975627183914185, | |
"27": 0.013017627410590649, | |
"28": -0.1065634936094284, | |
"29": 0.16817930340766907, | |
"30": 0.11511798948049545, | |
"31": -0.18439318239688873, | |
"32": 0.1905558705329895, | |
"33": -0.1223924532532692, | |
"34": -0.22114628553390503, | |
"35": -0.19365231692790985, | |
"36": 0.1344785988330841, | |
"37": -0.18410953879356384, | |
"38": 0.0191449373960495, | |
"39": -0.130402609705925, | |
"40": 0.01331572886556387, | |
"41": -0.008699173107743263, | |
"42": -0.045819781720638275, | |
"43": -0.1605120450258255, | |
"44": -0.19833286106586456, | |
"45": 0.132829487323761, | |
"46": 0.1518602967262268, | |
"47": 0.12389596551656723, | |
"48": 0.08297079801559448, | |
"49": -0.02383382059633732, | |
"50": -0.10610071569681168, | |
"51": -0.020823169499635696, | |
"52": 0.2035674899816513, | |
"53": 0.026980873197317123, | |
"54": 0.1825440227985382, | |
"55": 0.10305899381637573, | |
"56": -0.14173683524131775, | |
"57": -0.006001485977321863, | |
"58": 0.17707911133766174, | |
"59": -0.1882091760635376, | |
"60": 0.11771080642938614, | |
"61": 0.18571224808692932, | |
"62": 0.23855449259281158, | |
"63": -0.06563503295183182, | |
"64": 0.013349037617444992, | |
"65": 0.2220417559146881, | |
"66": -0.19291900098323822, | |
"67": -0.18360714614391327, | |
"68": 0.05112316086888313, | |
"69": -0.19002340734004974, | |
"70": 0.11394902318716049, | |
"71": 0.1132209524512291, | |
"72": -0.12355688214302063, | |
"73": 0.25294962525367737, | |
"74": 0.12676000595092773, | |
"75": 0.08274034410715103, | |
"76": -0.11590045690536499, | |
"77": -0.06358099728822708, | |
"78": -0.11478631943464279, | |
"79": -0.12867148220539093, | |
"80": -0.11935573816299438, | |
"81": 0.19714365899562836, | |
"82": -0.03580832853913307, | |
"83": -0.16706402599811554, | |
"84": -0.018236307427287102, | |
"85": -0.032433465123176575 | |
} | |
}, | |
"1": { | |
"bias": -0.09457044303417206, | |
"weights": { | |
"0": 0.11502128094434738, | |
"1": 0.13233497738838196, | |
"2": -0.027493087574839592, | |
"3": 0.07776865363121033, | |
"4": 0.0005186432390473783, | |
"5": 0.10062018781900406, | |
"6": 0.028584711253643036, | |
"7": -0.16444814205169678, | |
"8": -0.15763556957244873, | |
"9": 0.10692765563726425, | |
"10": -0.1232132762670517, | |
"11": 0.002933673094958067, | |
"12": -0.05483340099453926, | |
"13": -0.13444729149341583, | |
"14": 0.12234074622392654, | |
"15": -0.018780484795570374, | |
"16": -0.1764172613620758, | |
"17": 0.12729257345199585, | |
"18": -0.046653684228658676, | |
"19": -0.02079034596681595, | |
"20": -0.1849730908870697, | |
"21": -0.04714925214648247, | |
"22": 0.14319291710853577, | |
"23": 0.18708735704421997, | |
"24": -0.03688087686896324, | |
"25": -0.24342691898345947, | |
"26": 0.15561501681804657, | |
"27": -0.12565959990024567, | |
"28": -0.032889265567064285, | |
"29": 0.03274199366569519, | |
"30": -0.016166439279913902, | |
"31": 0.03706303983926773, | |
"32": -0.01598576456308365, | |
"33": -0.060167424380779266, | |
"34": 0.19731394946575165, | |
"35": 0.21079258620738983, | |
"36": -0.14061422646045685, | |
"37": 0.13996517658233643, | |
"38": -0.09339942783117294, | |
"39": 0.16470806300640106, | |
"40": -0.02393459714949131, | |
"41": 0.015549899078905582, | |
"42": -0.021044177934527397, | |
"43": -0.10714207589626312, | |
"44": -0.04133755713701248, | |
"45": -0.16351251304149628, | |
"46": 0.12275877594947815, | |
"47": -0.17323607206344604, | |
"48": -0.09378547966480255, | |
"49": 0.12095615267753601, | |
"50": -0.1753319799900055, | |
"51": -0.16305837035179138, | |
"52": -0.02579011395573616, | |
"53": -0.1394176334142685, | |
"54": -0.06377433240413666, | |
"55": 0.04703257977962494, | |
"56": -0.08987922966480255, | |
"57": -0.02432684227824211, | |
"58": -0.22190327942371368, | |
"59": 0.007550704758614302, | |
"60": -0.036614611744880676, | |
"61": 0.06484035402536392, | |
"62": -0.15090155601501465, | |
"63": 0.10355693101882935, | |
"64": -0.07487975060939789, | |
"65": -0.0004842017951887101, | |
"66": 0.05667002871632576, | |
"67": -0.10775730013847351, | |
"68": 0.12731462717056274, | |
"69": -0.19711409509181976, | |
"70": 0.2430098056793213, | |
"71": 0.07079452276229858, | |
"72": -0.1494113653898239, | |
"73": 0.002838666085153818, | |
"74": -0.10328685492277145, | |
"75": 0.04060981422662735, | |
"76": 0.12824909389019012, | |
"77": -0.03996357321739197, | |
"78": 0.09312918782234192, | |
"79": 0.07005716115236282, | |
"80": -0.1734434962272644, | |
"81": -0.10565968602895737, | |
"82": -0.01453770138323307, | |
"83": 0.09094598144292831, | |
"84": -0.12657825648784637, | |
"85": -0.14522922039031982 | |
} | |
}, | |
... | |
} | |
}, | |
{ | |
"0": { | |
"bias": -0.14548051357269287, | |
"weights": { | |
"0": 0.46635428071022034, | |
"1": -0.3777349293231964, | |
"2": 0.4673900902271271, | |
"3": 0.27385595440864563, | |
"4": -0.15480433404445648, | |
"5": -0.30337128043174744, | |
"6": 0.7689602375030518, | |
"7": 0.007116327993571758, | |
"8": -0.27507278323173523, | |
"9": 0.22069519758224487, | |
"10": -0.11079428344964981, | |
"11": -0.35586822032928467, | |
"12": -0.219791978597641, | |
"13": 0.6696177124977112, | |
"14": -0.5856861472129822, | |
"15": 0.46236351132392883, | |
"16": -0.6647834777832031, | |
"17": -0.5241032242774963, | |
"18": -0.4557340443134308, | |
"19": 0.5680762529373169, | |
"20": 0.09254752844572067, | |
"21": 0.2549460530281067, | |
"22": 0.6144667863845825, | |
"23": 0.41409969329833984, | |
"24": 0.20454536378383636, | |
"25": 0.7999222874641418, | |
"26": 0.1358945220708847, | |
"27": 0.4663151800632477, | |
"28": 0.4671632647514343, | |
"29": -0.009167474694550037, | |
"30": -0.15144474804401398, | |
"31": -0.08641974627971649, | |
"32": -0.601782500743866, | |
"33": -0.3245511054992676, | |
"34": -0.01279737614095211, | |
"35": -0.5905151963233948, | |
"36": -0.4151318371295929, | |
"37": 0.1177016943693161, | |
"38": -0.08814592659473419, | |
"39": -0.582362711429596, | |
"40": -0.1289762407541275, | |
"41": -0.5143193006515503, | |
"42": -0.1801266074180603 | |
} | |
} | |
} | |
], | |
"outputLookup": false, | |
"inputLookup": false, | |
"activation": "sigmoid", | |
"trainOpts": { | |
"iterations": 10000, | |
"errorThresh": 0.00005, | |
"log": true, | |
"logPeriod": 100, | |
"learningRate": 0.3, | |
"momentum": 0.1, | |
"callbackPeriod": 100, | |
"beta1": 0.9, | |
"beta2": 0.999, | |
"epsilon": 1e-8 | |
} | |
} |
Top comments (0)