 and beauty of this very simple algorithm, even in the form that I've written in here. Okay, enough, any questions before we jump into the code? Okay, so let's go ahead and look at... Sorry, we don't want to look there. We want to click on the links that I helpfully provided. And these links should work for you as well. Are we having any issues, Edgar, with the server? Let me manually open it up. All right, let me do it on time. Okay, so I think what I'm going to do in the interest of time is I'm not going to put you through the pain of watching me type this in. Although, pedagogically, that might be better. I mean, that's sort of the moral equivalent of me writing out the theorem on the blackboard and having you watch as I write every sentence. But I think it will be more efficient because I want to make sure we get to the sums of cubes if I show the algorithm and just talk you through it a bit. Okay, so we'll want to flip back and forward between our instructions, our recipe, the pseudocode that we had written down and the algorithm that we're implementing. So here's our instructions. We're supposed to, first step is to create a vector of n Boolean values with the first set to true. Sorry, with the first all, but the first set to true. Okay, and so here in my implementation, I'm in, this is in GP. So it's defining a function called sieve of n. This is a more complicated function than the Fibonacci function. So I'm gonna, it's going to have multiple statements, which I enclose in curly braces, just as you would if you're familiar with the sieve programming language. The next thing I'm going to do is create a vector of n Boolean values. Now there's not a special Boolean type in GP. So these are actually integers. And by default, they would take up at least 64 bits. I hate using 64 bits for when I only need one. So I'll at least make them vector small, which means they're only gonna take up 16 bits. I guess that's a reasonable compromise. So the way this function works is the first argument is saying how many elements of the vector there are. The second, excuse me, the second argument is actually a function that's going to be evaluated for each integer from one to n. And then the third argument is the function that's going to be instantiated. So x is gonna be set to one for x from one to n. That's what that is saying. And this is all enclosed in a my clause. And this is the single most important thing you need to know if you're writing in GP. If you forget everything else, you can easily go look it up in the reference card. But in GP, by default, all variables are global. And I mean really global. So if I write one function and it has a variable called x and it calls another function that has a variable called x, they're the same x. So if I call a sub function, they can modify variables that maybe I only created expecting them to be for temporary use that I was gonna throw away when I was done with them. And that can lead to very confusing behavior. And especially if you're used to programming in almost any other programming language because almost every programming language today uses what's known as lexical scoping. And by default, when you create new variables, they only exist within the lifetime of sort of the code block that they're contained in. But that is not true with GP. And confusingly, GP actually provides two types of local scoping. The one that you might think to use involves the keyword local, but that actually does dynamic local scoping, which is also not what you want. Because there, the scope of the variable or which, who wins when they decide to introduce a variable x depends on the timing of when they're run. And especially in a notebook where you might be clicking around to different cells and executing the same code in different orders, using dynamic scoping will be very confusing. To get scoping that matches what you're used to if you program in Python C or any other programming language that exists today, pretty much except, yeah, I don't think even early versions of Lisp that had dynamic scoping are still around, what you want is mine. Okay. So with that, that's sort of the main thing to take away from this slide. Now we're running a for loop from where P is going from two to the integer immediately below the square root of N. And then we're saying that was our second step we're doing for each integer from P to two square root of N for which SP is true. So we have an if clause here. We're now going to iterate over the multiples of P greater than P up to N. So M is gonna go from P plus P up to N with a step size of P. So this GP iterator called four step does exactly that. And we'll see how this works in other languages. One thing that's cool about GP is it has all sorts, it has at least a dozen different four statements that iterate over different things. So these are the simplest example of non-trivial examples four step, but there's even better ones like four divisor or four factored integer or for all sorts of interesting things. So you can construct a loop that iterates over standard things that number theorists tend to be interested in without doing any extra work in GP, which is a very convenient thing. In this case, we're sort of only using it's a very baby instance of it. So for each of those multiples, we're gonna set our Boolean value at the Mth Boolean value to zero in GP zero and one. One is true, zero is false, really anything that's not zero is true and zero is false just as in C. There's no true or false keywords. And then at the bottom, I've just written some code to just test that it does the right thing for the first 30 primes that account can correctly count the primes up to 1000 and it prints all as well, just because if these things succeed, nothing will happen and I wanna have some sort of visual evidence that everything went well. Okay, and now I'm gonna, let's try timing it. How long does it take to compute the primes, to enumerate the primes up to 10 to the seventh? So it takes about three seconds to find that there are 664,000 some odd primes. Of course, GP includes a built-in function for doing what we just did. There's no reason for you to write this function yourself. The point of this exercise is to give a non-trivial an example of an implementation of the kind of function you might want to write yourself. And it's reasonable to at least, you know, time it to see how it does, especially because we're comparing some of these different systems. Okay, while I have the GP code up, is there any question before I move on to the next page? I'm happy to come back if people think things later. Just as a heads up on the problem set, you're gonna be writing a much more interesting version of this sieve that computes not which integers are prime up to n, but actually computes the prime factorizations of every integer up to n with essentially the same kind of complexity, maybe slightly different, but very close to the same complexity, which is a pretty amazing average polynomial time algorithm for factoring integers, which for which we don't even know a deterministic algorithm that it runs in polynomial time. Question? Now this is the micromancer you've got there and it counts for the variation of the variable s, the variation of the variable x, or something. Yeah, so s is the variable that is gonna be locally scoped and here is our input, so it's referencing the same n. That x is actually just a function, not a variable. It's the identity function. Yeah, I realize that's kind of confusing. It would be less confusing if it was something more interesting. Or actually maybe a better way to think that is that variable x is gonna be ranging from one to n and it's sort of implicit in the vector statement, just like when I do my fourth step, m equals mp plus p, that m is scoped to within the fourth step. So the my only needs to be around thing, they're new variables that you're creating that aren't part of a built-in iteration that is built in GP. Another question? Comment, Edgar. Well, the p is scoped by the four. Yeah, yeah, thank you. So the p is scoped by the four here. So after this four finishes, p is no longer defined. Yeah, so the only new variable I created here is this vector s. Everything else was part of a loop iteration. Okay, let's take a look at the magma code now. Okay, so now we're defining a function in magma. This is done by using keyword function syven. Now this is not so different from GP. We don't have any curly braces anymore. Important thing to remember in magma is whether it's function if for a while, there's always a matching end function if for a while, whatever. So my recommendation is whenever you're writing code in magma and if you had a clever code editor, you could even configure this in Sublime or Emacs or something, whenever you type function, you should just type in function right away so you don't forget and put a semicolon at the end. Okay, it's not strictly necessary in the notebook. If it's the last thing, if Edgar was kind enough to add the semicolon in some cases, it will still work if it's the last thing in the cell. But I recommend just, when you're using magma, just put a semicolon after everything. And if you're like me and you're paranoid, I basically put a semicolon after everything in all of these languages because it never hurts. I tried to avoid doing that because I know some of the Pythonistas in the audience will be offended if I start putting semicolons after every Python statement. Okay, okay, so what are we doing? We're iterating for, this syntax is maybe a little more intuitive than the GP syntax. For P is going to, the other big difference, let me start actually with the S. The other big difference in magma is it's more mathematical in the sense that assignment is denoted by colon equal. The equal sign means something quite different, something much more like a mathematician might mean by equal. It's saying that two things are equal. It's actually a relational claim in magma. So to do assignments in magma, you use colon equal, but in GP, Julia and Python or Oscar and Sage, a assignment is done with the equal sign. And so we're creating an array here where every element is set to one and we're doing with us with what's known as a list comprehension, which is something that all of these languages support in some fashion. I didn't use it in the GP case because it has this very nice convenient vector initialization operation that I thought I might as well show you, but I could have done the GP, written the GP code with a list comprehension. And you should think of a list comprehension as something that's very familiar to mathematicians. We would think nothing of writing down. Well, let's consider the set of all X in blah, such that blah, such that X satisfies blah. So we might use a curly brace, X an element of Z colon or maybe a vertical bar and then some condition on X. X is less than a thousand or something or X is a prime less than a thousand and then a closing curly brace. That's exactly what a list comprehension looks like. And so here it's kind of a funny list comprehension because the value rather than it being X, it's just true. And this is the equivalent of assigning one to X in the vector small statement we saw in GP. So I'm just saying, so for every I in integer from one to N, true. Just set it to true. Okay, so this is the set of the value true for all I from one to N and then we're setting the first one to false because one's not prime. Then we're iterating over primes from two up to the floor of the square root of N. In magma, all of the intrinsics are in what's known as camel case, meaning it's if it's one word, it's uppercase. If it's multiple words, each the first letter of each new word is capitalized. I often when I'm writing my own code, especially if it's just functions that I'm not necessarily sure about. I'm there the official final version or maybe I'm just using them in other functions. I'll tend to write them in lower case just because I don't get them confused with an intrinsic. But one of the great things about magma, we'll see this tomorrow, is you can create your own intrinsics. Intrinsics just means a function that's quote built into magma, but it really means it's just any function any function that's been defined in what's known as a package file. Magma loads up thousands of these when it starts up of its own intrinsics, but you can write your own and they will work the same way all of magma's intrinsics do, including you can do tab completion on them, you can get documentation on them and it will do all of the type dispatching that it would do for its own intrinsics. So you can even preempt some of magma's functionality. If you don't like the way magma performs a particular function, you can write your own version that will get called in the special case you wanna modify and then it'll go fall back to the magma version when it doesn't satisfy your type declaration. Anyway, that's just a ramble on intrinsics. For now let's just focus on the basics. I'm calling the magma intrinsic floor, which returns the integer, least integer or the greatest integer below this real number, square root of n. And if I have ticked the box, meaning it's set to false, I'm going to continue, I could have instead said if sp and then put an if and then put this inside the if. I just wanted to show something slightly different. So this is a standard sort of trade-off when you're writing long loops. It's often convenient to just jump ahead to the end of the loop. If you're in a case where you know that needs to happen, then to put in an if and have to keep indenting if blocks until they get so deep that you lose track of where you are. It's sort of six of one half a dozen of the other case. I leave it to you to decide which you think is the most appropriate. And then here's the loop where we're crossing off the multiples of p from two p up to n. So for m going from p plus p to n by p. So this by p is maybe something that not everybody is familiar with, but in magma anytime you have a four i from one to n you could always add a by blah and it'll actually enumerate an arithmetic progression. We're gonna tick off the box by setting s to false and then at the end we're gonna return the indices of all of the unticked boxes. So this is another list comprehension. It's all the integers x from one to n such that s of x is false. Notice these s, this vector is a vector of Boolean values. So it makes perfect sense to just treat this as a Boolean condition. I'm just testing is s of x true or false? I don't have to say, you know, is it equal to true or equal, just it is true. All right, let's go ahead and run this. Great, it says all as well. We ran the same test we did before and let's go ahead and see how long it takes to count the primes up to 10 of the sum. I mean, it's doing a lot more than counting. It's actually enumerating all the primes. I don't wanna print them all out. So I'm just using the sharp symbol, which again is familiar to mathematicians. We use this very often to denote the cardinality of a set, but you can apply it to any sort of enumerable object in magma to ask it to tell you how many elements it has. Okay, and it was a little faster than GPU. It took about 2.4 seconds. Okay, let's take a look at Oscar. Okay, so here's one where I am gonna need to load Oscar before I run the code. But while it's loading, so it's actually working now. This, as I think Edgar mentioned in one of the tool notes, this takes like 20 or 30 seconds, we'll let it run. And we can look at the function that we're gonna write while it's doing that. So here the syntax is quite similar to magma. So for those of you who are not familiar to Julia but are familiar with magma, you'll find it quite easy going from magma to Julia. A lot of things are similar. For Python programmers, it'll be a little different. There are a lot of differences between the syntax of Python and Julia. So we're declaring a new function, function sieve. Unlike magma, you don't always, you don't have an end function, but you do have to have a word end. So I have function sieve end, and it uses the same end. So we end for end functions, we end if statements, we end fors, et cetera. Question. Thank you. Yeah, just because you're staring at a cell and thinking it's working away, it doesn't mean it is. A good tip, which I'm sure is what Edgar noticed, is that this thing should be black, meaning it's working hard. And yeah, or a star, a star next to the cell, this here. And there it is, it loaded. Okay. All right. So let's continue. So now we're gonna, similar to the vector statement in GP, Julia has a fill statement that can allows you to very quickly initialize an array to a given value. So this is going to return an array of end values all set to true, and I'm assigning that to S. In Julia, the equal sign is assignment. And then I'm setting the first element to false. Okay. And I should probably mention now, for those of you who are more familiar with Python than any of these other three languages, GP, Magma, and Oscar, and Julia, all index arrays and vectors from one, V bracket one is the first element. That's not true in Python, where V bracket zero would be the first element, or as it would be in C. Okay. Here's our loop where P is going from two to the integer of the value returned by floor of the square root of N. And this might seem really silly and annoying to you. You might wonder, why is floor not returning an integer? It's defined to be an integer. How could it not return an integer? Well, it returns a floating point approximation to that integer. I won't go into debating the reasons for that design decision, but this is not unique to Julia. There are many other languages that do the same thing. If we actually, but Julia is strongly typed. So if we actually want an honest to God integer, we have to make it an integer. And that's what we do with this. I'm actually going to make it a 64 bit integer, which is the default type in Julia for integers by putting an int 64 paren paren around it to cast this floating point number into an actual integer. The colon here, so this two colon law is how in you, you in Julia, oops, I shouldn't have done that, is how you denote a range where before I would have had maybe bracket two dot dot something in Julia, the equivalent is two colon law with no brackets around it. That takes, it took me a little getting used to it. It's a bit different than what I've been used to. Then we're going to check if sp is set to true. And if it's not, we're going to continue. Yeah, question. Sorry, say that one more time. Why do you have the store question there? You already have the store question there. I could have, I'm not a hundred percent how in 64 handles the floating point conversion. My guess is it does take the floor. You're probably right. Most likely reason is I would wrote the first version of this code I was written. I wrote it was not in Julia and I was cut in pasting and I said, oh, this is not an integer so I should wrap an integer around it. Okay, yes, it's probably a more elegant, well we can try it. Let's, that's the great thing about doing this interactively. Let's get, maybe we don't need this floor. Let's just get rid of it, see what happens. Oh, that's why the floor is there, because you need it. So it's trying to protect you. Julia's saying, it doesn't look like an integer to me. What are you doing converting it? Whereas if you were writing in C, it would of course just say, yeah, it's an integer and drop everything after the decimal point. Okay, so now let's try Julia and I'll just remind you how long it took Magma and GP. It was like two or three seconds. And so this was 0.06 seconds and a good chunk of that time was spent compiling the code. I could run it again. It'll be a little faster the second time. And so this is another thing I wanted to note about Julia because this is relevant. It won't be so relevant to the functions you're gonna be writing this week, which are gonna be short and simple functions. But when you're invoking functions built into Oscar, that might be quite sophisticated like computing a class number. There's a lot of code that needs to be compiled before the function can be run. And Julia is built around a notion of what's known as just-in-time compiling. So it basically gives you the appearance of being in interpreted language, which is very convenient. You can sort of just be changing things willy-nilly and it's very convenient when you're programming in notebooks. But behind the scenes, anytime you're running code that hasn't been run before, it says, oh no, I need to compile this code first. It very quickly compiles it into sort of a pseudo, what's called LLVM, a pseudo virtual version of sort of a low level assembly language virtual machine and then runs it. And for simple functions, that happens so fast that you don't even notice it. If I hadn't put the at time in front of it, you probably would have been impressed with 0.06 seconds just as much as 0.047. But for very long functions that first invocation, it could very well spend 99.99% of the time on the compilation and 0.01% on running it. So when you go to compute like the class group of Q squared of five, it takes maybe 10, 15 seconds. And then you go to compute the class group of some degree 43 number field and it might be instantaneous. And the difference is because in one case it was compiling the code in the second case it wasn't. Okay. All right. And finally, I saved Python Sage for last because I expected this is the one that is probably most familiar to you. In fact, Sage even comes with an Eratosthenes function built into it that William Stein wrote. But his SIV is more clever than this one. I intentionally made the SIV as un-clever as I could possibly do. So the Eratosthenes implementation that's built into Sage actually only iterates over the odd numbers. It doesn't just assume that it knows one's not prime, but assumes it knows two's prime before it starts. And that makes it a little faster. So what happens to Python for those who haven't seen Python before? So when we're defining a new function we don't use the word function anymore. We just use the word def. Def for define. We have a SIVn and then we have a colon. Sort of every sort of function definition if, for loop, while looping Python there's a colon to indicate that you're starting the loop. And then the first next thing that should come after a colon is always an indentation. Typically of four spaces. And this is very important in Python. This program will not work if those four spaces aren't there. So in all of the other languages the spaces were there just for visual convenience and it wouldn't have mattered if it was three spaces or five spaces or 17 or none. But in Python the program won't work or worse it will work but it'll do something different if you change the indentation level. And this is particularly important to know if you're cutting and pasting code make sure you always get all of the space at the beginning of every line. All right, so in Python there is a way to very quickly initialize a list of constant values. I'm starting with a one element list and then I'm quote multiplying it by n plus one which, and here this star really means concatenate it with itself. So this is the n fold, or n plus one fold concatenation of this one element vector with itself. Why n plus one? Well, because as I mentioned Python index is from zero and I really want to be able to access entries.