 Pacific Northwest Scala Talk. I'm pleased to be here again. It's a really fun conference. My name is Eric Osseim, and my talk is called Unruly Creatures, Strategies for Dealing with Real Numbers. It's a little weird, but I'll just say a bit about myself. I'm a proud type level member. If you're into Scala's Ed, Shapeless, what have you, I'm on your team. I'm one of the maintainers of Spire and a few other Scala libraries as well. I have sort of a, I guess I describe it as like a dual interest, where I'm interested in expressiveness and performance. I feel like it's easy to be into one of those things, and it's fun, but not always easy to be into both of them because they're often in tension. It's not, I guess it's not as relevant to this talk, but it's a little bit relevant. I work on like machine learning stuff at Meetup, primarily in Scala. You've got a nice little inverted Meetup logo here that looks great. And you can check out some code I've written at my GitHub page here. So just really quick preamble. I tried to make as many of the slides as possible, like runnable, but they all assume this series of imports that just give us a whole bunch of types to work with and some nice implicit for adding methods and things like that. So just when you read the, if you read stuff and it doesn't look like it's legit, it probably is legit. It's just doing something interesting that you may not be used to seeing. So there's, I guess there's like four phases, but the first three phases are gonna be kind of fast and the fourth one's gonna be a little bit longer. There's gonna be kind of a primitive math blooper reel. A lot of you are probably gonna be already familiar with this stuff, but I just wanna create like a common language for sort of understanding some of the issues and talking about them. And then after the blooper reel, I'm gonna try and explain maybe why some of the bloopers occur or like what the difficulties are. Sort of breeze through a bunch of like what I consider to be kind of straightforward and interesting number types that Spire supports. And then we're gonna hit real numbers which are hard and we're gonna spend some time on them. So it's gonna be fun. And there's sort of like a math warning here. Like I'm not assuming a huge math background. In fact, I'm trying to make this very accessible. Like I know a lot of people have like math trauma or post-traumatic math disorder. So, you know, like that's totally legit, but I may still go too fast or I may not explain something clearly. So feel free to like stop me to ask questions or even if you're not confused, feel free to ask a question on behalf of maybe like an imagined person who might benefit. That's totally fine. And also if you are a math person, feel free to correct me if I get it wrong. Cause, you know, I'm not trying to say that I, I'm a hundred percent, you know, up on all this stuff. So to start with fix with integers, right? In and long. We've got great syntax to support them. We use them every day. They're awesome. But the big problem that I would say they have is that they mostly work, but when they fail, they just fail in a completely catastrophic manner. And again, I feel like people are familiar with this, but let's just kind of do this little pop quiz. So how many int values will this expression return true for? How many can you think of? Okay, two. Someone's already got it. Great, yeah, that's true. There are two int values. Zero is the obvious one and int min value is the non-obvious one, where it's so low that there's no positive number that it can represent. And it's related to this next puzzler, which is that there are more negative numbers than positive numbers. That's related that, I guess, again, I think most people, if they think about it, realize that it's true, but it's easy to forget this and then you write code that uses negation and you forget that you might get messed up in a few cases. So similarly, relatedly, obviously, if you've got two positive numbers and you add them, is the result positive? Well, no, just like we talked about, you have overflow issues. And those are particularly bad in loops and stuff. Scala's range for a long time, I think I had pretty serious bugs here where if you gave it a big increment over ints or something, it could actually go off the top, go negative, and then just keep going and you'd actually have an infinite loop in your code because, I mean, and I'm not trying to say that that was just bad software, it's just hard to get that stuff right. And so more than that, these aren't great because often you might have a quantity you need to express and you just can't do it. So int is pretty bad. I mean, if you need to measure US spending in dollars or the number of humans on earth, you're already boned with int. Long's a little bit better. People use long for a lot of these new databases for IDs and stuff. And long won't fail for that kind of stuff usually, in my experience, but it will fail to measure US energy consumption in joules. It'll fail to measure the weight of the earth in pounds. I mean, these are things you might be interested in, like if you're a physicist or something. So integer types, I would say they're easy to understand, but when they fail, they fail really badly. So how about floating point? It's a big improvement. We can represent fractional values, which is great. So integers just can't do that. And there's a much wider range of values. I mean, the largest double you can do is, I think, 10 to the 308th power. That's pretty big. I don't really know of a number bigger than that that I would need to use in relation to the earth or something like that. Also, the cool thing about these is that they do overflow to a positive or negative infinity, which is nice because you wouldn't have that infinite loop problem. If your quantity overflowed, the sign tests or the comparisons would still work. Infinity's bigger than everything, your loop would exit. So great. So that stuff all works. And it's roses. Well, not exactly. So again, I think this isn't news to people, but some of you may not have seen that results we're used to in math class just don't necessarily work with doubles or floats. So here's an example where we have two different sums that should be the same value, and they aren't. And it might be surprising, but basically the answer that's going on is that the underlying floating point value can't exactly represent some quantity. So it's using an approximation, and then that's creating error that then gets passed through your addition. So here's four increasingly complicated attempts to try and do this kind of fuzzy equality correctly. How many people have implemented fuzzy equality for floating point values or something like that? OK, so a bunch of you, right? So great. Everyone's on the same page. This is hard to get right. I think everyone can agree that none of these attempts actually work. These are all things that you might care to do, and they're not really right. So I mean, I'm not even saying this one is right, but I got this off the internet. I sort of believe, or mostly got it off the internet, I sort of believe this one might possibly be doing something correct. I guess I'm just saying that it's hard to actually get this stuff right. There's a lot of cases you have to handle. It's really not easy to work with. We work with the floating point values a lot, but I think few of us, certainly not me, have internalized this method of working with it. And here's another illustration of the kind of error you can have, where we've got 1,000 values that are all like a 100th small value. And if you add them together, you probably should get 10, right? I mean, in math class, if you add them together, you get 10. We get something very close to 10. I mean, that's basically fine. We all know it's 10. If you see that in your code, if you see it in logs, you know what happened, right? Your floating point value isn't quite 10, but it's basically 10. And so that's great. So then here's a little modification on this, where I wrote this nice method that should return 10 no matter what double you pass it, right? Because we put big on one side of a list, we sum the list, and we subtract big off the other, right? Everything's going to be fine. And we pass it 0, and sure enough, we get the same value we got previously, a bunch of 9s with an 8, 3, 1 on the end. Great. So yeah, so it's not the same, but it's pretty close. And we're back below 10, OK. So you're like, well, but this is, I mean, still close. And then all of a sudden, you're not getting 10 anymore, and then it's not great. And then you're getting 15, and you're like, what is going on? How did that even, I don't know. I mean, I'm not going to explain why that's 15. It's sort of funny why, but anyway, point is these things are minor until they're not. And it's not always obvious when it's going to be a problem. So there's just some more details of, and this isn't really floating points fault, this slide. This is almost more Java and floating points fault, where floating point defines infinity and not a number. And so divide by 0, you don't actually always get the same thing. You get different values if you have a non-zero and zero numerator. And there's this weird issue here. How many people understand what's going on here? Have you seen this? Have you been bitten by this? I feel like it bites people sometimes. The issue is that floating point spec requires not a number not equal itself, because it's like an error condition. It's like two exceptions wouldn't equal each other. They're exceptions. We don't really want them to be equal to things. But Java requires that everything be equal to something, like the sort of comparator interface. And so what's happening here is that Java's comparator interface decided that not a number had to equal something, had to return something in relation to itself, so it should return to 0, even though when we compare it directly, it's false. You get something kind of similar with positive and negative zeros, which, again, I don't want to get into why you have positive and negative zeros, but floating point has them. They're important in some cases, but then they can really trip you up because they're the same in one case, but then they're not the same in the other, because it's just a thing. And so is floating point done? Well, I would say no. It's actually really smart. So it's definitely not dumb. I would just say that they had a very, very hard problem, and they did a great job. But we don't really have the hard problem that they had. So their hard problem is kind of like, you have a fixed number of bits, and you need to represent these gigantic quantities. And you can't use more bits. It's not on the table. And so they did something really smart. Most of us actually could use more bits if we wanted. 32 bits isn't some cutoff for us. And just, again, to reiterate, compare double and long. It's the same number of bits. And long doesn't do any rounding. Long fails catastrophically. Double doesn't feel catastrophically. Long can't do fractional values. And so if you think about it, given that it doesn't have more bits to work with than long, how can it just be better in every way? Well, it's not better in every way. It kind of moves the problem around a little bit. It's better in the obvious ways, and then it has these weird approximation problems that we ran into. It's a bit like Apollo 811, right? I mean, I think they used 2K of RAM computer to get to the moon. Like most of us in this room probably couldn't do that. I mean, at least not with our current training, we couldn't. I mean, we probably could learn. But point is, we shouldn't build web apps like we're going to the moon with 2K of RAM, right? We all have much more than 2K of RAM. We don't need to do the kind of tricks that they did in like 1967 to pack memory and be really, really efficient in that way. At least not most of the time. Maybe sometimes we do. Often we probably don't. So it was intended to be useful for scientists and analysts who are going to do things like compute error bounds and check for floating point traps and errors and who understand the structure of how it works and what the limitations are. And that's not really true. I mean, this is like a back of the envelope estimate, but I tried to calculate how many computer programmers there are in the world and how many analysts or quantum numerical analysts. And I feel like this is maybe a conservative estimate, actually. So 99.9% of people using floating point weren't really its intended audience, I don't think. So anyway, like I just that we're sort of violating our mathematical intuitions. Like we all learn math, we know how it works, and then it doesn't work that way. And then there's these other problems, like in terms of using a fixed number of bits and then these rounding or precision errors. And then these legitimately hard things like roots and transcendental functions that they're just hard, gonna be hard to do, really. So let's kind of, I'm gonna quickly tour the different number types and how they relate to sort of mathematical types you might be interested in. So first we got the integers. We can fix the integer overflow issue. It is very easy to fix. We just use more space, right? Instead of wrapping around, we just grow the size of our number. It's not really hard. So Spire has a really nice type for doing this called safe long. You can use big int too if you want, it's just slower. And basically what it does is it uses a long internally until it can't. And if it was gonna overflow instead of overflowing, it basically starts using a big int at that point. So it's a nice way to, you get a little bit slower than long performance, like a boxed long kind of, until it gets big and then it turns into a big int transparently for you. So it behaves like a mathematician would probably expect. And it does support all those sort of integer operations you were gonna expect. And it's pretty fast, pretty good. The big things it lacks obviously are stuff that integers lack anyway. So if you were gonna use int or long, I really encourage you to use safe long in a lot of cases. I mean if you're writing an array of a million things, maybe you wouldn't do it, but if you're writing an API and you need one thing, why can it hurt? But yeah, it does lack some of these other things, which brings us to rational numbers, which give us that. So when I say rational numbers, I mean fractions, like you learned in school. So like a third, 99 out of 100, and so on. Here's kind of the more formal definition of it. It's like a set of equivalence classes, but really the point is just that it's fractions that we're used to working with. And when we work with them, we make a couple simplifying assumptions to avoid problems. So whatever fraction you try and build, it's gonna always make sure that zeros are presented the same way, and it's gonna simplify the fraction to make it be more efficient to work with, and always use a positive denominator so the sign will be in the numerator. And this is just to avoid the problem of like two fractions that are actually the same, but look different, not being different. It's like an implementation detail. Maybe I shouldn't have even put it in there. But rational numbers never need to round or approximate. It's great. Whatever they are supposed to be, they're exactly that. They don't have this problem doubles due. So here's like a little example where I sum some numbers and I get these exact results. And there's sort of an interesting thing here, which is that you notice that all these numbers in the vector I've got, they're getting increasingly close to one, right? They start out at half, two thirds, three quarters. They're never gonna reach one the way I wrote it. So you'd expect that whatever my range is, I'm gonna get a sum that's like less than that, like a little bit less than that, kind of decreasing over time. But if you look at the rationals we get, the denominator and the numerator get bigger and bigger and bigger, and the answer is because we're not losing any precision, we just need more and more space to represent this sum. And that's really the only drawback with rational numbers is that you have this issue where in order to have that exact issue, you can't throw away information, even if it's mostly not necessary. If you're very, very close to one, but not one, rational tracks that difference. And so we can approximate a really big number, it can just get as big as we want. So, but when we turn into an integer, you can see that it's really only 90, it's like a little bit above 95. So, mathematicians probably wouldn't be surprised by this behavior, and we probably wouldn't be crucially. I shouldn't have just said mathematicians, like I don't think any of us are. The surprises come where with the fact that there's this weird problem with addition where you have to find common basis, and so we've got a couple of these small, relatively small rationals that look really simple, and when you add them together, you get this sort of much more complicated rational. And you can imagine with those numbers I showed you how if you're doing like a map-produced job, if you turned all your numbers into rationals, you know, it's possible that could cause you a problem if you've got like a million of them, and they all have different denominators. They all have the same denominators, probably actually fine, they have different denominators. It's gonna have to find a common denominator between all million of your numbers, and that potentially is just gonna take a long time. And this isn't the case for multiplication and division. With those, there's no need to find a common denominator. So anyway, if you're gonna work with a rational type, which again, I think it's like really nice when you wanna be precise about this stuff and just not worry about those floating point issues, it supports pretty much all the arithmetic you would wanna do, you know, true division. You've also got quotient, which is like kind of floor division, remainders, exponentiation for any integer, like inverses, that kind of stuff. But it still lacks square roots and log in the exponential function and things like that. And it doesn't have, and it lacks the guarantee that working with small numbers is fast. As we saw, you could have a number that's actually very close to one, but it's gigantic, because it's like, you know, a million nines over one and a million zeros or something like that, which is gonna be very close to one. And so it's a small number in some sense, but it has a lot of complexity built in. And it doesn't guarantee that adding these numbers is fast either. So this is sort of the meat of the talk and hoping to kind of get through it. And then we can maybe do some live coding demos and ask questions, but again, please stop me if I'm going too fast. I'm just trying to make sure we're all on the same page. So this is the one that's a little bit hard to talk about. The set is described as uncountably infinite. How many of you all have like heard of like countably infinite and uncountably infinite? Like is that a thing people? So maybe like a third of people. There's different kinds of infinities people talk about. The basic idea there is that something's countably infinite if you can count it. If you can put it into a one-to-one relationship with like the counting numbers, like one, two, three, four, five. Most infinite sets we've talked about so far or all of them so far have been countable. This new one isn't, which is, and it's very hard to think about for that reason. The real numbers contain the rational numbers, but they also contain these irrational numbers like roots and like trigonometry, outputs of trigonometry functions like pi, other transcendental numbers. And the weird thing is that almost all the numbers in R are transcendental. Almost all the real numbers are transcendental. So the numbers we're used to working with, that's like a tiny, tiny, like almost like completely ignorable fraction. Like if you were gonna throw a dart you would never hit a rational number. You'd always hit it, pretty much always hit a transcendental number. Even though we really work with them. There's sort of three mathematical ways to think about these. It's not really important. I just wanna try and, the reason I'm even saying this is I just wanna point out that representing real numbers correctly is hard because it's a complex idea. I mean, I don't, it's an idea that I think is tricky to get right. So one way you can think about real numbers are a sequence of approximations that are getting closer to the actual value you care about. So if you really wanna have pi, you might have like, you know, three and then 3.1, 3.14, 3.141 and so on, you'd keep having like an extra digit or something like that. And it'd be getting closer and closer. And it'd be an infinite series of these things that are getting closer and closer to the actual value you care about. That's like, like, Cauchy sequences. That's like one characterization. There's another one involving these things called data-kind cuts where you create these partitions of the rationales which are also an infinite set. That's the thing that makes this hard to think about. It's like hard to imagine how you're gonna do this correctly. And that turns out to give you an idea of the irrational numbers and transcendental numbers. And there's other stuff. Disclaimer, I was not actually great at real analysis in college. A lot of you, some of you were probably much better than I was. So I'm just trying to like give a very quick like overview of how you might think about this stuff. What does it all mean? Many values in R are difficult or impossible to express. And so we may need to use approximations which we saw earlier are kind of dangerous. And there's gonna be error bounds involved probably. There's also a weird thing which is real numbers might not be real. They might not exist. How many people here think real numbers do exist? Raise your hand if you think real numbers exist. Like one person. How many people think real numbers don't exist? Are pretty sure that they don't exist? Like two people. Okay, how many people are undecided on the issue? Okay, like most, and then some people don't wanna vote. That's fine. But right, it's sort of interesting. There's kind of this debate in the mathematical community about whether real numbers actually exist or not. So there's like a finiteist view that says that, you know, I'm sure some people have heard this quote, God made the integers, all the else is the work of man. This Leopold Kroniker doesn't really think real numbers exist. He thinks Cantor's like a devil, like a bad guy. And then, you know, Hilbert on the other side says that Cantor's brought them into paradise with these kinds of, you know, uncountable infinite sets. So even mathematicians don't really know, always know what to make of this stuff. I mean, people mostly agree that they, most mathematicians probably hold the classical view, but, you know, there's definitely two sides to it. And the problem that we're in is even if we are like classical mathematicians, computers sort of make us finiteists and constructivists because we have to be able to actually build these values to work with them. Like proving that the number you need exists isn't a useful computer program for most of us. Like a mathematician would just be happy with that. They'd be done. They'd go write a paper. But we have to actually know what the value is, which means that we have to have a constructive approach. So in practice, what this talks about is cheating. We're not actually gonna build you the real numbers. We're gonna build you a different set, which is the computable real numbers. And they are countable, unlike the actual real numbers. So almost all the numbers in R are uncomputable. However, it's very hard for us to get our hands on these uncomputable numbers because we can't compute them, right? So they exist, but we can't really use them. So it's kind of fine to leave them aside. If you're interested in that topic, there's actually kind of a funny link here where Mark J. Dominus gives an example of an uncomputable number that you can talk about. But I had to Google for that. And there's not a lot of them. It's hard to find them. So what's a computable number? Well, a computable number is basically, you can think of it like a function where whatever precision approximation we want, we have a function that, given that approximation, gives us the best possible, sorry, given the precision requirement, the result is the best approximation we could want. No matter how precise, we can always do it. And that number, we can be said to compute that number. And if you think back to the definitions they gave, you can sort of see how it's maybe related to the definition of a real number. So here's like a really quick example in Scala of how you might build it. So we've got this CR type, which is like a computer real number. And it takes this function from int, which I'm gonna call like the number of bits that we want our representation to have to an integer. And this integer is gonna be like the numerator of a fraction. So we're producing rational numbers, we're producing fractions that can be as close to the real number as we want for any precision. And I feel like it's pretty straightforward. If someone had this function to hand us, it's not very hard to take the function they gave us and build the actual rational number, right? We just construct the numerator. We construct a denominator, which is just two to the power of the number of bits we want. And that gives us our rational number, which is our approximation. So obviously the hard part there is to produce that function, which we will talk about in a minute. So this is implemented in Spire as a type called real, not CR, I just used that for the demo. And there's a few complications that we do to try and make this a little bit faster and accuracy, there's some memoization that goes on. But the ideas are all the same. So if you go read the code, it's not gonna look like CR exactly, but the important part is completely the same. I just wanted to simplify it for the talk. And the original design, I shouldn't claim credit. Like the original design is based on the ERA library by David Lester, which is I think in Haskell now is Serial. There are some improvements Spire makes to it. It's not just a complete copy, but it was definitely inspired by it. It's a great, good paper. It's an interesting implementation. So I just wanna credit that. So how does this all work? Like I sort of showed you how it works, but how would you use it to do stuff? Well, let's talk about how you need to do it correctly. Let's look at how you can do it wrong. So imagine that we're approximating these two values. We've got 1.14 and 2.84. Those are the actual values. So all we have on paper is 1.1 and 2.8. Those are approximations. If we wanna add those, the result we get from adding the approximations is 3.9, but actually the real value we wanted is 3.98. And if you look, 4.0 would have actually been the better approximation. Does everyone kind of get this step? The idea is we have some actual number that's maybe really complicated, a lot of decimal digits. We have an approximation in hand, but when we add those approximations, we lose precision. We no longer have a correct approximation anymore. Those, these two approximations are correct. They're the closest one you could get. But then this approximation is not a good, not as good. We've lost precision. That's sort of clear to people? Yeah? All right. So, you know, this is really the idea behind all of numerical analysis is analyzing this problem of like, you need more precision on your inputs to be able to produce the, the desired precision on your outputs. That just tends to be the case. And how much precision you need, that's kind of the interesting question. Like it differs depending on what you're doing. And there's a lot of interesting work there that we're not really going to go over. But here's, here is how we would do addition for our, the type that we just talked about, the CR type, simplified computable reel. So, got like a little helper function here that just does some rounding for us. And then all we really do is we build this little inline anonymous function where we're given two computable reel numbers. And then we say, you know, for, for ever bits of precision we want, let's calculate those two, the two inputs to our, to our addition with an extra two bits of precision. We're going to do it, calculate them at higher precision than we actually need. And then we can add those two together and then round. So the idea being that, that we, we've got extra precision in hand so that when we need to add them and we know we're going to lose some precision, we still have enough. Do people kind of see how this works? Yeah, maybe. Great. So that's, this is really it. This is all we're doing is we're, when we build things like addition or multiplication, we just have to figure out how much precision we need for the component parts and then we can build our result. And, and it's all completely lazy, right? We don't, we haven't done any work until someone calls, tries to evaluate the computable reel. You know, adding the things is literally just, you know, building up like a case class or something. And then some of these are actually super easy, like negation. Negation's great. You don't actually need more precision. Just whatever you got out of the, you know, your, your, your input, you just flip the result, right? You negate it. It's, it's super easy. And min and max are kind of the same way where, you know, if you have two inputs and you want to know which one is the smaller, for whatever precision you need, calculate them then and then take the smaller one and return that. You know, pretty easy. You don't even lose precision there. So that's kind of great. It's kind of cool how simple those ones are. And then it gets a little more complicated, right? So here's multiplication. This is still not too bad, but you know, someone who's not like me had to do the work of actually doing the numerical analysis to know how much precision we needed to calculate. Like, this is sort of determining based on how big the arguments are, how much extra precision you need. Sx is like the extra precision for x, or size of x really, with some extra precision. And then same for y. We calculate those with the extra precision and then we multiply them, but we create like a big denominator. That's sort of the strategy that's going on. A little, the details are more complicated but the idea is still very simple. You know, find how much precision we need from x and y, calculate them and then build our approximation. And do rounding as necessary to ensure that we get the best approximation for the number of bits we have. So here we go. So how many of you took calculus at some point? You didn't have to enjoy it, just took, okay, so like a lot of people, great. Do you all remember Taylor series, anyone? Yeah, Taylor series, great. So that's all we're doing here. So if we need to calculate like the exponential function, right, we can look on Wikipedia or our textbook that just have a great memory and think back and remember the Taylor series expansion for that. So here it is, I've just written it. In case people haven't seen it before, exclamation point is the factorial function. That just means one times two times three and so on all the way up to n, whatever the argument is. So this will give us, you can sort of start to see how this might give us like a good approximation for the exponential. And here's some nice Scala code using streams which are great to sort of show you, they're kind of slow but they're great to kind of show you how it works. So the thing I've done here is I've written this terms function method that calculates a single term of the Taylor series which is x to the n divided by n factorial. And then I've called terms to get the next one after that. So this is like an infinite stream, infinite stream of terms of the Taylor series. And then I've put it all in this vowel called series which starts with a one just because that's the definition. And so we can get the first 10 terms of the Taylor series just by calling series.take and it shows you them down there. Which is kind of neat, right? It's like when I was in math class I feel like I didn't, if I had been able to play with this stuff in math class I think I would have probably done a better job because I would have been able to see what was going on a little bit more clearly. Anyway, as we look at this you can notice that the numbers are getting smaller, right? They started, you know, at two and they're getting smaller and smaller and smaller and so they're approaching zero, right? They've kind of agreed that's what it looks like they're doing. And so what's gonna happen there is it's an infinite sum but the further you out you get the terms are less and less important. You would only need them if you need a lot of precision. If you don't really need much precision you probably would never bother calculating them. And so here's kind of, this is the double this is like scholar standard library exponential function and we can show that sure enough if we grab 30 terms we get the same result, right? Our dumb little stream which, you know, we just threw together, right? I mean it's like duct tape and, you know, whatever and it actually, it gets it right, you know? Which is cool. It's kind of neat that it does that. So, I mean clearly it works, right? No need to write any other tests, but anyway. And the thing that's neat about this process is that you can actually get more precision than double, right? So like double only told us, you know, up to that 65 there but here you can see that actually we can take it out a lot further. We can take it out as far as we want. We can take 40 terms or 80 and, you know, the number here just gets bigger and bigger and bigger. I'm sorry, it doesn't get bigger and bigger and bigger. The decimal expansion gets bigger and bigger and bigger. The number itself is getting closer and closer and closer to E squared. Which is a transcendental number that we can't just, we don't have an easier representation at hand. And so we handle all our rational numbers like this. You just have to find some kind of infinite series which is, you know, an infinite stream of things that can be added together, you know, that will approximate x and get closer and closer to x as you keep adding new terms. And then what you have to do is just figure out how many terms you actually need to get the precision you want. And again, that's where like, you know, someone who's good at real analysis or numerical analysis can kind of just, you know, do an analysis and tell you, which is cool. And then you just compute the terms, you add them and you've got an approximation. In practice, you actually kind of want series that have converged quickly. Like the one I showed you was probably not going to be how you'd actually do it, but the idea is the same. I mean, it doesn't really, it's just an implementation detail. So with real, with the actual real type, it's the same, but it's a little bit nicer because it kind of does all this work for us. So there's two, it's just the number two. And like, literally, that's not any smarter than you think it's like, however many bits you want, you just basically take, you know, put two, multiply it by what your denominator is going to be and return that number. That's, it's like almost like a constant function to return something like two. It's pretty easy. And then you can see that we can take the square root of it. We can take the, you know, cube root, the 10th root. You know, I'm sure the 10th root of two is super useful to everyone here. So, but, and then we've got some nice transcendental values too. Like we've got pi and E and then, you know, E squared exponential of two, which is, you know, the thing we just calculated. So do people kind of see, see what's been happening here? Yeah. And so then the thing that this is like, you know, this is the thing that I'm really excited about. So this was like one of my goals of doing computer real numbers was to be able to support stuff like this. And so here we're using a complex type that I haven't talked about. But basically what's happening is we're constructing values for i and for e, and getting pi. And we're saying that e to the i pi, e to the i pi theta plus one, we're evaluating that and we're getting back zero. Which, you know, doesn't maybe seem impressive until you do it like doubles or something. Like it's never gonna be zero. Like any, anything you try like that, it's never gonna come out right. So I'm pretty proud that our thing works well enough that you can do this. Yes? Well, so, I mean, so, there's sort of like two answers, right? So part of it is, I mean, basically what's happening is the part that I haven't showed you here, I'll just jump over, that's a complex number. So there's zero in the real part, it, so, right. Okay, okay, you have anticipated, so great. So what it means is that at the default, at the default representation that I choose to print these numbers at, it wasn't different than zero. So it doesn't, right, you've spotted my trick. I have a slide about this, but you've spotted my trick before I even mentioned it, which is great. No, this is great. So what's the catch? You have anticipated the catch. Yeah, right, what's the catch? And the catch is, you got me. There's an Achilles heel, which is that there's no 100% reliable way to compare them, which is I think exactly what you're anticipating. It looks like zero, but we don't know that it really is zero, right? It could be very, very close to zero, so close that however long we take to calculate it, it looks like zero, but secretly there's like a sneaky little, one out there somewhere. And that sounds crazy, so I'll try and explain, which is that all we're doing is producing approximations. We can produce very, very good approximations, but that's all we're ever talking about. We can't be like, we're done. There's no idea that we're done. It's the best approximation at a particular granularity. And we have to ask for it, so we only get what we ask for. We can't say, give me infinite precision. Or you sort of can, but then you have to wait forever, and we don't have time for that. So we ask for a computer, we have a computer real number r, and we want an n-bit representation, and we get back an x. We know that x is the best n-bit representation, but we don't know if x is less than r, if it's equal to r, or if it's greater than r. We know that at that precision, they look the same, but at a finer precision, they might not, they might, you know? And when is this a problem? So this sounds maybe like it's a catastrophic problem. It's not catastrophic, but it's important to realize. So I've got two methods here, and I would contend that the first one is probably okay, despite using a comparison test. So what's gonna happen here is if we have like a very small negative number that looks like zero, we'll use the zero branch when really we should have used this branch. But since the number is so close to zero, we multiply it by three, we're gonna get zero either way. That's probably okay. I mean, if we're multiplying by like three billion, then maybe it wouldn't be okay. But in this case, it's probably okay for most purposes. Maybe not. This is a case where it's almost certainly not okay. We're checking if something is zero, and we're returning 100 everywhere except at zero when we wanna return to zero. In this case, there's gonna be like a little penumbra of very, very small values that whatever precision you care about, they're gonna look like zero, but they aren't zero, right? So that's just a problem. And I think it's maybe what the questioner was anticipating by asking the question. And maybe it makes sense, but I'll just kind of stress it again. There's this idea that couldn't I just ask for more precision to get it right? And the answer is you're never gonna know that it's enough. So you can get the correct sign for any non-zero number, but you have to let the program hang if there's a zero. In other words, if it really is zero, you're just gonna wait forever, right? The machine's not gonna terminate. And that's almost never acceptable in our jobs. Our boss is never like, hey, tell me if the person has a positive or negative bank balance, but if they have exactly zero, then just hang forever. No one ever wants that. So this ends up being a problem for sign tests. That's like the obvious one. So the signum function, stuff like that. But the same problem affects whether two numbers are actually equal or whether one's greater than or the other. If they seem equal, they might not be. And furthermore, we can't tell which side is which. If one were bigger than what it would be. If they're not equal right away, we know that they're gonna continue to not be equal. That's not the problem. If at a certain precision, the numbers appear different, they definitely are different and we're done. The only issue is when they seem equal, we don't know that they really are. So it ends up being like a partial ordering, really. And then certain trig functions require branching, like a tan is like an example where, I'm pretty sure you have to use branches to get that right. It's definitely discontinuous, so you're gonna have problems there if you end up in the wrong branch. But interestingly, you might not have thought this, but abs min and max, despite seeming like they use comparisons are actually fine. And the reason for that is that, given that they don't actually purport to tell you whether something's positive or negative, whatever, they're just gonna give you the least value or just a positive version of the value. They're sort of deferrable. You can defer the issue until you hit a precision where it matters, and once you hit a precision where it matters, you know that it matters and you're fine. So great, so what are some plans around this type? Real does provide these methods, and that's kind of contentious. Some type of people probably aren't super happy that it does, because they're like methods that, they don't exactly throw exceptions, but they're not always correct, and that's kind of bad. So I've thought about making them restricted by default, so by default you don't get this kind of equality operator, you don't get sign tests. You'd have to do some kind of import, like I know the risks or something like that. I don't know if that's a good plan though, I feel like most of the time, for stuff we're doing it probably doesn't matter, it's only sometimes it does, but of course that's what they say with double and look where we are now, so I don't know, maybe that's not right. But the one thing we definitely need to do is right now the precision for these kinds of sign tests is not pluggable, like there's no way to import an implicit that's like, I wanna use this much precision, that we should definitely do that, that's definitely on the radar, so, and I don't know if this type's getting a lot of use, but if any of you are interested or are using it, I definitely value your feedback or ideas, because I'm sure that it could be better. I definitely, I mean it's just something that we have, but it could definitely be better. How am I doing on time by the way? I have like no sense of time, okay? Perfect, that's great. So there are some alternative solutions, so how many people here have used or heard of continued fractions? Okay, like a few people, so there's actually an experimental branch that has continued fractions, and those end up being a different way to encode this same idea. Someone I think at any scholar this year was like really mad at me that I implemented computable reels instead of continued fractions and was like really insistent they were just better, so I went ahead and tried to do them, and they might be better for some things, but it turns out they actually have most of the same drawbacks, and I find them a little harder to work with, I have much less intuition about them. I think some people like them a lot more. If someone out there like loves continued fractions, I'd love to like work with you or ask you questions. So they're a little bit brain bending. I was thinking about going into them for this talk and decided against it. There's also a cool thing that I regret, I don't really have time for, which is there's an algebraic type spire has, it's like a little more constrained than real. It doesn't support trigonometry, it doesn't support E or pi. It does support roots, and you can use it with FP filter to do interesting work where you're mostly using floating point, but it kinda like detects when there would be a floating point problem and uses this more precise method of calculation in those cases. The thing that's good about algebraic, which I turned it, I left off the slide somehow, is that you can do sign tests. It always knows whether it's positive, negative, or zero. It doesn't have the problem computable reels do. So if you don't need trig, but you do need roots, and you also need good sign tests, then this is definitely the type you should be using. And so here are the papers it's based on. We actually have one of the, I think advisees of Yap, who's the person who kind of figured out this whole scheme here. So that's sort of exciting. They're cool stuff. I haven't really worked with algebraic a lot. I didn't wanna present it. I think Tom should. So if you look at the last slide, I sort of did a call out. Tom Switzer should do a presentation on this. As far as I know, he hasn't done one. And you could imagine implementing symbolic evaluation where you don't even try to create approximations. You work with these terms symbolically and you use complicated rules from real math to figure out what the answer is. Like, Mathematica kinda does this. SimPy does this. Someone opened an issue about this I think on GitHub and was like, why don't you have this? And the answer is, it's like a ton of work and I'm not, I don't know how to do it. I mean, it sounds cool. If someone is like, someone out there is like, I wanna write you a check to do symbolic evaluation inspire. I would sign the check, take the check and a heartbeat, quit my job, I would totally do it, but I don't really think that's gonna happen. So the end result kind of like from my talk is that there's no magic bullet, but I think there are better bullets. In and long are fast and dumb. Like if you really are like, yeah, I want a fast and dumb type, I don't care if it overflows, then great, but like often we don't really want that. We're just sort of, we're using the fact that there's a nice built-in syntax for them. Like we don't wanna have to put, you know, a type annotation or a type constructor or something. In those cases, maybe consider using safe loan. It's, you know, Python programmers get on, they essentially use the equivalent of safe loan every day and they're perfectly fine. You know, all their programs work, you know. They don't have overflow issues. So I mean, sometimes it seems like a nice way to live. Like closure people have the same thing. I think they've got like an expanding integer type. It's pretty cool. I kind of wish it was easier for us to do that. Double is fast. And I would say it's as smart as possible, but no smarter. And it's not possible for it to be that smart. It doesn't have that much space to be smart. So, you know, it's great, but it has these issues. And, you know, I definitely can't keep all these issues in my head. So primitives by default, I don't know if that's like a good design philosophy moving forward. Like in 50 years, if people are still using like 64 bit numbers and having these problems, I'm probably going to be a little disappointed. You know, assuming I'm not dead, right? I mean, if I'm still alive, I would be disappointed. So if you use double, you already work with approximations, whether you know it or not. Like this stuff seems kind of scary and weird, the idea of these error bounds and approximations. But like, we're all doing it. Like whether we want to admit it or not, we're doing it, we're just not paying attention to the result and we don't really know what's happening. So, you know, if you want to know what's happening, I suggest using rational if you can, which might, you might in practice find that it's bad, but at least, you know, it's going to be a sense of what's going on and you can use real to do approximations correctly. So the idea with real is even though it has this issue I talked about with sine, it's no worse than the issues you would have with double already. In other words, you can do all your work with real, get a double out at the end. You know that there have been no intervening issues. You know that all the additions and stuff are all correct and then you can take your double and do your sine test or whatever else you want afterwards. So it's essentially like instead of paying a numerical analyst who knows what they're doing, you can sort of pay a, you can run a computer program that doesn't know what it's doing, but it, you know, has it encoded and you get something that's like a little bit slower than what the numerical analyst would come up with, but you didn't have to pay them and you know that it's, you know, pretty reliable. Like I said, there's no bullet, you know, silver or magic. And previous talks that I've done or I think Tom's done have talked about generic numeric programming and this talk isn't about that, but hopefully you can kind of see why it's exciting because if you only have one type to use, if all you have is double, if like you're in JavaScript land, then that's all you're going to be doing and you don't need anything else. But in our world, we have all these cool types and it's like, I'd like to be able to use them, see how they work, like let's compare double, let's compare rational, let's see how they run for this certain algorithm. And if you implement your algorithm generically, you can try it, you can try it using a rational number and see it's actually almost a way of doing like after the fact analysis where you're like, we wrote our thing, we run it with doubles, how wrong are we? Like let's have a bunch of test cases and measure the error and we'll see how we were doing. Like in production, we can use double if we're really worried about speed, but we can actually test it with rational or real and get a sense of like how bad it is. Like are there cases where our double code is just falling over and going to return like tons of error or something? Anyway, I sort of hope this presentation kind of motivates that a little bit in terms of like being interested in doing that. And you can learn more about this stuff in the Spire users guide, which I have linked here. Then the final thing is like, the real number types like a small part of Spire, it's probably not the most exciting but it really relies on the whole library and a lot of work that like tons of people have done. I mean, I think there's like six people here who've worked on Spire. So I definitely want to like sort of give a shout out and thank all the people who've been participated like submitting patches, fixing bugs, reporting bugs, any of it. So hopefully I'll meet you all in person and say thank you but you know, collectively thank you. So this is the end of the talk. I'm happy to like do little demos or answer questions but that's all I've got.