 Yeah, I'll probably now be less able to be reactive and the single, like what is asked in the HackMD and chat, but please do not hesitate to interrupt me and I guess also Jarno you can DJ me if you want to follow the chat and HackMD and try to answer and bring up anything that is especially interesting. Yeah, perfect. So can you see my empty notebook now? No, I don't see reactions. Okay, perfect. Yeah. So now we are going to talk about multiple dispatch, which is actually one of the things that makes Julia really Julia. So one of the cool things of Julia and here we are just giving like a basic introduction if you want to know more. There was a talk from JuliaCon 2019 called the unreasonable effectiveness of multiple dispatch from Stefan Karpinski, the creator of Julia. You can find that on YouTube. And if you want to know more, you can look at it and know like how multiple dispatch works internally and why it's very powerful. But now to understand what it means to have multiple dispatch, let's take an example. So suppose I want to define a function, let's call it f of x, which is say two times x, for example. Okay, simple function. Oh, this shouldn't take that long. Okay, good. So as we said before, this function Julia uses stack typing, which means that now if I just say x, this x can be any method. And then when it can be any type. And then when I call this function with something, for example, f of one, then it will just run the code and see, okay, now here I have this star operator, is it defined for an integer? And if it is, it will work. So if this one behaves like an object which has this star operator, it will work. But sometimes we may want to have different behaviors for different types. For example, maybe I want to define a function, my func, which behaves differently for, for example, integers and for floats. And that way I can achieve this flexible behavior using multiple methods of the same function. So what it means is that now I can do my func x of say an integer. And I want that this function with an integer will add one, for example, like this. Now if I call my func of one, it works. If I do my func of one dot zero, it throws an error. It says no method matching my func called with a float. And these candidates are my func integer. So what does it mean? This means that my func is only defined with inputs of type integer. And there's no definition of the function when the input is type float. And this instance of a function with a specific type, it's called method. So now we say that my func has a method which works with integers. And if I want, now I can add more methods. For example, I can do my func of x of type float 64, for example. And say if I call it with a float, I want to multiply by two. That doesn't really make sense. But if now I do methods, my func, now it says that now I have, sorry. So there's this function called methods, which takes its input to the name of the function. And it will list the methods associated with that function name. So now it's saying that with my func name, there are two methods associated. One method defined with integers, and one method defined with floats. OK. And so in this way, I can define different behaviors for integers and floats. So now if I do my func of one, let's say my func of two, it brings a three. Because for integers, it's defined as x plus one. But if I do my func of 2.0, for example, so now this is a float, I get four. Because now it's calling these other methods defined for floats. OK. And now let's have a quick poll, quickly. So suppose now I define my func of x, which is just a number. And in that case, it just returns x. OK. Now I do methods of my func. And now I see I have three methods. One method defined when the input is integer. One method defined when the input is a float. And one method defined when the input is a number. OK. And now if I call my func of, say, two, what do you think that would be? So now two is an integer, but it's also a number. So what do you think will happen now? Can you, I don't know, put a green mark if you think it will print four. And so a green reaction prints four. And red reaction prints two. What do you think will happen? Oh, maybe I spoiled it a little now. Oh, now I spoiled even more. OK. I'm sorry. Yeah, so there was a comment in the chat that maybe error. And that would also have been like good, like guess. So now it prints three, because what it does is that if there are multiple methods, it chooses the most specific. So now these two is both an integer and a number. But integer is a more specific type. So it's said to be a subtype of number. So every integer is also a number. So this method is more specific. And hence, it picks the more specific method. OK. Yeah, sorry, I accidentally pressed the Enter and spoiled the fun for everyone. Oh, oh, yeah, it was also this method. Yeah, this one. So with integer. So with integer is defined as x plus 1. And so it called. I guess we could use hackMD to do the poll, but maybe next time. Yeah, true. If you have more than three options. Yeah, true, sorry. Yeah, I didn't count to my mind. But yeah. And now if you want to know which method it calls, I will maybe talk later about what this at which means. For now, I think it's at the magic word. And if I type at which, and then I call the function, it will tell me which method it's using. So now it's telling me that it's using the method with integer. OK, great. But the power. So far, we have used only like one single input. That's not like maybe very exciting. But the powerful thing of multiple dispatch is that I can use it also with multiple inputs. So suppose now I have, I want to define a function, my func. So a function which we will call add, for example, OK? And what do I want this add to do? I want that add can sum two numbers or concatenate two strings, OK? So I want to define a function called add, which works both with numbers and with the strings. And if it's called with numbers, it will sum those. And if it's called with the strings, it will concatenate those two strings. And how can we do it? We can do add. And the first input we specify that must be a number. And the second input must be a number. And now we say that when I call the function add with two numbers, then it will return the sum, x plus y. If I call the function add with two strings, string, then I will concatenate the string. And string concatenation is starting, Julia, with this star operator, with the multiple operator, OK? Now we have defined the function. And it says that it has two methods. We can verify methods add. We have a method with number and number and a method with a string and string. And now what we can do is we can do add one two. It will return three and add the hello world. And it will concatenate the strings. And this is very cool because the same function will behave differently depending on the type of the input it's called with. And that's like a very powerful feature of Julia because if this is done smartly, is wouldn't that essentially be polymorphism? Kind of, but not quite. I'm not expert in programming language theory, but my, well, it's kind of polymorphism. But polymorphism is a concept associated with object oriented programming. So you have a class and then you can associate like the behavior with different classes. Like if you have some classes. I was planning to comment on this. So I mean, this is what happens in object oriented languages is called dynamic dispatch. So of course, the word multiple dispatch is a sort of, I mean, it comes from the same. Or it's sort of trying to tell you that it's the same idea as dynamic dispatch. So dynamic dispatch will check the type of the first argument, which is the, when you say object dot method, the first argument is the object. It will check the type of the first argument. And based on that, decide which function, which method to run in polymorphism. Essentially, what polymorphism as to that is runtime type inspection. So you don't necessarily need to know the type of the object when the code is being compiled. If you're familiar with a language like Python, then that isn't really a huge difference because it's not compiled anyway. So all the types are runtime. But in C++, for example, the difference between a runtime type and a compile time type is a big deal. Yeah. So what dynamic dispatch does is essentially the same thing, but it checks all the variable types, all the parameter types, not just the first one. And of course, the syntax is very different. Yeah, and also the powerful thing of Julia, which is, I guess, also how it differs from this object-oriented stylish polymorphism, is that now, when I call this function, I'm giving two strings. And now, when Julia compiles the code, it knows indeed that compile time that these are strings. So it can actually choose the method before the computation at compile time. It can already choose what method to use at compile time. And that's also one of the strong reasons why Julia is efficient. Because if you do things smartly so that you know the type of your variables at each step, then Julia can choose the methods all already at compile time. And that's where the speed comes from. So that also explains the use of the word method here. So in object-oriented languages, classes have methods. And then those methods can be called with objects. And in Julia, it may seem a bit weird. The functions have methods. But if you think of it as being, instead of belonging to a class or being depending on the object, it depends on all the variables in all the parameters in the function. So it kind of makes sense then. It doesn't make sense to assign them to the first parameter or any of the parameters. But it does make sense then. You have to have it someplace to assign it. So it does make sense to assign the methods to the function. So it's kind of that's why we use the same word, the word method. Yeah. Yeah, perfect. So now I have defined this method for two numbers and for two strings. For example, I may want to define mixed combinations like number and the string. So now, if I call it like this, add one tie. It throws an error because it says there's no method matching. This function called with an integer and the string. The options are like number and number or string and string. But I can also define this method if I want. So for example, I can do add of x when is a number and y when is a string. And this will do the, I mean, I can decide what it will do, but it could maybe like interpolate inside the same strings or something like this. And now if I do add one high, it will concatenate. So it will interpolate the values inside this and return a string. OK. I should also define the symmetric if I want. So I can do add of x when is a string and y when is a number. And in this case, it will give, I mean, I can decide will do just x, y, OK? And this will do it. So add the high three. Yeah. So the key idea is that in Julia, you can associate several methods with the same function name. And you can use these to specify the different behavior of the function depending on the type of the input. So the same function, you can define the different behaviors for when the input is number, when the input is string. And if you want to see the list of methods, you can do methods and the function name. And it will print everything. So it will print the list of methods. And let's take, for example, let's have a look at how is the plus, the Julia built in plus defined. Let's do method plus, oh, methods plus. And you can see it has 190 methods defined. So we have a generic one where they're both like integer and side integers of the same type. A method for when one is inside integer, one is begin. It doesn't matter. The important thing is that defining the different methods for different scenarios. So a method for addition when they're both inside integers. A method for addition when one is inside integer and one is floating point allows you to have optimized code for each one of the different scenarios. So I can use an optimized code for the case when they're both inside integers. An optimized code for the case when they're both floating point. An optimized code when one is floating point and one is integer. And doing this, since I know if I know the types at compile time, so if I know the type of the variables when I'm typing, then the Julia compiler will be able to decide already before computation what method to pick. And this will allow to produce very optimized code. Yes. Then there was this question in the chat before about the example with update. So someone had, so the idea is that once you define a method, you can overload it if you can redefine it, but you cannot delete it. So if I define f of x, y, just like this to be, okay, let's do like f of x integer, y integer to be x plus y, okay. I define the function, it works, f12, okay. Then if I change my mind, I say, wait a second, I don't want it to work for all integers. I want it to work only for unsigned integers, for example. So I think I redefined it and I do fx of unsigned. Y of unsigned, unsigned equal x plus y. So now if I'm not like familiar with Julia, I think, okay. First I had defined to work with both with integers. Now I'm defining to work with unsigned integers. I think it works. But then if I do f of minus one, minus two, it still computes it. And the reason for this is that if I do methods f, okay, I had this one, which I had from the previous example, but now I see that when I redefine, I didn't replace it. I added the new method to the previous one. So once I have defined the method, when x and y are both integers, I cannot get rid of that except by killing that apple, the Julia session is starting again. So I cannot redefine it, fx is integer, y integer. And now I can say that this is x minus y, for example, or I can have it through an error if I want that will do f minus one, minus two, should be the difference. But I cannot get rid of, I cannot delete it. So there's some conversation going on in the chat. I think it's better to bring it up because this is kind of a, this is a very important topic why Julia is fast and how it works. Yeah, so the first question is indeed great, well done. So, because we talk about a compiler and an interpreter. So in Julia, whenever you run a function, that function is first compiled and then it's run. If you run it for a sec, of course, yeah. If you run it for a second time with the same parameters, the same types of parameters, then it doesn't get compiled again. Of course, and it just, it uses that compiled version. But so basically in Julia, whenever you are inside a function, that will be compiled code. So, and it's compiled with LLVM, which often is used to compile C and C++ code. So it is basically equally fast as C code. But that only happens when you're inside a function. So the basic compilation unit. So in C, for example, it's a file. A one file is always compiled into one sort of binary code file. In Julia, it's always a function. So functions are always compiled. Whenever you're inside a function, it is compiled and it always compiles basically one function at a time. Of course, if you are calling a function from inside a function, also that function gets compiled. So, but the basic compilation unit is one function. Yeah. So if you want something to be fast, you need to put it in a function. Yeah, that's a common like mistake when people start to use global variables and then things inside functions. But yeah, then there was another question. So it compiles automatically. And if you can compile an entire script, so I tried to also answer that second, well, that last question. So essentially the answer is no, it will compile the functions. Yeah, so if the question is like, can you get an executable, like any major executable images from the file? It's not super easy. There's a package to do it called the package builder, I think, so principle there's a way to do it, but it's not exactly super easy. Or maybe it's easy and I just cannot do it. Well, but I mean, that's not really, that's not exactly the right way. It's not wrong to create an executable, but like in Python, that's not really the right way. Yeah, it's not the Julia way. Yeah, one last thing I wanted to mention about this like topic before we move to exercise is that the common misconception when people start to Julia is that they think that they should always put like type annotations to make the code faster. So people may think that if I just write X, Y, well, let's use a different name now, GXY of X, something like this. People will get started with Julia. I think that if I don't specify the type here, then the compiler will not know the type and will not be as efficient as it could be. That's actually not true. So if I define a function like this and then I call G12, now that Julia sees that this is an integer and this is an integer, it can already figure out by itself that this is like to call this with integer and integer. So when it called this plus, it knows that this plus is called with integer and integer. So adding here the specification integer and integer, it doesn't really make your code faster. That can be used to restrict the behavior. If you want that it doesn't work with non-integers but putting all the annotations, just thinking that we'll make it faster, it actually doesn't make faster. Annotations are used to specify different behaviors. What about multiplication science? So then the function, the compiler doesn't know in advance what X and Y are their integers or spin. You mean like if I define the function as this or? Yes. So as much as I know now the multiplication sign means as well as concatenation of strings. No, no, I mean, this means concatenation if it's called. So this has different methods and if it's called with the strings, it means concatenation. So the method multiplication string string means concatenation. But if it's called with integers, it means multiplication. And if I just do G12, now it knows this is an integer, this is an integer. Now I want the star with integer, integer. And under the hood is implemented as multiplication. So it knows it. So the Julia compiled the function during call of this function, not definition. Yeah. Yeah. Yeah, so it is compiled when it's called and for the types it's called with. Yes, thank you. Okay, so are there other questions or comments? Or otherwise I think we could have an exercise about this and then move on to the next notebook. So the way we split the second half of the day after lunch break was that we have 40 minute sections which 40 minutes is now gone. Yeah. I think we do need one more 10 minute break. Okay, so let's have maybe a... Well, maybe a five minute break because then it's only 40 minutes and it's an exercise. Yeah. Okay, so let's have a maybe small exercise and then a break. What do you think? Yeah, that's fine. So give a thumbs down as a reaction if you want the 10 minute break. Okay, I think we're fine. Okay. So nobody gave me thumbs down as present. Okay, so as an exercise I think for example we could play rock, paper, scissors, okay? So I assume everybody knows the rules of rock, paper, scissors. So what I'm doing now, I'm defining three types. Stroke, rock and just like this so without like parameters, I have to construct the paper and construct the scissors, scissors, okay? So now I have three types, rock, paper, scissors. And now I can create a rock. So this is an object of type rock, a paper and a scissors. Oh, scissors I had written now. Let's go with scissors now. So the exercise is write the function play X, Y which well implements the game. So like if the first is rock and the second is paper, paper wins rock so should the print play X, Y. So which prints which one wins? For example, play of rock and paper should return second wins. And I mean, you don't need to do all the different cases but just to get a taste. Play of rock, rock, could play in print ties for example. Sounds like this. So maybe 10 minutes for this. I think it's appropriate and then we can have a break maybe. Okay, so I guess we can have a quick sketch of the solution. So the idea is that now I define different methods depending on the type of the inputs. So like if the first is of type rock and the second is of type paper then I will print second wins. And also note that if I don't need to use the inputs like this, I could write like this when the input is X of type rock and Y of type paper if I don't do anything with these variables I can just like leave those unnamed and say that when the first input is of type rock and the second is of type paper print, return the string second wins. When, what do we have? Rock and rock, tie, play paper, scissors, second wins and for example play paper, rock, first wins. Now I can say I have these four methods defined and I can play play, rock, paper, second wins. Play, paper, scissors, for example. And I didn't define that one. Okay, what did I define? No, I defined that. Oh, it's called scissor, paper, scissor, second wins. Play, rock, scissor, scissor, scissor, scissor. Oh, it's called scissor, paper, scissor, second wins. Play, rock, rock, second wins, tie now. So for example, if I had to find all the methods I could also like create a function that generates those random nets so it would be like a real game instead of just piping the offset that signs. Do you have questions about multiple dispatch and about these methods, like methods, functions? Yes, if you try to summarize the highlight, methods list all methods associated with a given function name and is which magical word prints which method is used. Then if multiple methods are possible, for example, if I had to find a method with number and with integer, and if I could with an integer, it's both an integer and the number. So if multiple methods are possible, the most specific is picked. I guess that's like the way of summarizing the highlights of the lesson. So should we now have a 10 minutes break or what do you think, I don't know. How should we proceed? I think five minutes. Yeah, sounds good. Not too exact. In three minutes though, could you or I can also show the epidemic simulation example? Yeah. It takes more than three minutes then we'll move the break by one minute. Yeah, so you mean the epidemic then? Yeah. At the towards the end, right before the fruit bowl, extra exercise. Yeah. Just to have it in the repositories or I mean that we have done it. Oh yeah, that's... And it's kind of fun also, you can redefine how things are printed, how your own types are printed. Can you see the notebook now? I can see your untitled notebook. Oh. I mean, it is still, it is better if you type it out, I think. Okay, yeah. Just generally, it's not too long. Yeah. Yeah, so to come back to this like general like epidemic modeling storyline that we have in this lesson, before we have the... Oh, I would need to redefine the plans. I need to define a plan here. Yeah. Okay. I have it now. Okay, I can like copy that part and then paste. Yeah, so that's what was done before. Yeah, and they are both the plan type itself. Yeah. Okay, so that's what we had done before. And now if I create a plan, like I call it the uninfected and the time instant zero, it creates it and it's by default to print it this way. So it's simply the name of the type and then the list of the values for the attributes. So status is uninfected, it means uninfected. Infection time is zero, it's print zero. That's still like decency readable. So you could understand, but in generally when you have custom types, it gets pretty fast, like difficult to read. So what you want to do is that you want to define a like pretty printing, like a custom printing. And the way to do it is to overload the method show, which is this Julia like built in a function to like print the things in the Ripple in the terminal. And you want to overload that function and define a method with your specific type. So you want to tell Julia how the method show should behave when the input is an object of type plant. So let's do it. And since this method is defined inside the base module, which you can think it's kind of Julia's brain in a way Julia score. Then when I extended this method, I need to do it this way. So I'm extended it, I need to tell Julia explicitly. Then I'm extending the function defined in the base module in the base world. Then the first part is just like it starts with this. So this is like the input output stream where it's going to print it. And then we have our plant of type plant. So now I'm defining, I'm overloading this method and say that when the input is an object of type plant, you should print it as we want to print. And maybe we want to print it so that it's red if it's infected and white if it's uninfected. So if plant.status is infected, so now we check if the status of the plant is infected. Then we will print with red square. Can I find it? I guess it's just a field square. Field square. You can just copy paste the symbol, yeah. Okay, you write that. Yeah, yeah. Otherwise, so if it's not infected, we want to print like white square. By the way, if, so that's the function. And now, it's called x square. Okay, oh, okay, I'm color blind but I didn't know my color blindness is that bad. And now that I have defined the way to print it, plant and infected the zero. Now, when I press enter, it displays, it's displayed as a white square. If I create an infected plant, it will display the case of black square. By the way, if it happens like it happened to me that you didn't know how to type it, the symbol, you can just use the question mark and copy paste the symbol. And it will tell you how to type it. So it will tell you, this symbol can be typed by black medium square. And similar, the other one, you can question mark and copy paste the symbol that you want to get them. And that will tell you that this is white medium square. So that's a nice way to figure out how to print symbols, how to write symbols. So, okay, let's take a five minute break and come back at 38 past, more or less. And I guess then we'll move into the control flow section. Yeah, I guess it makes sense. All right, I don't know if it's all right. Should it go or should it wait? Yeah, I guess we can continue. Yeah, okay. So we are probably two minutes late. Yeah, okay, let's just, okay. So next topic we will talk about is how to apply functions to multiple elements. So how to broadcast functions to a collection of elements. So let's start with a simple example. Let's create a matrix. One, two, three, four, five, six, okay. Now we have this matrix. And suppose, well, taking the absolute value maybe doesn't make much sense because they're all positive but suppose that we wanted to take the absolute value element-wise of this matrix. If you come from a MATLAB background, you may be tempted to do just apps of A. Like if you have used MATLAB or Octave, that's how you would do it in MATLAB. However, it gives an error that you cannot apply the absolute value to a matrix because mathematically it doesn't make sense. The absolute value of a matrix is not defined generally. So the idea is that Julia is a little more conservative than MATLAB. MATLAB is very like soft in just generalizing functions to element-wise, Julia doesn't do it. So if you want to apply a function element-wise to a collection of elements, in general, you cannot do it by just slapping an array inside the function as you would do in MATLAB. And there are a few alternatives now to apply this function to all elements in a collection. The first one is a map, which is maybe familiar to you if you know some functional programming. The function map, what does it do? Well, let's ask Julia. It takes as input a function F, a collection and applies the function F to each element in the collection. So now if we return to our example before, map, ups, a, now it works. Now, well, it's not very interesting because the world positive, but let's take another example. Okay, this matrix, map, ups, a. Now it applies the absolute value to each element in the matrix. Yeah, you can also give multiple collections in which case map will apply to all elements in there. So, I mean, like pair-wise, so for example, if you want to sum to arrays, you have a, one, two, three, b, two, three, four. Now you could just do a plus b because addition of two vectors is defined, but if you want to use map, you can do map plus addition, a, b, and it will compute the element-wise sum of the elements. Okay, another option to do, to apply a function element-wise is to use broadcast, so the function broadcast. This behaves almost like map, but there are a couple of differences. So, yeah, in a nutshell, the syntax of broadcast is very similar to map. So the first input is a function, for example, ups and the second input is a collection. And remember, this was the A matrix. I now am broadcasting the absolute function to all elements in the array. Okay. Then the second example that we had before, we can do broadcast plus a, b, and we'll again compute the sum element-wise. However, there's a small difference between map and broadcast, and let's see this with an example. Suppose I do map plus one and one, two, three. Okay, thank you. Yeah, now let's do these two versions. Map plus one and one, two, three. And the second version, broadcast plus one, one, two, three. Okay. Now let's run both. Now they behave differences differently. And what is the difference? The difference is that map just applies element-wise the function, so this is just one element. This has three elements, so it just applies this map to this one and this one. So like, because it starts with the first element of the first collection, then it takes the first element of the second collection, but then the first collection is already over. So there are no other elements. And so it stops with this one plus one. What broadcast does in January, it tries to be smarter and tries to find like a common dimension and broadcast the function in that dimension. So now it says that, okay, this is one object, dimension one. This vector has a length, so it's dimension one times three in a way, like no hate and the length is three. So it says, okay, there's a matching dimension because this is one time one, this is one times three in a way. And so I can apply this through the whole length of the vector. Is it clear? So broadcast is in general more powerful than map. And it will try to find like a common dimension of the two collections if it's called with multiple inputs and broadcast in that dimension, the function. Indeed, so now if you probably noticed that the writing map plus one, one, two, three, broadcast plus one, one, two, three is pretty annoying. It's a lot of typing and we don't want to do it because we want to be short. So in Matlab, sorry, in Julia, there's this dot notation. So dot notation, which basically means that you can add a dot after a function or before if it's an operator to apply broad casting. What does it mean? It means that if we take the matrix example of before, if you want to broadcast the absolute value to all the elements, so now we want to do broadcast apps A. But we don't want to do all that typing. So what we can do is apps dot A. And this dot here after the function apps will broadcast the function to all elements in A. If it's a symbol operator, infix operator, like plus, minus power, then the dot goes before. So for example, if I want to repeat this example, so I want to do this broadcast plus one, one, two, three. So I want to add one to all elements of the vector. I can do one dot plus one, two, three. And this will broadcast to all elements in the vector. Yeah, and with this, we can get even more complicated expressions. For example, if the matrix A, we want to add one to all elements, then we want to take the absolute value. Maybe we want to, I don't know, divide each, well, it's defined, but we want to take the logarithm of that, for example, like this, and we can do it. So we can broadcast operations multiple times, adding a lot of dots. So first I say, hopefully a quick question. Yeah. I'm not sure exactly about the answers. So is there implicit vectorization? Does it do implicit vectorization if you're running broadcast or map? I'm not sure either. I think maybe broadcast does. I think I'm not super sure either. I'm not so sure because it is compiled. But if you want to be sure that it gets vectorized, then I guess use a for loop, or see which one is faster. But yeah, there's one important point here, though, that so in like in Julia or Matlab, or so sorry, in Python or Matlab or languages that are not compiled, you want to vectorize instead of having a for loop because running Python code is slow. So a Python for loop is slower than a vectorized version, like an unpy expression. That's not true in Julia. If you put a for loop in a function. Yeah, it will be fast. That's natively fast. That's probably faster than broadcast. So you want to use the dot syntax basically to make it more readable. Yeah. Actually, I said it's probably faster, but I would assume that it's inlining the broadcast operation. So if it's in a function, it's probably the same. I mean, there are also tricks to make for loops faster in general. So if you use all the cases in your sleeves, you can make for loops faster. So also actually in a software maintaining I'm a little like experimenting with this broadcast map and it seemed that generally for the operations that we're doing, which indeed like broadcasting like functions to make this is a map was faster. Sometimes it was like quite faster, like not 100% speed up, but still a significant speed of the map compared to the broadcast. Okay. Yeah, but that's just empirical based on like when it is doing more complicated things.