Thursday, January 19, 2017

Function.prototype.map III

This is starting to sound like the band Chicago. First they came up with the album "Chicago", then "Chicago II",  then "Chicago III", and so on.  I want to assure you this is the last blog-post in my series of "Function.prototype.map" -blog-posts.  But this needs to be written ...

If you want to catch up with the previous episodes, they are Function.prototype.map  and Function.prototype.map II.

1.  WHAT MORE CAN THERE BE?

So Function.prototype.map can take as argument either an Array OR any (non-Array) object, and knows how to handle them appropriately. This raises the question are there any other argument-types it could beneficially handle in a similar or at least somewhat coherent manner? What if we gave it a Function as an argument? What should that do? That is the subject of this 3rd blog-post in this series, which I aim to make short, not a double-album ...

We can look for answer on what Function.prototype.map() should do when given a Function argument by looking at the  type-signatures of the two previously provided extensions:
  • Function.prototype.map (Array)   ->   Array
  • Function.prototype.map (Object)  ->  Object
From the above, an answer suggests itself:
  • Function.prototype.map (Function)  ->  Function

I think it is clear that should return a function, to preserve commonality with the other two cases.  But what function?  Notice this should be a method of any function, taking as argument some other function. What should the resulting function do, in general?   What's the most basic way to use two existing functions to produce a third function as a result?

To make the question more concrete, if
  funkA . map (funkB) -> funkC

then how should we produce funkC from funkA and funkB?

The best answer I believe is "Function Composition".  The argument to funkC is first passed to funkA, whose result is then passed as argument to funkB, whose result is then returned  finally as the result of funkC.

Note that this puts some constraint on the type signatures of functions funkA and funkB to be combined: It must be possible to call the second function funkB with the result of the first function funkA. We  need to be aware of this constraint when we go about our jolly ways of combining functions.

However there is one thing we can do to make different functions more "recombinable": Allow a function to return undefined IF it does not know how to handle its argument - OR if it more simply wants to state the answer is undefined.  For instance if you try to divide something by zero it makes a lot of sense to say the answer is undefined.

If we allow for undefined results then the combination rule can be amended as follows:
IF the first function funkA returns undefined, the second function funkB is NOT called with it and  the result of funkC will be undefined as well.


2. EXAMPLE III  

The first example below shows how it is possible to "map a function to itself". This is possible IF the argument-type and result-type of the function are the same:

  var times8 = double.map(double)
                     .map(double);
  ok (times8 (1) === 8 );
  ok (times8 (2) === 16);

The function double() expects a number as argument and returns a number that is double the argument, so this is clearly the case here. And clearly there is no limit as to how many times we can combine such a function with itself. The external inputs just keeps traveling though the "pipeline" created by the combination, doubling in value at each stage. You can think of the above example as there "amplifiers" connected in series to produce and 8-fold increase in the volume of our album "Function.prototype.map III".


The second  example shows how we can take advantage of the "Do not pass undefined information on" -amendment we gave to our function-composition rule.

Function "odd()" returns its argument number if it is odd, else it returns undefined. If we combine it with the function double() we get double the argument for every odd number, and undefined for every even number given as argument. From that the following ensues:

  var doubleOdds     =  odd.map(double);
  ok (doubleOdds(0) === undefined);
  ok (doubleOdds(1) === 2);
  ok (doubleOdds(2) === undefined);
  ok (doubleOdds(3) === 6);

 function odd (n)
 { if (n % 2) {return n}
 }
 function double (n)
 { return n * 2;
 }
 function odd (n)
 { if (n % 2) {return n}
 }
 function even (n)
 { if (n % 2 === 0) { return n;}
 }
 function ok (b)
 { if (!b)
   { throw "assert failed";
   }
 }

3. IMPLEMENTATION

The full  source-code and tests for Function.prototype.map() which implements the examples in this and previous blog-posts is available at GitHub and also at the Node.js npm -repository. The latter means that if you have Node.js installed you can get the code simply by executing:
  • npm install fpmap
If you plan to use this code on client-side JavaScript and don't have Node.js installed you can get it by downloading from GitHub (see "6. LINKS" below). The GitHub  -page also shows you more extensive test examples than above, so it may be worth a look.


5. DISCUSSION

So what have we now?  We have a function you can use wherever you use functions, to  transform and filter both Arrays and Objects. And by passing in a function as argument you can now do also "function composition".

To avoid any ambiguity,  let's refer to the implementation of Function.prototype.map discussed in this and previous blog-posts as "fpmap". Since that name now exists in the global npm-registry, it's a fairly good unique name for it.

One potential issue you might have against using fpmap is that it requires an addition to the JavaScript built-in object Function.prototype. This is potentially problematic if multiple sources provide similar additions to the same built-in objects of JavaScript. Something named Function.prototype.map might even be part of the EcmaScript -standard one day, who knows.

To make potential name-conflicts less of a problem, our implementation "fpmap" will cause an error if you try to install it but it detects that Function.prototype already has the property "map" . You will then see an error so you will immediately know there's a conflict.

What you can do then is install "fpmap" under a different name, say "map2". We propose other writers of base-object extensions should follow the same convention:  Never assign a property to built-in JavaScript objects if they already have it,  and always provide a way for the client to choose the name they want to install the extension as. The  README.md -file on GitHub gives an example of how to install "fpmap" under a different method-name in Function.prototype.

6. LINKS

GitHuh:   https://github.com/panulogic/fpmap
Npm:      https://www.npmjs.com/package/fpmap


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

No comments:

Post a Comment