- Post History
- Subscribe to RSS Feed
- Mark as New
- Mark as Read
- Bookmark
- Subscribe
- Printer Friendly Page
- Report Inappropriate Content
on 10-25-2020 05:22 PM
This post introduces an alternative construct to replace a for loop. It leverages JavaScript5 native behavior to help transform code into scalable, flexible, and readable work. A code fragment will be refactored using Array.prototype.filter to remove hard-binding of its algorithm and boilerplate syntax.
On readability
Como todo conocimiento, aunque sensillo, Todo se vera extrano si nunca se ha visto previamente.
The Spanish sentence above is very simple to a Spanish speaker. To others, it's simplicity is difficulty. Not really because it isn't simple but, because it's new information. That is the case for all knowledge, even basic, simple constructs seem difficult and unreadable. To appreciate the beauty and possibilities of new knowledge we must learn more about topics we may already believe to know well.
That is how code should be viewed, can new ways of achieving the same, transform idiomatic code into human code. The code refactoring here will attempt to do that and more.
Idiomatic for loop
function getOddNumbers (numbers) {
var oddNumbers = [];
for (var x = 0; x < numbers.length; x++) {
if (numbers[x] % 2) {
oddNumbers.push(numbers[x]);
}
}
return oddNumbers;
}
var numbers = [1, 2, 3, 4, 5, 6, 7, 8];
var oddNumbers = getOddNumbers(numbers);
gs.debug(oddNumbers); //1,3,5,7
The snippet above is a very basic piece of code. What does it do? Easy to read, the name alludes to the contents. But how can this piece of code be improved for both readability and scalability?
Of interest:
1. Remove the need for mutation oldNumbers
2. Remove what it means for a value to be true
3. Remove binding to a seed value (oddNumbers = []) (limits what it means to work with the loop)
4. Continuous need to write for (var x =0..). just to loop through a different set of values
5. Idiomatic programmatic can be cleaned up
6. It breaks a Single Responsibility principle in that instead of doing just one thing it does 4
a. loops through values
b. determines when to apply an algorithm
c. determines a seed value
d. determines how to work with the seed value
7. creates duplication by necessity
8. will lead to a unnecessary code surface
etc...
The items listed above limit flexibility. They also lead getOddNumbers to abandoning human syntax in favor of idiomatic programming that will be unequivocally duplicated over and over. While there is nothing wrong with being idiomatic and there is such a need to a certain degree, that code can be adjusted to where there is only need tell it what it means to:
1. be a seed value
2. identify a predicate
3. react to a predicate condition
Refactoring the original code fragment
JavaScript native behavior will be coupled to low level abstractions that carry out work to increase the chances for human readable syntax, and scalable code. The original snippet will be broken into single units of work, and utilities used to share what it means to loop through values when a predicate is needed.
Note: that this an incomplete solution quickly put together as demo, therefore not fully mature, nor fully refactored solution for optimal interpretation times.
Step one:
Replace the for loop with Array.prototype.reduce to allow passing in the behavior we want, and not hardcode it inside the for loop.
for (var x = 0; x < length; x++ ) {
}
//vs
Array.replace
Step two:
Extract predicate behavior evaluated within the for loop into it's own predicate function
if ( numbers[x] % 2 ) {
}
//vs
function isOdd (value) {
return value % 2 ? true : false;
}
Step three:
Extract the algorithm executed within the if that pushes a value into the array with a strategy function that does the same thing but without mutating the original array oddNumbers.
oddNumbers.push( numbers[x]);
/vs
function concatValues (collector, value) {
return collector.concat(value)
}
Because JavaScript Array.prototype.reduce higher order function works with multiple parameters, two parameters are used. One to collect the result each time through a loop cycle, and the other is the value to be collected.
So far this is what the code looks like
//The if statement becomes
function isOdd (number) {
return number % 2 ? true : false;
}
//to strategy of what it means to add values to an array when the if is true becomes
function concatValues (collector, value) {
return collector.concat(value)
}
//and something we want to do reduce like
array.reduce { function (collector, something) {
if (isOdd(someting) {
concatValues(something)
} else {
??
}
},[]);
Step four:
A low level utility function that puts it all together and can be leveraged by Array.protottype.reduce
function arrayPredicate (predicateFn, collectorFn) {
return function applyAgainstReducer (reducer, value) {
return predicateFn(value) ?
collectorFn(reducer, value) :
reducer;
};
}
Low level code is where the gut of the operation happens. This is where all that machine language code belongs, where advanced JavaScript is written. Deep down in places far away from the beginning of a process. This is where more involved skill is required to craft utilities that will allow human grammar to be used by top layers. Expected at this level is for beginners to struggle more identifying constructs such as curried functions, higher order functions, partial application, etc. This low level is also where documentation comes is viable (how is the function used), and likely where comments inside the code itself can be found stating why something is happening. Remember, comments say why logic happens, not what it does nor how; what and how are done by code itself.
arrayPredicate function is curried function (a function that returns a function) that is used as arguments for Array Higher Order functions. In the first function execution, two parameters stand for the predicate function (a function that returns true/false), and the second that determines how to handle a true result of the predicate function in the first parameter.
The second function execution is used as an input to Array.prototype.reduce where it will be executed against each value in an array. It's body executes the functions initially passed in during the first function execution (they stand for what it is to mean true/false, and what it means to react to a true result). Do note that this is an incomplete strategy as it doesn't allow for an algorithm to react what to do when the predicate evaluates to false.
Step five:
Define a utility that converts an array reducer into a predicate function. It needs to work by passing in strategies (algorithms to execute) rather than embedding them as the original fragment at the top of this article did.
function filterArray (arr, predicateStrategy, seedValue) {
return arr.reduce(predicateStrategy, seedValue);
}
filterArray is the entry point that applies a predicate strategy (what is and how to react to a true result) to an array. It also asks for a seed value as to allow for flexibility, and not force new functionality just to react to the result of a predicate. Step four and Step five are what will allow the replacement of any for loop that uses a predicate function to carry out work. Filters they are called in JavaScript Array vernacular. This means that there is no longer need for High Level code (functionality at the beginning of the process) to share low level code characteristics. Those layers at the top can grown into a more human readable syntax.
With the low level logic satisfied, it's time to look at how the low level lcode impacts the layers closer to the beginning of the process.
function getOddNumbers (numbers) {
var collectOddNumbersStrategy = arrayPredicate(isOdd, concat);
return filterArray(numbers, collectOddNumbersStrategy, []);
}
The for loop is replaced by the functions that say in plain English what it means to be an odd number and, how to handle that odd number. It also gives a seed value of [] in which to collect the result of collectAddNumbersStrategy. This is the chance to study functions names to assess if they reveal the precise articulation sought. If they don't, it's time to change them. Because predicate is part of English language and grammar, arrayPredicate seems okay. Yet, it really is up to author to determine what name is most suitable. The function went from JS dialect to human syntax resemblance. It also went from 5 lines to 2. In practice it could become a single line by using tacit programming. If it needs to be broken down into multiple pieces to better convey meaning, so be it. Do at this level.
function getOddNumbers (numbers) {
return filterArray(numbers, arrayPredicate(isOdd, concatValues), []);
}
The beauty is as follows. Code has been reduced from 5 lines to 1. Chances of bugs in one line of code are less than with 1+ lines. Because functions have become single purpose, they are extremely small, allowing a reader to fully concentrate on what the function does independently of everything else. Keep in mind that because this function is closer to the beginning of the process, there is no care to how it does something, the goal is to see what it does. It filters an array by applying a conditional function, and concats the values. If we don't like how it looks, refactoring at this point is also trivial.
Also, because functions are injected, there is no dependency on outside code. That behavior belongs in another place completely as to gain maximum flexibility. There is no more need to write for loops with predicate behavior, those have already been accounted for in about 10 lines of code. Future code is then fully concentrates on what it means for a value to be true, and how that value is to be collected. There is no need to retest low level libraries because we know they work, if there is an error, it is with the newly introduced functions; hence, only those functions are tested independently of everything else, granting further flexibility even when testing. Function getOddNumbers is no longer needed either but, it's a lot friendlier to have a named function as an entry point than it is to interpret what a multi-parameter function call-site stands for.
Maybe hidden in plain sight is that this solution is scalable to any type of object, limiting future code to handling what it means handle the objects that are going through a loop. This is a function to execute against a value in a loop, and the other to handle how to put together that result. Authors now concentrate on what inputs and outputs are needed, rather than how to step over them as well.
For example, we got odd numbers already but, what if there is a need for even numbers? A need to add the odd/even/all numbers in an array? Because boilerplate is already accounted for in low level libraries, there is no need for loops, nor conditional statements. It's time to determine what it means to get even numbers, what it means to add odd/even/all numbers and bging coding those cases.
Lets see how some of those cases could be handled.
Functions to determine even numbers and to add numbers
function isEven (number) {
return !isOdd(number);
}
function isNumber (value) {
return typeof value === 'number';
}
function add (collector, value) {
return collector + value;
}
Reuse isOdd to determine even numbers, introduce a function that knows what it means to be a number. And to add, a basic sum operation.
Its time to build some reader friendly named functions to carry out adding odd/even/all numbers. Even print them out.
Since our code doesn't know what it means to print a number out. Lets create that utility function first
function log (collector, value) {
gs.debug(value);
collector.push(value);
return collector;
}
with some utility functions to account for even numbers, integers, adding values, even logging them, the impact on what would have been many loops with embedded conditions and duplicate algorithms to collect data, transform reusing existing functions.
function getEvenNumbers (numbers) {
return filterArray(numbers, arrayPredicate(isEven, concatValues), []);
}
function printOddNumbers (numbers) {
return filterArray(numbers, arrayPredicate(isNumber, log), []);
}
function addAllNumbers (numbers) {
return filterArray(numbers, arrayPredicate(isNumber, add), 0);
}
function addEvenNumbers (numbers) {
return filterArray(numbers, arrayPredicate(isEven, add), 0);
}
function addOddNumbers (numbers) {
return filterArray(numbers, arrayPredicate(isOdd, add), 0);
}
This is how functions would be called:
var numbers = [1, 2, 3, 4, 5, 6, 7, 8];
gs.debug(' even numbers -> '+ getEvenNumbers(numbers));
gs.debug(' odd numbers -> '+ getOddNumbers(numbers));
gs.debug(' sum of even numbers -> ' + addEvenNumbers(numbers));
gs.debug(' sum of odd numbers -> ' + addOddNumbers(numbers));
gs.debug(' sum of all numbers -> ' + addAllNumbers(numbers));
gs.debug(' printed numbers -> ' + printOddNumbers(numbers));
and the output
: even numbers -> 2,4,6,8 (sys.scripts extended logging)
: odd numbers -> 1,3,5,7 (sys.scripts extended logging)
: sum of even numbers -> 20 (sys.scripts extended logging)
: sum of odd numbers -> 16 (sys.scripts extended logging)
: sum of all numbers -> 36 (sys.scripts extended logging)
: 1 (sys.scripts extended logging)
: 2 (sys.scripts extended logging)
: 3 (sys.scripts extended logging)
: 4 (sys.scripts extended logging)
: 5 (sys.scripts extended logging)
: 6 (sys.scripts extended logging)
: 7 (sys.scripts extended logging)
: 8 (sys.scripts extended logging)
: printed numbers -> 1,2,3,4,5,6,7,8 (sys.scripts extended logging)
It is the potential, the scalability and varied articulation that makes clean code easier to maintain. Once key functionality is in place, it's time to play around with names to hone-in what best suits the thoughts being expressed. A function breaks, pull it out to be studied and debugged independently of all else. No need to have to interpret, or haul in outside context, even how a function fits as part of a larger object, how a long piece of code is affected by the bottom, middle and top. See it in a few lines and move on to something more worthy of your time.
My apologies for the long post, hope it helps into the possibilities of what this platform allows us to do.
Happy SNowing....
- 1,671 Views
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
The complete code:
function isOdd (number) {
return number % 2 ? true : false;
}
function isEven (number) {
return !isOdd(number);
}
function isNumber (value) {
return typeof value === 'number';
}
function concatValues (collector, value) {
return collector.concat([value]);
}
function log (collector, value) {
gs.debug(value);
collector.push(value);
return collector;
}
function add (collector, value) {
return collector + value;
}
function arrayPredicate (predicateFn, collectorFn) {
return function applyAgainstReducer (reducer, value) {
return predicateFn(value) ?
collectorFn(reducer, value) :
reducer;
};
}
function filterArray (arr, predicateStrategy, seedValue) {
return arr.reduce(predicateStrategy, seedValue);
}
function getOddNumbers (numbers) {
return filterArray(numbers, arrayPredicate(isOdd, concatValues), []);
}
function getEvenNumbers (numbers) {
return filterArray(numbers, arrayPredicate(isEven, concatValues), []);
}
function printOddNumbers (numbers) {
return filterArray(numbers, arrayPredicate(isNumber, log), []);
}
function addAllNumbers (numbers) {
return filterArray(numbers, arrayPredicate(isNumber, add), 0);
}
function addEvenNumbers (numbers) {
return filterArray(numbers, arrayPredicate(isEven, add), 0);
}
function addOddNumbers (numbers) {
return filterArray(numbers, arrayPredicate(isOdd, add), 0);
}
var numbers = [1, 2, 3, 4, 5, 6, 7, 8];
gs.debug(' even numbers -> '+ getEvenNumbers(numbers));
gs.debug(' odd numbers -> ' + getOddNumbers(numbers));
gs.debug(' sum of even numbers -> ' + addEvenNumbers(numbers));
gs.debug(' sum of odd numbers -> ' + addOddNumbers(numbers));
gs.debug(' sum of all numbers -> ' + addAllNumbers(numbers));
gs.debug(' printed numbers -> ' + printOddNumbers(numbers));