5
\$\begingroup\$

In this code, I tried to integrate my little knowledge of (and great fascination towards) functional programming. I'd like to make this code more functional, and faster, if that's possible.

'use strict';
var insert = require('./insert');
const STEP = 50;
const WIDTH = 200, HEIGHT = 100;
var ptr = [[0, 1, 0, 0, 0, 0, 0],
 [0, 0, 0, 1, 0, 0, 0],
 [1, 1, 0, 0, 1, 1, 1]];
function init(scr) {
 var cwObj = {
 canvas: document.getElementById(scr),//set up the graphics department
 gMap: new Array(HEIGHT).fill(new Array(WIDTH).fill(0))
 };
 draw(cwObj, ptr, 30, 60);
 step(cwObj.gMap);
 return cwObj;
}
function draw(cwObj, pattern, x = 0, y = 0) {
 if(pattern){//draw pattern to map
 pattern.map(function (cur, i){
 cwObj.gMap[x + i] = insert(cwObj.gMap[x + i], cur, y);
 //insert() is a helper, just inserts at an index 
 });
 }
 var out = '';
 cwObj.gMap.map(function (cur) { //create a string of active and 
 cur.map(function (cell) { //inactive cells 
 if(cell) {out += "<b>&#9632;</b>";} else {out += "&#9632;";}
 });
 out += "<br>";
 });
 cwObj.canvas.innerHTML = out; //put it in the HTML
}
function step (gm){
 return gm.map(function (cur, y) {
 return cur.map(function (cell, x){
 var n = getNb(gm, x, y);
 n = cell === 1 ? n - 1 : n;//getNb includes the cell itself
 return n === 3 ? 1 : ((n === 2 && cell === 1) ? 1 : 0);//rules
 });
 });
}
function getNb (gm, x, y){
 var w = WIDTH, h = HEIGHT,
 xmin = (x <= 0) ? 0 : x-1, ymin = (y <= 0) ? 0 : y-1,//handle borders
 xmax = (x+2 > w) ? w : x+2, ymax = (y+2 > h) ? h : y+2;
 var env = gm.slice(ymin, ymax).map(function (row){//get environment array
 return row.slice(xmin, xmax);
 });
 env = [].concat.apply([], env);//flatten array
 return env.reduce(function (prev, cell) {//sum the neighbors
 return prev += cell;
 });
}
var Game = init("screen");
setInterval(function (){//repeat the steps every 50ms
 Game.gMap = step(Game.gMap);
 draw(Game);
}, STEP);
Mast
13.8k12 gold badges57 silver badges127 bronze badges
asked Sep 1, 2016 at 1:06
\$\endgroup\$

2 Answers 2

2
\$\begingroup\$
const STEP = 50;
const WIDTH = 200, HEIGHT = 100;
var w = WIDTH, h = HEIGHT,
 xmin = (x <= 0) ? 0 : x-1, ymin = (y <= 0) ? 0 : y-1,//handle borders
 xmax = (x+2 > w) ? w : x+2, ymax = (y+2 > h) ? h : y+2;

Be consistent. I usually recommend a const/let/var per variable and a line each to to make it easy to add and remove variables as needed. No surgical removal of variables in between commas and it's easy to spot the variables.


var Game = init("screen");
setInterval(function (){//repeat the steps every 50ms
 Game.gMap = step(Game.gMap);
 draw(Game);
}, STEP);

The problem with above is that it's not kosher in terms of functional programming. Each step is mutating Game, specifically Game.gmap. If I remember correctly, the functional way to create a game loop is to use recursion and construct a new game state on each step.

function gameLoop(gameState){
 draw(gameState);
 const newState = update(gameState);
 return gameLoop(newState);
}
return gameLoop(createInitialState());

The big difference is that you don't have Game or Game.gmap that mutates on each step. Each step takes in an entirely different state object than the previous. This is inefficient given existing JS data structures, but can be optimized using persistent data structures.

But JS has no TCO yet and you'll blow away the stack if you do it in existing engines (Node.js has it under a flag). The closest you can get to maintaining a similar code structure is by scheduling the next step asynchronously using setTimeout.

function gameLoop(gameState){
 draw(gameState);
 const newState = update(gameState);
 setTimeout(() => gameLoop(newState), STEP);
}
gameLoop(createInitialState());

function init(scr) {
 var cwObj = {
 canvas: document.getElementById(scr),//set up the graphics department
 gMap: new Array(HEIGHT).fill(new Array(WIDTH).fill(0))
 };
 draw(cwObj, ptr, 30, 60);
 step(cwObj.gMap);
 return cwObj;
}

