Thursday, January 5, 2017

Function.prototype.map


1. PROPOSAL

This blog-post unofficially proposes a new standard API-function for JavaScript: Function.prototype.map. While it may not become part of any official standard ever, you can use it to make your code simpler in some cases, by copying the code for it from below.

The specification for Function.prototype.map is simple because it is stated in terms of  the existing Array.prototype.map:

  • IF for any array arrayX and function functionX the expression arrayX.map ( functionX, arg2 )  produces result arrayY, THEN   functionX.map ( arrayX,      arg2 )  must produce the same result except  without any undefined elements.



2. EXAMPLE

[1,2,3].map(doubleF);   // -> [2,4,6], standard 
doubleF.map([1,2,3]);   // -> [2,4,6], proposed
ifOddF .map([1,2,3]);   // -> [1,3]  , proposed

function doubleF (currentValue, index, array)
{ return currentValue * 2;
}

function ifOddF (currentValue, index, array)
{ if (currentValue % 2)
  { return currentValue;
  } 
}


3. IMPLEMENTATION

Below shows a simple implementation of Function.prototype.map. The first paragraph of code does the equivalent of what Array.prototype.map would do, if given the "recipient-function" as its argument. The 2nd code-paragraph removes undefined elements before returning the  result without them:

Function.prototype.map = 
function ()
{ var args    = [].slice.call (arguments);  
  var args2   = [this].concat (args.slice(1));  
  var result1 = [].map.apply  (args[0], args2);  
  var result2 = [];  

  for (var j=0; j < result1.length; j++)  
  { var e = result1[j];    
    if (e !== undefined) 
    { result2.push(e);    
    }   
  }   
  return result2;


4. MOTIVATING EXAMPLE

Below code-example shows the function filesOfCurrentDir()which  returns an array containing the absolute path-names of all files in the current directory in Node.js. We want to know what are the files apart from sub-directories. Node.js does not have a simple API for doing this, so we need to create our own.

Node.js does have the  function  Fs.readdirSync() for listing the elements of a directory-path given as argument. That returns an array of the names of the directory components including files and the sub-directories. But we want more than the names, we want path-names which uniquely identify their corresponding files.   

The solution is to create another function  ifFile() and call the standard function bind() with it as argument to return a derived function which will always call the original function with the 2nd argument of bind() as the first argument of  ifFile()That sounds complicated.  Therefore, it's worth the while to try to make everything else as simple as possible. We can use Function.prototype.map() to help do that.   

The code for filesOfCurrentDir() below shows two ways of trying to get the files in the directory, the first of which doesn't quite work:

  var filesA = fileAndDirNames                         // A
.map (ifFile.bind (null, currentDir));  

Above uses the standard Array.prototype.map() to call  a function derived with bind() from ifFile() for each file- and directory -name  in the current directory. We want the result to be an Array of the absolute path-names of files in the directory.

But there's a problem: ifFile() is meant to filter out  directory names by returning undefined if a path-name points to a directory. Therefore filesA now contains the absolute path-names of every file in the directory PLUS an undefined value for every sub-directory of it. 

We could easily write a loop for removing the undefined values. However we get that done automatically if we use  Function.prototype.map() instead, like this:     

var filesB = ifFile.bind (null, currentDir)            // B
                   .map  (fileAndDirNames);    

So one reason to use Function.prototype.map() is that it does more, it removes undefined values automatically. Which is what you usually want, not much you can do with undefined array-elements. If you do want undefined array-elements you can always fall back on Array.prototype.map to give them to you. In Object Oriented Programming you can have your cake and eat it too!     

A more fundamental reason I prefer Function.prototype.map() over its Array-cousin  in this case, is seen by comparing the code-structure of  the  two alternatives. The first one contains nested parenthesis, the second doesn't!


Trying to understand an expression that contains nested parenthesis is more difficult and laborious and easier to misunderstand. When reading the first example (A) from left to right you can't understand what the whole expression does without first understanding what the argument expression does, then you have to "back up" to restart calculating the value of the containing expression. 
  
Whereas reading the 2nd expression (B) from left to right you can parse and understand what each step independently does because there are no "inner expressions".  Just know that the data flows from left to right, like in a pipeline.

You could rewrite the first example to get rid of nested parenthesis by taking the inner expression out and storing it into a temporary variable which you then use as argument. But then you would have to make a similar "jump back" to recall what was the value you stored into the variable. And more variables means more locations that can hold a wrong value. There's a cost to every variable.


function filesOfCurrentDir ()

  var Path            = require('path');  
  var Fs              = require('fs')  ; 
  var currentDir      = __dirname      ;                
  var fileAndDirNames = Fs.readdirSync (currentDir);
 
  var filesA =  fileAndDirNames  
.map (ifFile.bind (null, currentDir));
  
  // Above ALMOST does it, but the array 'filesA' 
  // contains undefined for each sub-directory. 
  // Using Function.prototype.map() we get rid of
  // those automatically. NOTE: Above call should
  // be removed after studying it and understanding 
  // why it does not work.       

  var filesB  = ifFile.bind (null, currentDir)       
                    .map  (fileAndDirNames);    
  return filesB ;


  function ifFile (currentDir, fileOrDirName)
  { 
    var path = Path.join(currentDir, fileOrDirName);
    if (Fs.lstatSync(path).isFile())    
    { return path;                    
    } 
    // else: return nothing, a.k.a undefined
  }

}


4. DISCUSSION

The JavaScript API of Function would seem to be an obvious candidate for extension, for improved support for Functional Programming in JavaScript.

The downside of per-programmer extensions which add something to the basic JavaScript classes of course is that if everybody comes up with their own extension, there will be conflicts between the implementations.  But the extension proposed above is simple and  benefits of it substantive enough that I've found no reason not to use it.

Having Function.prototype.map available in addition to Array.prototype.map helps us get rid of nested argument-expressions without having to add extra temporary variables which would require more lines of code.

Stripping away undefined values allows us to use Function.prototype.map for not only transforming arrays but also for filtering out elements from the results of the transformation.

Note what the function Function.prototype.map presented here does is in no way dependent on what method-name of Function.prototype it is installed as. You can decide which method-name to use for it on a module-by-module basis. What an extension-function does is dependent on which prototype it is is installed in.

5. HOW TO USE IT 

Copy the code-segment from Section 3. to the start of your JavaScript-file. After that you can call  'map()' on any function with any array as argument.



6. UPDATE !

A newer  blog-post Function.prototype.map II  provides and describes an improved implementation for Function.prototype.map, with the ability to loop over non-arrays as well


7. LINKS 

If you 're not familiar with some of the external functions used in the code-example you can find their documentations here:

1. Array.prototype.map:  https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map

2. Function.prototype.bind:   https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind

3. fs.readdirsync:   https://nodejs.org/api/fs.html#fs_fs_readdirsync_path_options

4. fs.lstatsync:  https://nodejs.org/api/fs.html#fs_fs_lstatsync_path

_____________________________________________________________
Copyright © 2017 Panu Viljamaa. All rights reserved unless otherwise noted.
Reuse of code examples in this blog-post is granted under the terms of 
Creative Commons Attribution 4.0 International (CC BY 4.0) -license 

No comments:

Post a Comment