Double the Beyoncé: Navigating Numbers in JavaScript
(funky electronic music) (audience applauding) - Thank you.
Hi, so great to be here today.
I have actually slightly rejigged the title of my talk from When Your Code Does A Number On You to Double the Beyonce, 'cause it's more aligned with my passions.
(audience laughing) And there's gonna be a bit of talk about Beyonce here. Can I get a show of hands, who is a fan of Beyonce? Oh, good, good.
All right, you're gonna enjoy this.
Everyone else, listen to better music. (laughs) (audience laughing) Cool, so I am Meggan.
I am a software engineer at Jaxsta, and I am here today to talk to you all about numbers in JavaScript, because they can behave a little bit unexpectedly at times, as I'm sure many of you have encountered.
So, I hope, you are ready.
There's gonna be a lot of Beyonce gifs, so I'm not sorry. Let's go back to basics, what is a number? Well, a number is an abstract concept which we use to count, measure, label and identify things. We pretty much use them every day.
If you're not, I don't know what you're doing. So, they're pretty ingrained into, like, we understand them. And to help us understand numbers, we can classify them. So, we can group them into different categories. So, we have our natural numbers, which is positive integers, or whole numbers. We have our integers, which is negative and positive whole numbers and include zero. We have rational numbers, which are numbers that can be expressed as fractions.
Irrational numbers are numbers that cannot be expressed as fractions, such as pi or the square root of two.
And we also have real numbers, so real numbers is like a superset of all of the above categories and represents everything that we can see on a number line.
But that's not it.
We can also have complex numbers, transcendental, imaginary, infinity and negative infinity, infinitesimals, surreal, none of which I know anything about, so we're not gonna go into that. (chuckles) That's your homework. (chuckles) And just as we can categorise them in different ways, we can also represent them in different ways. And when I say that, I just mean how we communicate them. So, we can communicate numbers in different ways in human languages.
So, if I asked you all to think of the number eight, a lot of us would think of the Arabic numeral eight. Some of us might think of Ba, which is the Mandarin Chinese character.
Some of us might think Athais, which the Hindi character for the number eight.
So, all of these are the same thing, just slightly different in terms of writing and pronunciation, which I apologise, if I've just butchered.
Feel free to come and correct me later.
And just as we can represent numbers differently in human languages, we can also represent them differently in JavaScript.
So, in JavaScript there are five different ways that we can represent our numbers.
We have base 10, which is pretty standard, if we think of the number 1,234,567, that's it in Base 10, we should all kinda understand that. I hope.
We also have binary, which is base two.
Octal, which is base eight.
Hexadecimal, which is base 16.
You'll definitely be familiar with that, if you're also writing CSS.
And we also have scientific notation, which is what we'll use, if we're representing really large numbers.
So, all of these are safe types that we can use in JavaScript.
What is a number in JavaScript? Well, just as a girl has got to have her standards, so too does JavaScript.
And JavaScript follows the IEEE-754 standard, which is the Institute of Electrical and Electronic Engineers.
I can never remember that, I have to always read it off my notes. (chuckles) And it's the standard for Floating Point Arithmetic. It's like a really long spec, really dry document. If you're struggling to sleep, I can recommend it. (audience chuckles) Like a light bulb, just out. (chuckles) And it outlines how we implement numbers in JavaScript, or as I like to say, it tells us why 0.1 plus 0.2 does not equal 0.3, which is like everyone's favourite. JavaScript sucks, ugh! (audience laughing) Doesn't even do math properly.
Well, I'm about to tell you why that happens. So, the IEEE-754 specifies the implementation of floating-point arithmetic in JavaScript, which is just how we represent decimal points in binary. And it allows us to represent real numbers as an approximation, to support a trade-off between range and precision.
So, when I talk about range, that's just like the size of numeric data that we can safely represent. And in JavaScript that range is from about negative nine quadrillion all the way up to positive nine quadrillion. So that's a pretty big range, pretty good.
And when I talk about precision, I mean how many decimal places we can represent numbers to. So, in JavaScript that is to 17 decimal places. That number up there is what 0.1 and 0.2 equals, apparently. So, computers are not really that amazing.
They actually have limited space.
They're pretty basic.
There's a pun there.
(audience laughing) Thank you. (laughs) And everything in a computer is just binary. It's all just ones and zeros.
That says hello, Melbourne.
Hi. (chuckles) Yeah, so regardless of how you write your number, whether it's in base 10, base two, base eight, base 16, it's always gonna be stored in binary, and it's always gonna be stored in its binary floating-point representation. We can't have decimal points in binary.
Decimal, coming from Deka, meaning 10, for base 10. Binary is base two.
So, floating-point arithmetic tries to account for that. Let's think of that number 1,234,567 again. If I were to represent that in its floating-point form, that would be what it looks like.
A bit of a mouthful, I'm not gonna say it out loud. But let's break that down.
So, in that number, it's 64 digits, so 64 bits. And it helps, if you are familiar with scientific notation, it's gonna kinda help me explain what each of those bits is for.
So, our first bit represents the sign.
So if it's a zero, it's a positive number.
If it's a one, it's a negative number.
The next 11 bits represent the exponent, which means how far along we're gonna move that decimal point.
And the next 52 bits represent the significand or mantissa. And that's the actual, like, digits around the number. That's that representation.
So, let's have a look at our favourite bug in JavaScript. 0.1 plus 0.2 gives us this number.
0.3 and then a bunch of zeros and a four, which we know is incorrect.
When I was in school, one of my teachers always used to tell me that calculators are never wrong, and humans are wrong.
Yeah, all right.
That's all I have to say about that.
(audience laughing) (Meggan chuckles) So, when we're representing numbers in binary, integers are totally fine.
They're super easy to convert, that's no trouble. It's when we start representing fractions or more complicated decimals, that we have trouble making that conversion. So, let's think about 1/3.
If we convert that into decimal, we can't convert it perfectly.
It's just a recurring decimal.
So, we say 0.333, and then maybe that's it. We don't say 0.33333333333333333333 for the rest of our lives, because that's silly. We round it.
But to anyone who's ever tried to create maybe a three-column layout, pre-grid, you'll know that when you multiply 0.33 times three, you're not gonna get one.
There's always gonna be that pesky little 1%. Aaargh, every time.
And so, when we're dealing with numbers in JavaScript in binary, it's a similar problem. If we think about 0.1 in decimal.
Super easy to represent.
If I represent that in binary, it's this much longer number. So, we get 0.000110011001100, and it's a repeating, it's a recurring one, one, zero, zero. It happens forever.
But because we only have 64 bits to represent this number, we just have to round it.
So when we convert it back, we get this.
So, that's where this problem comes from.
Let's think about how big a problem is this really. Let's say, I was measuring one centimetre, and I had a margin of error of 0.00000000000000004 centimetres.
That would be about 0.0000000003 times as long as a glucose molecule.
Okay.
What about a kilometre? If I were trying to measure one kilometre, and I had a margin of error of 0.00000000000000004 kilometres, that'd be 0.0000000000000000000001 times as long as the distance from Earth to the Moon.
Pretty tiny, but still hard to comprehend.
Let's go big.
If I were trying to measure a light-year, that's the distance that light travels in a year, it's a long way.
And if I had one light year, and I had a margin of error of 0.00000000000000004 light-years, that'd be about 38.38 centimetres, which is like this, all about the height of a thin bowling pin. That's a fun fact. (chuckles) So, it feels like it's pretty negligible.
And in most cases that degree of accuracy is probably not gonna be that important.
If you're working in, say, like banking or finance or gaming, where precision really matters, it's probably a different story, and maybe you don't wanna be using JavaScript. (chuckles) I'm told the WebAssembly is great technology to resolve some of those errors, so maybe in the next talk about WebAssembly, you can be sold on that.
So, moving on.
We've talked a bit about precision.
Let's talk more about that range.
'Cause size does matter. (chuckles) So, I've talked about a trade-off between range and precision, because we can't have both. We can't have a huge range and hugely precise numbers. We can make sacrifices in each of those.
So, we've talked about precision, let's talk about range. And earlier I mentioned that JavaScript has a limit of about nine quadrillion, which isn't true. I lied, I'm sorry.
Building trust.
If I go into my JavaScript console, and I type in 1.7 times 10 to the power of 308, that's a 17 followed by 308 zeros, it's a pretty big number. I get that number back.
So this is scientific notation that we were talking about earlier.
So, that's a lot bigger than nine quadrillion, nine quadrillion is only like a 16-digit number. This a 310-digit number.
But if I type in 1.8 times 10 to the power of 308, I get infinity? Which isn't really how I thought maths worked, but I'm not an expert.
So, in JavaScript, we actually have a really large maximum value.
We have this number 1.79, all of the other numbers, times 10 to the power of 308, which is a huge number. And we also have a minimum value, which is 0.5, sorry, five times 10 to the power of negative 325, so that's 0. 324 zeros, five.
That's like the minimum possible value that you can represent in JavaScript.
So, we do have really big numbers in JavaScript. And for really big numbers we have naming conventions. A million, billion, quadrillion, and that goes all the way up to centillion, which is a number followed by 303 zeros.
So, just by that you can tell JavaScript goes beyond the naming convention. So, that's a pretty good set of numbers.
If you're still not convinced that it's a good range, I want you to think about this number.
3.28 times 10 to the power of 80.
It's the number of particles in the Universe. So, I don't know what you would be doing with numbers greater than that.
(audience laughing) Like causing a black hole or something. (chuckles) If you are using numbers bigger than that, like, come and tell me, I wanna know, I wanna hear about it. Yeah, so, in JavaScript we have this massive range, but we can't represent all of these numbers safely. So, we have a maximum and minimum safe integer, and that's that nine quadrillion value that I keep banging on about.
If you go beyond either of these numbers, things start to get a little bit weird.
Let's have a look at that maximum safe integer. It ends in 991, just want you to focus on the last three digits, 'cause otherwise it's gonna get, it's gonna be hard to read. (chuckles) Let's add one to that.
Okay, all right, well, I don't know why that top one is the safe integer, I can see that if I add one, it's fine. Keep going, oh.
That's not what I would expect.
It's not, it's not really how it works, but the arithmetic just doesn't add up. (chuckles) It's just, yeah, it just doesn't make any sense. And so, this particular issue is what actually inspired this talk.
So, I was at work one day, and for a bit of context, at Jaxsta we're building a massive database of music credits, so you can go on and look up your favourite song and see who produced it, mixed it, whatever. And I was sitting at work one day, and I was rendering data on a page, which is like 50% of my job, so I'd like to think I'm okay at it.
And I noticed this strange bug.
Let's just pretend that I was looking at Beyonce's "Drunk in Love" page.
Happens to be one of my favourite karaoke songs, if anyone's interested later.
Also, in case you weren't aware and up to date with Beyonce's discography, this album cover is where my T-shirt comes from, BeyonCSS, Beyonce, ah. (Meggan laughs) And it also happens to have actually been designed by Henri, who was just on before me, so amazing coincidence. (chuckles) Yeah.
Right, so I was at work, and I was looking at this page, and I noticed an issue.
I had Beyonce appearing twice on my page, where she shouldn't have.
So, she was coming up as the main artist on "Drunk in Love", but she was also coming up as the featured artist, which is a bit weird to be both the main and the featured artist, especially because she is not the featured artist on this song, Jay-Z is the featured artist on this song.
And so, Beyonce is not happy about this.
She would like Jay to be represented too.
And so, I had a look inside of my developer tools, and I noticed something funny.
So, I had two resource subjects coming through, One for Beyonce and one for Jay-Z, but their IDs were the same, which is weird, because IDs are supposed to be unique.
And I had a look at that number, and I was scratching my head for about three hours. Just like, what? Looking in the database, seeing this, they're not even the same IDs.
And I discovered that when your numbers are too big, JavaScript's gonna do really weird things with them. So, their actual IDs, which ended in 167 and 168, were just becoming the same number, hence this problem.
Yeah, hence this problem, where I had no Jay-Z and just Beyonce on the page. The front-end framework that I'm using is Ember, and the data layer on that said, "Hey, I've already got "a resource object with that ID, "so I'm not gonna fetch it again.
"So just have double Beyonce." Which to me is the feature more than a bug. (audience laughing) My boss didn't agree, so I had to fix it. (chuckles) And now, solution here was to move out IDs to UIDs. So, UIDs are 128-bit number, but because they contain non-numeric characters, they have to come through as strings.
Yeah, so, we moved to UIDs, and now Beyonce and Jay-Z are both coming through with different IDs, which is correct.
And now they're both on the page, which is great. (chuckles) And the king and queen are happy. (chuckles) Yeah, so, big numbers are kinda weird, but this just in, is a new numeric primitive called BigInt. Has anyone heard of BigInt? Okay, a few people, cool.
So this is really exciting, or it was really exciting to me when I was putting together this talk, yeah. So, BigInt is a new numeric primitive, which was just introduced last year.
It's still really new.
And it's only available in Chrome, Firefox 68 beta and Edge.
But it basically means that we can represent numbers beyond that maximum safe integer.
So, let's have a look at how it's implemented. If we wanna create a BigInt, we can just append an N to a number, and we'll get that green type, is now BigInt. Or we can also use the BigInt like method in passing a number.
We cannot do this with any decimals or floats, because it's a BigInt, integer, whole number. So, we're not gaining any precision here, unfortunately, but this could kind of set the path for maybe a BigDecimal type in the future.
So, we can do arithmetic with other big integers, so we can add 100n to 100n and get 200n.
Or we can subtract.
The behaviour is expected.
We can multiply.
We can divide, but because we can only return whole integers, it will just always round to zero, so 10 divided by three is 3.33333, but we just get three. We can do the to the power of operator.
And we can also use modular.
We can't do any mix type operations, so we can't mix big integers and regular integers. But we can compare.
Big integers have loose equality to numeric types, and we can also compare them like normal.
So, 1n is not bigger than two.
And because we can compare them as normal, we can also sort them, if there's like a mixed array for some reason.
So, let's have a look at the maths of this properly. We had this problem earlier, where, when we were adding beyond that maximum safe integer, things got a little bit weird.
Let's do this with our new BigInt.
So, I'll set a variable of bigNum, and I'll add one. Okay, it's working.
I add two, it's working.
Oh my god, it just keeps working.
(audience laughing) It's so beautiful. (chuckles) So, it works, which is really amazing.
I was excited about this.
You don't have to be, but that's all right. (chuckles) So, some caveats, of course.
It's not ready for production apps yet, because it's only available in a few different browsers. And there's not a lot of documentation available, so it is a fairly new feature, less than a year old. It was only shipped in Firefox like two weeks ago too, so that's, yeah.
I still think it's like this.
This is how I feel about BigInt.
I'm just like yes, so cool! (chuckles) So, what I think is really exciting about BigInt is that it's reflective of the JavaScript ecosystem as a whole.
I think it's very easy to get caught up and feel like, oh, JavaScript is so fatiguing, like this just needs stuff all the time.
But I think it's really cool to leave it to be working in an ecosystem that is kinda growing and learning from its mistakes, and adding in features. And even though JavaScript is still kind of a baby, like it's only 23, 24 years old.
Some babies grow up to be Beyonce.
So, you know, it's pretty cool.
So, to summarise, I think that numbers in JavaScript can be a bit confusing, but when you understand why they have been implemented that way, it can help you navigate some of the bugs that may surface.
And Dan Abromov kind of summed this up.
I saw this on Twitter the other week, and he said, "It's fun to like poke at JavaScript "even when JavaScript behaviour is totally reasonable." And so, we can joke about it and say, "Ha, 0.1 plus 0.2 doesn't equal 0.3", but when you really look into it, it's totally reasonable behaviour, and we can understand why that happens.
So, things to look out for when you're handling numbers in JavaScript, is just to remember that when you're thinking about precision, we only have 64 bits to represent those numbers.
And so, there's gonna be precision errors.
So, if you are dealing with precise numbers, round them. You don't need to know your shopping cart to 17 decimal places, you only really need to know at two. And when you're dealing with larger numbers, just remember that there is a maximum safe integer, and that BigInt is coming, which is cool.
So, it's my hope that after this talk you'll be a little bit less like this, when you come across numbers in JavaScript, and a little bit more this.
(Meggan chuckles) (audience laughing) Or a little less just like confused Beyonce. (audience laughing) And mostly Beyonce. (chuckles) Thank you so much. (chuckles) (audience applauding) (funky electronic music)