I would suggest this init function only construct state. Drawing should just be a part of the game loop.

function createInitialState(){
 return { /* initial game state */ }
}

pattern.map(function (cur, i){
 cwObj.gMap[x + i] = insert(cwObj.gMap[x + i], cur, y);
 //insert() is a helper, just inserts at an index 
});
var out = '';
cwObj.gMap.map(function (cur) { //create a string of active and 
 cur.map(function (cell) { //inactive cells 
 if(cell) {out += "<b>&#9632;</b>";} else {out += "&#9632;";}
 });
 out += "<br>";
});

You are misusing array.map. array.map is meant to be a 1:1 transform of each item in an array into another. What you're doing here is a job for array.forEach. And since array.forEach does nothing but loop and return undefined, it has to be mutating something outside the callback, making it not functional.

Your first operation could be done with an array.map as it's essentially updating each value with a new value. You will have to replace the array, not mutate the existing one.

cwObj.gMap = pattern.map((current, i) => updatedValueForValueAt(i));

The second one appears to be a job for array.reduce as it is reducing the array into a single string.

const out = cwObj.gMap.reduce((prev, cur) => prev + cur.map(c => c? '<b>&#9632;</b>' : '&#9632;').join(''), '');

JS isn't entirely functional and you will often break rules where necessary. At best, you keep mutation to a minimal.

answered Sep 2, 2016 at 5:08
\$\endgroup\$
1
  • \$\begingroup\$ Very helpful answer, though some of the codes you supplied don't work. I can't create the whole map via going over the pattern array. It'll just return 3 arrays, no more. But you're absolutely right, I am misusing map. \$\endgroup\$ Commented Sep 2, 2016 at 11:58
2
\$\begingroup\$

A few things to make it more functional:

  1. Avoid if statements.
  2. Prefer functions over setting temporary variables and making calculations based on those.
  3. Rather than deal with exceptions, reframe the problem so that the exceptional circumstances are no longer exceptional.

Examples of 2. and 3. come up in the way you treat border cells. An alternative is to get all the neighbors of every cell, and then filter out invalid ones. Here's an example of that approach. Also note: The main algorithm is a pure function -- we take in a grid and return a new grid.

function nextGeneration(grid) {
 var deltas = [[-1,-1], [-1,0], [-1,1], [0 ,-1], [0 ,1], [1 ,-1], [1 ,0], [1 ,1]],
 m = grid.length,
 n = grid[0].length,
 validIndex = (i,j) => i >= 0 && j >= 0 && i < m && j < n,
 numNeighbors = (i,j) => deltas.map(d => [d[0]+i, d[1]+j])
 .filter(ij => validIndex.apply(null, ij))
 .reduce((m,ij) => m + grid[ij[0]][ij[1]], 0),
 nextLive = n => n == 2 || n == 3,
 nextDead = n => n == 3,
 show = isLive => isLive ? 1 : 0;
 return grid.map( (row, i) => row.map( (isLive, j) => {
 var n = numNeighbors(i, j);
 return isLive ? show(nextLive(n)) : show(nextDead(n));
 }));
}
test = [
 [1,0,1,0],
 [1,1,1,0],
 [1,0,0,0]
];
console.log(nextGeneration(test));

On another note, you should use more descriptive names. Short is ok, but it should be clear what they mean.

answered Sep 2, 2016 at 5:16
\$\endgroup\$
4
  • \$\begingroup\$ Perfectly functional! I appreciate that a lot. Although, sadly, this version is slower(1.5x) than the original. \$\endgroup\$ Commented Sep 2, 2016 at 12:22
  • \$\begingroup\$ If speed is a concern, I would probably do this quite differently, even if I were sticking to a functional style. But note functional code will nearly always lose to hand-optimized procedural code in a speed contest. Functional code buys you readability, simplicity, and maintainability, not speed (in general). All that said, you might try defining all the helper functions using function -- some engines will recreate them on every call as they are now. Also define the deltas as a one-time constant outside the function. You may see a boost after making those changes. \$\endgroup\$ Commented Sep 2, 2016 at 12:49
  • \$\begingroup\$ One final recommendation: If you are interested in exploring functional code in js more deeply, I highly recommend you check ramda.js and experiment with writing code like this in a point free style. I could probably get my above sample to be half as long and even more readable with ramda.... \$\endgroup\$ Commented Sep 2, 2016 at 12:51
  • \$\begingroup\$ Thank you for your input. I'll try ramda.js, but I find it fair to accept the more detailed answer. \$\endgroup\$ Commented Sep 3, 2016 at 22:25

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.