Give and Receive the Right Number of Gifts
Making a List
Santa is already hard at work preparing for this year's Christmas. Little boys and girls across the world are sending him their Christmas lists, which look like this:
Shawn.txt: 3 wooden toys 1 dog clock 1 hobby horse 2 glider planes
Santa naturally uses Perl to parse this list, as he has been these last 25 Christmases. Santa's Perl script produces the work orders that his elves use to make all those toys.
Santa's script simply looks at each line in the child's Christmas list and keeps a tally of how many gifts in total they would like. Here's what Santa sends to his elves on my behalf:
Shawn would like wooden toys (3). Shawn would like dog clock (1). Shawn would like hobby horse (1). Shawn would like glider planes (2). Dearest Elf, please make 7 gifts for Shawn.
All is well and good in Santa's Workshop!
First, he changes the Perl script to emit JSON instead of text:
STDIN, it is concatenated onto the
json variable. Then when
STDIN is closed, the full JSON is consumed and examined to print out the work order.
Santa fires off these two scripts and examines the first work order that comes out.
Shawn would like wooden toys (3). Shawn would like dog clock (1). Shawn would like hobby horse (1). Shawn would like glider planes (2). Dearest Elf, please make 03112 gifts for Shawn.
Do you, beloved reader, see the problem?
This problem is driving Santa batty, so he starts shifting a little bit more blame than is deserved.
"Does node.js not even get addition right?"
"Is this abominable computer playing tricks on me?"
"That Larry fella? Coal!!"
That JSON is wrong! Why is each
quantity a string? That makes no sense. And since the JSON is wrong, there must be a bug in the Perl program.
Each quantity is a string ultimately because Perl is very forgiving. If you write
$str1 + $str2, Perl knows that you meant addition and not concatenation because you chose the
+ operator instead of the
. operator. Concatenation doesn't even cross Perl's mind! So Perl dutifully complies with the
+ operator by numifying
$str2 then adding whatever it pulled out of those two strings.
In Santa's original completely-Perl program, and indeed in most programs, this behavior is very helpful. When Perl deconstructed each line of the Christmas list with a regular expression, it pulled out two substrings: quantity and name. But when Santa summed up the number of gifts, Perl numified each quantity. We didn't need to tell Perl to do that beyond just using addition. This correctly produced the total 7.
However when Santa changed his program to start emitting JSON, there was no more addition to hint to Perl that quantity is actually numeric. Instead, when JSON came to serialize each quantity, it saw that Perl currently thought the value was a string not a number (since it was produced with a regular expression capture group). So JSON produced the strings
+ operator will do depends on the types of its operands.
number + number means addition and
string + string means concatenation. It's not better or worse than how Perl does it; just different. But it means that when the JSON contained strings for quantity, node.js chose concatenation, not addition, during each iteration of
toy_count += item.quantity;. This is how Santa accidentally ordered
"03112" gifts for me (recall that
toy_count was initialized to
0, so that's where that leading
0 came from).
Now we understand the bug. But what is Santa to do about it? It's already December 23rd, he doesn't have much time here!
The fix is to force Perl to treat the quantity value as numeric. You can do this in several different ways: adding zero, multiplying by one, using
int(...), and so on. These operations can only produce numbers, which lets Perl annotate such values as being numeric. That way when JSON comes to serialize quantity, it sees that the value is a number not a string, so it leaves off the quotation marks. Then when node.js parses this JSON, it treats each quantity as a number, not as a string, so
+ means addition not concatenation, so we should end up with the correct sum.
Let's add zero to
$quantity to produce a number.
With this change, the JSON looks like this:
And the toy order looks like this:
Shawn would like wooden toys (3). Shawn would like dog clock (1). Shawn would like hobby horse (1). Shawn would like glider planes (2). In summary, Shawn would like 7 gifts.
Great! Problem solved!
Santa is very happy that orders are now flowing correctly. But he is concerned about that
+ 0. It's not particularly obvious what the
+ 0 is doing, because, practically speaking, when would you ever want to add zero to something? Next year when it comes time to incorporate 2013's cool tech, Santa may have forgotten the reason for the
+ 0 and outright delete it, or simply drop it when he next refactors the code. He could leave a comment:
But he's understandably still concerned about potential maintenance problems because, let's face it,
+ 0 is fundamentally strange.
Luckily for Santa, there is a new module called JSON::Types that makes fixing this and other similar problems a treat. JSON::Types provides a subroutine called
number which encapsulates the messy bit of convincing Perl to produce a number.
The best part is that just by putting
JSON::Types::number into the source code, it's a lot more obvious what is really going on. Even a thousand years from now, Santa will be able to understand what that piece of code means and why it must happen. After all, JSON::Types has easily-findable documentation explaining the problem. It's hard to document
+ 0 in general, and it's hard to search for too. Just seeing the word "JSON" in that line of code might even be enough to jog Santa's memory.
JSON::Types also provides
string for turning numbers into strings. You could of course use
"$number" to force a string into a number, but that has the same kinds of problems as
Finally, JSON::Types also provides a
bool subroutine for producing the constants
false that JSON has. In Perl, we get by just fine with using
undef or the empty string for
1 for true, but in other languages, that simply will not stand.
bool($value) will use the same kind of logic as Perl's
if to decide whether
$value should produce
false. Without JSON::Types, you'd have to do something silly like
$value ? JSON::true : JSON::false.
So, this year, be sure you're using addition, not concatenation, to count how many gifts your loved ones get!