Let's say you have a script that process an array with multiple transformations to finally compute your result.
"use strict";
const input = "732829320";
const valid =
Array
.from(input)
.map(character => parseInt(input) || 0)
.map((digit, index) => index % 2 === 0 ? digit : digit * 2)
.map(digit => digit > 9 ? digit - 9 : digit)
.reduce((sum, digit) => sum + digit) % 10 === 0;
console.log(valid);
As you can see, we are using a lot of Array methods to ease the development of our script that could have been really verbose if written without these methods. But still, there is a problem and impossible to know what is happening since I'm really tired and it is late night. The valid
variable stores the false
value, even if our input is valid.
By the way, I am here validating a SIREN number, which is a special enterprise identification number used in France to identify companies. You don't need to understand what is going on here but for those who are curious, it uses the Luhn algorithm, which is the same algorithm used to validate VISA's credit card numbers.
Maybe you tried something like that.
"use strict";
const input = "732829320";
const valid =
Array
.from(input)
.forEach(character => console.log(character))
.map(character => parseInt(input) || 0)
// Error: cannot read property map of undefined
.map((digit, index) => index % 2 === 0 ? digit : digit * 2)
.map(digit => digit > 9 ? digit - 9 : digit)
.reduce((sum, digit) => sum + digit) % 10 === 0;
console.log(valid);
WARNING: this is not production-ready code. Don't copy/paste it to solve this problem in your applications! It is just a pretext to give you an example of how the tap
method can be used here.
Unfortunately, it won't work due to the fact that the forEach
method will return undefined, meaning that it cannot be chained by others calls to map
, filter
, reduce
, etc...
But we could store values for each steps and just log the output of each. That's a solution.
"use strict";
const input = "732829320";
const array = Array.from(input);
console.log(array);
const digits = array.map(character => parseInt(input) || 0);
console.log(digits);
const multiplied = digits.map((digit, index) => index % 2 === 0 ? digit : digit * 2)
console.log(multiplied);
const digitSum = multiplied.map(digit => digit > 9 ? digit - 9 : digit);
console.log(digitSum);
const sum = digitSum.reduce((sum, digit) => sum + digit);
console.log(sum);
const valid = sum % 10 === 0;
console.log(valid);
But it is really verbose, like a lot. And I had to come up with new names for my variables, which is something I wasted time since I will not use these except for the purpose of logging them.
But it works, and I finally managed to figure why I had an error. The second log for the digits
variable gives me something like that:
[ 732829320,
732829320,
732829320,
732829320,
732829320,
732829320,
732829320,
732829320,
732829320 ]
Which is weird at first glance since I was expecting to turn all my characters to a single digit. But in reality, I am parsing the input
variable, instead of the character
variable. So here is my error. I found it and successfully validated my script.
"use strict";
const input = "732829320";
const valid =
Array
.from(input)
.map(character => parseInt(character) || 0)
.map((digit, index) => index % 2 === 0 ? digit : digit * 2)
.map(digit => digit > 9 ? digit - 9 : digit)
.reduce((sum, digit) => sum + digit) % 10 === 0;
console.log(valid);
But can we do better? Yes! By using a tap
method. In a nutshell, and in this case, a tap
method will help you loop through your array, without touching it, and will return it to be chained in others calls. If you didn't understand, that's okay. An example is worth a hundred words.
"use strict";
const input = "732829320";
const valid =
Array
.from(input)
.tap(character => console.log(character))
.map(character => parseInt(character) || 0)
.map((digit, index) => index % 2 === 0 ? digit : digit * 2)
.map(digit => digit > 9 ? digit - 9 : digit)
.reduce((sum, digit) => sum + digit) % 10 === 0;
console.log(valid);
As you can see, we are using the tap
method to log our characters, before they can be mapped to numbers in the next map
call. All I did was to branch my tap
method between those calls and tada, we got a logging of our data without even having to make a mess in our code. The tap
method here will produce the following output.
7
3
2
8
2
9
3
2
0
And we can keep going and branch our tap
method as much as we want since by definition, it will always return the same thing, meaning an array of data.
Let's be crazy and branch it everywhere.
"use strict";
const input = "732829320";
const valid =
Array
.from(input)
.tap(character => console.log(character))
.map(character => parseInt(character) || 0)
.tap(character => console.log(character))
.map((digit, index) => index % 2 === 0 ? digit : digit * 2)
.tap(character => console.log(character))
.map(digit => digit > 9 ? digit - 9 : digit)
.tap(character => console.log(character))
.reduce((sum, digit) => sum + digit) % 10 === 0;
console.log(valid);
Of course, this will log out many things, maybe not the best way to debug our code but it is an example of how far you could go with this method. And of course, you could shorten this call by passing console.log
as a first-class function.
"use strict";
const input = "732829320";
const valid =
Array
.from(input)
.tap(console.log)
.map(character => parseInt(character) || 0)
.map((digit, index) => index % 2 === 0 ? digit : digit * 2)
.map(digit => digit > 9 ? digit - 9 : digit)
.reduce((sum, digit) => sum + digit) % 10 === 0;
console.log(valid);
Or do anything else with it! But remember that it will always return the array untouched, so even if you try to update the array, this won't return the updated values to the next chained call!
Ok, ok... I will now show you how to implement this so called tap
method. First of all, we need to augment the capabilities of the Array
object in JavaScript to be able to chain call the tap
method like that.
Array.prototype.tap = function() {
// ...
};
Now, we need to find a way to get the array that we want to iterate over. We can do that by using the this
keyword to get the full array. Let's use a for...of
loop to loop over each elements of that array.
Array.prototype.tap = function() {
for (const element of this) {
// ...
}
};
Now we need to do something... As you can see, in the previous examples, we passed a function as a first-class citizen. So looks like we are getting a callback as our parameter. Let's use this callback by passing it the current iterated element of our array.
Array.prototype.tap = function(callback) {
for (const element of this) {
callback(element);
}
};
Last thing we want to do in order to prevent breaking the chain of calls we made earlier is to return the array untouched. Since the for...of
loop won't update the array here, we can safely return the this
keyword that will reference the original array here.
Array.prototype.tap = function(callback) {
for (const element of this) {
callback(element);
}
return this;
};
But nothing tells us that the people behind the ECMAScript standard won't implement a tap
method as part of the Array
prototype. Maybe they will read this article and think about how useful this function is! And if you keep your script as is, and use a newer (hypothetic) version of JavaScript that implement such a functionality, you may end up breaking your script as this definition will clash with the standard definition. We need to add a special guard to prevent such cases from happening.
if (!Array.prototype.tap) {
Array.prototype.tap = function(callback) {
for (const element of this) {
callback(element);
}
return this;
};
}
Ah! That's better. We could also make the for...of
loop a one liner using the forEach
method of Arrays instead. Since this
is an array, it can easily be used for this purpose, just for the sake of saving some bytes.
if (!Array.prototype.tap) {
Array.prototype.tap = function(callback) {
this.forEach(element => callback(element));
return this;
};
}
And here is the final source-code.
"use strict";
if (!Array.prototype.tap) {
Array.prototype.tap = function(callback) {
this.forEach(element => callback(element));
return this;
};
}
const input = "732829320";
const valid =
Array
.from(input)
.tap(console.log)
.map(character => parseInt(character) || 0)
.map((digit, index) => index % 2 === 0 ? digit : digit * 2)
.map(digit => digit > 9 ? digit - 9 : digit)
.reduce((sum, digit) => sum + digit) % 10 === 0;
console.log(valid);
Now you can easily track down your state and bugs by using this neat little trick!
You could also use a map
to mimic this kind of behavior, without having to write a definition for the tap
method.
const valid =
Array
.from(input)
.map(character => { console.log(character); return character; })
.map(character => parseInt(character) || 0)
.map((digit, index) => index % 2 === 0 ? digit : digit * 2)
.map(digit => digit > 9 ? digit - 9 : digit)
.reduce((sum, digit) => sum + digit) % 10 === 0;
And it would totally work! It has the advantage of not taking the risk of clashing with an hypothetical ECMAScript definition of a tap
method (though we added a guard for such case) and with the drawback of being a little bit of a mouthful.
Should you use it? Some are saying that using prototype-based inheritance in some cases can lead to problematic behavior that can be hard to track in case of bugs. But I think we can agree that well used, these kinds of patterns can be powerful and really enjoyable to use for the Developer Experience. There is an interesting conversation in the comment section that continues on that idea so I suggest you don't stop just here and keep going!
Top comments (6)
Interesting... but you need a warning that this is an anti-pattern:
You should almost always avoid adding to built-in prototypes!
I also may have misunderstood the problem; but why not do everything in a single reduce?
If nothing else you can achieve the equivalent of your
tap
method with map:Hi Ben and thanks for your answer.
Maybe I didn't quite catch all that was in your example but could you elaborate more on how is this an anti-pattern? I think it can greatly benefit to others reading your comment!
From my personal point of view, I don't think it is an anti-pattern as it is a feature of the language that, well used, can be really useful, powerful and cool to use! And by well used, I mean no overriding of existing prototypes, just augmenting the capabilities of the language as I said in this post. I also said that maybe they (the TC39 committee's people) will add a
tap
method but looking at what the proposals are here, I think we are good for a while since it is not even a staged feature (and probably never will, but I can dream, right? haha).That's a really great way of reducing the complexity of the problem. That clearly makes it shorter. But maybe harder to work on.
Unfortunately, that was not the point of this article as this was only a pretext to show how to use the
tap
prototype we built in this post. But I'll keep in mind your idea as it may serves me someday! Thanks for pointing that out. I'll update my post accordingly to explain clearly that this is not a production code.You are correct! That's an another way of doing the tap method as an equivalent. But isn't it a bit of a mouthful though? Adding the
tap
method with theconsole.log
call just between twomap
calls seems to me shorter, easier to work with especially when doing only debugging. If I ever forgot to put the last return at the end, I'll end up with two bugs (the one that I am looking for and this one). Plus it forces you to write two separate instructions as theconsole.log
call does not return the called arguments (unfortunately).I guess what I am trying to say here is that JavaScript is a wonderful piece of programming language and that it supports many programming paradigms. It's up to the reader to choose how he want to use this language. I'm just trying to show other opportunities of doing things but it absolutely does not mean that it's the best way of doing it. Maybe it won't suit you and that's fine to me.
Again, thank you for your comment. I'm taking notes of what you said to improve my article!
The reason for not extending built-in objects is well documented and founded on historical experience. It's a similar reason for avoiding global variables: you risk naming clashes and unexpectedly breaking code elsewhere in your application.
I'm not saying the approach is entirely without merit; but I think it's sensible to include a warning about this up-front so people understand the risks ;)
You do (later) add a guard; but you only talk about 'tap' being added to the language. But the problems come when you're working with multiple libraries; each of which extends the same native object with a same named function; each with a different implementation... Welcome to bug hell :(
Extending native prototypes happened a lot in the early days of JS; and people soon realised it was a really bad idea.
Understood on this being example code; and agreed that the solution does boil down to coding style ;)
The suggestion to use map() with a console log was included to show that there was a simple enough alternative to tap that didn't come with the risk of extending the native prototype.
And my comment wasn't meant as a criticism: it's a good article; and if nothing else highlights JavaScript's often misunderstood use of prototype-based inheritance.
Thanks for clearing that out!
I'm divided between showcasing a feature, but knowing this is a risky one that can be misused or not sharing something that could be useful for others. But in the end, I always end up sharing with the community! That's why I try to add as many guards as possible and experiment a little bit on my own. As shown in this article.
Agreed on saying that using this pattern in a library and sharing that with the OSS community can lead to several and critical bugs, that's why I personally never do that (mostly because I'm only working in a private community and rarely write entire modules for the OSS community, unfortunately!).
But this is a subject I would love to talk about for hours and would greatly deserve an entire post...!
Thanks for the article. I don't fully understand the difference between your
Array#tap
and the built-inArray#forEach
.Hey Dor, thanks for your message!
The simple difference is that the
forEach
method will not return anything exceptundefined
. This is a problem! Since we want to map to a function but still continue the chain calls ofmap
s. So we cannot do something like that in JavaScript.Since it will return
undefined
, you will get an error trying to chain call themap
method on anundefined
value.I'll add that to my post because I think it can benefit to others. Thanks again for your question!