 Hello everyone, welcome to this session on type classes in Scala3 with Ayush. Thank you for joining this session and we are thankful for Ayush to be part of this conference and speak with us. Hey, so hi everybody and welcome to this talk. Really happy to be here and thankful to all of you who have joined. Before we begin, I really like to thank the organizers of Functional Conf on behalf of all of us. They have put together a really nice conference. There are so many FB languages and talks on so many FB languages, so it's really good. So in this talk, we are going to discuss type classes. Before we start something about me, my name is Ayush. I work as a software engineer at ING Bank in Netherlands. If you wish to know more about me and what we do at ING, specifically about Scala, then please do reach out to me on my mail address, on my Twitter handle and of course after this talk on the Hangout. So the purpose for this talk is for us to understand what are type classes and how we can write better code using type classes. You can break this talk into two parts. The first part is simple and easier. It's about introduction to type classes. We will encode type classes using Scala 3 syntax. So we would have a look at some fairly new concepts of Scala 3. And wherever possible, I will point out the difference between Scala 2 and Scala 3 syntax. The second part of this talk is going to be about automatic type class derivation. This is where we will look at some metaprogramming concepts. So we will try to take some easy examples because metaprogramming can be a bit challenging when people hear it for the first time. So let's get started. First things first, why do we need type classes? What benefit do they bring? Let's see an example and understand their importance. Suppose we have these two nice case classes in Scala. There is a case class called worker and there is a case class task. They both have some attributes, an employee ID, a name for worker, an ID and channel for task. So sometime later, we get a requirement that we must write a feature to show these classes or stringify instances of these classes. So we need to convert an instance of worker into a string and the requirement is that maybe we are logging that string somewhere in an application. We are also told that the default two string that the Scala gives for case classes is not enough. We need to write more domain specific data in the two string conversion. So we are looking for our own methods to convert instances of worker and task into a string. So how do we start? The first approach that everybody would take is to write methods that take those instances and convert them into a string. So what we do here is we basically have two different methods. They have the same name. It's called show. Each of them takes a worker and a task instance and then returns a nice string version for that particular instance. So we are using, we are saying worker has a corporate key and he has a first name and we are using some property. And for task, we are saying here is a unique identifier and it is a task channel. So we wrote two nice show methods and you would see that we are doing method overloading here because the name is same. The return type is same and the instances are different. So using method overloading, we are able to achieve what we want. We are able to stringify these two instances. However, there are some issues with this approach. Where should these methods go? The show methods. Ideally in FP, we would want them to be inside the class worker and the task. We want to couple them together. But what if these two classes are not in our source control? Maybe these classes are coming from a third party library and we don't have control over their source code. Then we can't add these methods in these classes for sure. So that's one problem with this approach. The other problem is that method overloading in general is better avoided because especially with Scala where you can do lifting of methods to functions and where you have default parameters. Method overloading can cause some issues. There is a nice stack or flow post where in detail people describe why method overloading is best avoided. So we look for alternatives to this problem of stringifying a worker and a task. We introduce a parent trait called as show. And this trait has a method show which returns a string. And then we basically extend our worker class and task class using the show trait. Once we extend and when we implement the show methods for these two classes. We can basically call worker dot show or task dot show and it works fine. But again, we have the same set of problem. We had to change the original worker class and the original task class to achieve this behavior of showing them. So to make worker and task showable, we had to change their source code, their original code. And that's something that is really not in our control sometimes. Sometimes we won't have access to the source code. We could want, we might be applying these methods on a class which is beyond our source control. So that's a problem that we are seeing repeatedly that we are trying to add behavior to a class which is not in our control. So we want to add a behavior but we don't want to change the original code. So how can we achieve this behavior? So there is a pattern in Scala called the type class pattern, which is the reason why we are in this talk, which has this exact purpose. So this feature that we are trying to achieve, this feature of building behaviors which are completely decoupled from the original data type. So we can build behaviors on a type without changing the type itself. This feature is known as ad hoc polymorphism. So it allows us to build functionalities which are decoupled from the underlying data type on which those functionalities operate. Now we can implement polymorphic functions which operate on types. We have no control over. So I could even show for a class which is maybe in standard Scala library like int or long. So that's the goal that we are trying to achieve using type class pattern. We are trying to add functionality without changing the original type because the original type might not be in our control. So now we have this definition of type class and the use of type class clear. Let's see how this pattern works. The first place to start this pattern is a parametric trade. So in this case, because we want to show something, we create a trade called showable. It takes one type parameter a and it has a method called show. The method show takes an instance of a and returns a string. So this method basically will take an instance of a and return a string and that's all it does. Now it should be pointed out at this point that showable of a by itself is not a new type. I mean, there is no type right now. Only once we specify what a is, we get a new type. So if we create a showable of integer, we get a new type. If we create a showable of dog, user, employee, whatever case class, we get a new type. So when we specify a, we get a new type and that is why this is also known as a type constructor. It creates new types and this type a is known as a type parameter. So what we are saying here is that in order to show a, in order to show a and a can be anything, we need to have a showable of a present. So let's see how we can give a showable of a. So we have the case class worker here again and this is how we give a showable of worker. We see a new keyword here. This is a keyword in Scala 3. It's called as given. So we are saying that here is an anonymous given for showable of worker with this implementation of the show method. So this is basically how we provide the compiler and implementation of showable of worker. This is how we give the compiler. And this, these keywords are also quite nice. It makes it easier to understand you are giving a showable of worker with this definition of show. Now all we have done so far is we have defined a type class called showable and we have provided a showable instance for worker class. Now the same could be achieved in Scala 2 also using the implicit case object annotations and the implicit case object keywords and extending the showable of worker. But as you can see the Scala 3 syntax is probably more concise. Okay, so now we have a showable type class and we have a showable of worker. How do we use it? So using type classes. So we defined a showable type class or showable worker. So we might have a method in our code, say log, which basically is going to log instances of type A in our system for, for just simplifications I have been a printer learn but in reality it could be logging to Kafka or to elastic or whatever. So what we are saying in this method is that in order to log a, you need a showable of a to be present. So log will use this showable instance to log an instance of type A. So the keyword here is using. So this method can only be called on a type A for which we have a showable of a instance present. So in Scala 2 you would have seen the same being achieved by passing the implicit parameter and in Scala 3 we are using the keyword using. Now, because we already have a showable of worker, we can call the log method on a worker instance and this will compile fine and run fine. However, if we try to call log on some other class, say task for which we do not have a showable defined yet, it will not compile. It will fail at compile time because it needs to use a showable of task and there is no showable of task given yet, so it won't work. So this is a compile time safety check that we get. Now, it's possible to provide a showable of task. Of course, we can do that irrespective of where this task class is present. It can be in a third party library. It can be in the Scala standard library. It can be anywhere, but we can still give a showable of task with this show implementation and then we can call the log method and now it will work fine. This is basically decoupling of a type and the functionality that we get on that type. Now let's look at another example and just get some more familiarity with this type class concept. We have a trait called equals, which takes one type parameter. And it defines a method called equals, which takes two instances of A, A and B, and then checks them for their equality. Now we can create an instance of equals of worker with this implementation and we are free to choose whatever implementation we want. So for us, we are saying that if the two workers have the same employee ID, then they are same. So now we are going to use, we already saw how we use the instance of equals in the previous log method. I have to pause for a minute because my Roomba just got on so one second. Yeah, I'm back. So my Roomba automatically runs at Friday 1130 and he was on time. Okay, so we were saying that we already saw in the previous methods that by the keyword using we could pass a given instance of a type class to the method. But what if we want to get a handle to this instance ourselves, we want to get a handle to this given instance ourselves, how we do it. So the keyword for that is summon. We can basically summon a given instance for a type class. So when we say someone equals for worker, the compiler is going to look for a given implementation of equals for worker. And if it finds one, it is going to compile fine and return that otherwise it will fail at combine time. And the summon keyword is somewhat similar to what implicitly function would do in the Scala tool. And once we have a handle to the summon instance we can call the equals method on it. Now again, if you try to call the summon method on a type class which is not available yet like the equals of task, then we get a compile time failure. Okay. There's one more feature to type classes in Scala three and which is extension methods. So there is a keyword called extension. And the syntax is like this. So we have the type class again, the equals type class, it has a method calls equals or takes two is represented by a and a B and checks for their equality. And we also have a section here called extension. The section has a parameter a and within that extension section, you can define multiple methods. In my case, I'm defining one method is equal to which, which takes another as input and this and then just cause the equals method for a and B. So what this syntax is basically saying is that for any type a for any type that is a. If we are able to find an equals implementation for a. Then, then the method is equal to can be added to that a. So, I will explain with an example. If we are able to define an equals for worker using some implementations for equals. Then basically the is equal to method is available on the worker class itself. And we didn't change the worker source code at all. It just feels natural. It just comes naturally that the is equal to method is defined on the worker class itself, and that is because of this extension keyword. So the extension keyword allows us to extend existing types existing classes with new methods by using a type class. So if there was a type class instance present for a for equals of a, then the method is equal to will be available on the type a. So that's a nice feeling that we get by using the extension methods on type classes. So, so far we did an introduction to type classes. We saw what type classes are we saw some new keywords given someone using extension. Now the second section of this talk is about type class derivation. It's about some metaprogramming concepts. Let's see an example of how and why we would need this type class derivation. We see a scholar three enum here. This is a new syntax for enums in scholar three. The enum is called customer types. And there are two sub types possible that can be an open customer and a close customer. It has a single property called session ID, which is of type string. The close customer has two properties, a customer ID of long and a name of type string. Now, let's assume we have a use case for our testing for our UI or for adult testing. This case is we want to produce values of customer type. We want to create instances of customer type randomly. We want to use these random instances of customer type to fill up some UI or to do automation testing or for some use case. So that's our use case. We want to produce values of a type. And in this case, the focus is on customer type, but it can be for anything we want to produce values of type. Going back to our type class pattern, we created a trade called producible with one type parameter A. And this producible type class has a method called produce, which produce values of type A. So this is how we represent the behavior of producing values of type A. And we want customer type to be producible for now. What we also do is we come up with some nice implementations of producible of string and producible of long. So we are telling the compiler, hey, here is how you can produce a random string. We are using the random library from the Scala standard library. And here is how you can produce a random long again by using the Scala standard random class. Now, so far we have given the compiler information on how to produce a string and a long. And if you look carefully at the customer type and the subtypes, they are eventually composed of strings and longs only. I mean, a customer type can be either open or closed. If it's open, it has just one string property. If it's closed, it has a long and a string property. So it would be very cool that just by using these, just by using these two givens of how to produce string and long. If the compiler could also produce open customer, close customer and customer types, that would be nice. And the Scala 3 compiler can do that for us. It is possible in Scala 3 standard library to have this feature. But the compiler will need some help from us. So our goal for the remaining talk is to produce a producible of customer type or to generate automatically generate a producible of customer type just by using these two producibles. And the logic behind that reasoning is that because a customer type and its subtypes are eventually composed of string and long, this should be possible. That's what you're saying. We are saying build up the bigger things from the smaller things. That should be something logical to do. And as I said, we will need to give the compiler some hints for this to work. So what are those hints? Before we discuss those hints, let's clear two concepts for us so that everybody's on the same page. What is a product type and what is a subtype in Scala? A product type first. So this is a term or this is a concept of product type and there is also a type in Scala for product. But the concept is that a product is something that when you need all the values for a type to be valid. So if I create a case class user and if I need to create an instance of user, I need to provide the ID and the name. If I miss any one of them, I cannot create a valid user class. So that is why user is a product type because I need all the elements of user to be present to have a valid user. The same goes for a two element tuple. If I have a two element tuple, I need both the elements to create this tuple. So that is product type when you need all the elements to create the original type. The subtype is the reverse. So in some types, we just need one of the elements to be present to create a valid type. So if you have a enum called customer type, which has two types open and close customers. If we can create open customer, then we have a valid customer type at hand. We don't need to have both open and closed customers to create a valid customer type. Just one of them is fine. The same goes for seal trades also. If we have any one of them, we get a valid seal trade as well. So some is basically when we need any one of the values to be present for a type to be valid. So this is an important concept because I will use the words product and some types a lot in the coming section of the talk. So now we go back to our original problem. We have some type, which is an enum of customer type, which has two subtypes. And we have a property or a behavior for producing types of value a. And we have a way to produce values of type string and long and using these two we want to produce or automatically produce values of type customer type also. So let's see how we can, let's see how we can define a flow to achieve what we require. Let's see a flow diagram to understand how we can solve this problem. So the problem we are trying to solve is create a reducible of customer type without having to define one ourselves. So the first thing we need to see is what is a customer type? What is it? It's a some type. It's an enum and it has two subtypes. It has an open customer type and a close customer type. Now the compiler should be able to figure out the names of these subtypes and the type of these elements also. Now for each element that we find, we have to apply the same concept again. For open customer, what elements does open customer have for close customer? What elements does close customer have? So open customer has a long element. The close customer has a long element and a string element. Now eventually the compiler will keep doing this recursively for all the types and then it should find reducible instances for these last element types. So eventually the compiler will have to find a producible of long, use that producible of long to produce a long value and using that long value create an open customer. Similarly, for a close customer, the compiler must be able to find a producible of string and a producible of long, produce a long and a string and create a close customer. And once it has either an open customer or a close customer, it can also create a customer type. So that's the kind of flow that we want to achieve that we check the elements of a type, we destructure them and then from that structure we find the producible instances for the end types. So if for some reason the compiler is not able to figure out throughout this recursion, the producible instance of any one type, it should fail with the compile time error saying okay, I was trying to derive a type class for you but I could not figure out the type class for this element so I must stop now. So this destructuring that we saw of types can be achieved in Scala 3 using a class or a type known as mirror. So this mirror trait or type is present in the Scala standard library and the mirror trait is defined in the Scala library, something like this. It has some properties which are properties about the type level information of a type. So mirrors will give us labels for a type, the elements that the type has, the labels of the element itself and so on and so forth. We also need to know what the type is, whether it's a sum or a product. So mirror is extended by two other classes. There is a product extending mirror and there is a sum extending mirror and a mirror is generated basically for every case class for every seed trait or for every enum in your Scala code. So the Scala compiler will generate this mirror automatically for you and it will extend the right class also depending on what the type is. If it's an enum, it will create a mirror of sum. If it's a clays class, it will create a mirror of product. So this information is what they're going to use to basically derive, automatically derive and give hints to the compiler. Just to add more clarity for a enum type, the mirror will be a sum type. The mirror type will be a customer type. The mirror element types will be open customer and close customer. These are the elements of a customer type. The label will be customer type and the labels for the subtype will be again open customer and close customer. And if we have a product like the case close customer, we will have the mirror type as the other type itself close customer. The element types will be long and string because customer ID is long name string. The mirror label itself is the close customer string and the element labels are customer ID and name. So what this class is giving us is a lot of type level information about our types. And you see that the product also has a method called from product, which can allow you to create the type also. So if you give the elements of the type, you will get the type also back. And this similar mirror product is also created for open customer as well. So it is created for basically all the case classes, enums, seal traits in our Scala code base. Okay, so now we know there's a mirror trait in Scala. We know there are product types and some types and we are trying to derive automatically a type class for any type a. Now this type a can be a product or a sum. So we first focus on the product type. So what we are trying to say is automatically generate a producible of a if a is a product type. Let's see how we can do that. So the method is called derived product just to name that we gave can be anything. It works on a type a. The type a must be product type. And the end result of this method is a producible of a. So if we are able to, if we call this method derived product for any product type a, it will give us a producible of a back automatically. Now, the first parameter is a proof that a is actually a product. The second parameter is interesting. It is a list of producible instances for all elements of a that is all elements that make up a. The implementation is pretty simple because this is a product. We need to form all the elements. We need to create all the elements to make an a because is a product. So we call the produce method on all these instances. And then we pass them to the from product method and we get a back. So this is the implementation for product to derive a producible of a for a product. For example, if we were looking at the close customer, which is a product type and has ID as long and name a string. The way to call derived product for a close customer would be like this. The first element is the proof that mirror of product of close customer. This is as I said given by this color compiler by itself. The second element is the list of instances of producible for all elements that make up a. And in our case is a close customer elements that make it up is a long and a string. So we will have to pass a producible of long and producible of string as a list. So, once we have those instances we can call produce on each of them. And then when we have each element so basically we'll create each element of type a here and then from all those individual elements will create a back again. So that's the simple logic that we apply to create a producible of a for a type a which is product. Now this was for product. The same concept has to be applied to a some type also. What if the type is what if the type is some type, and we need to create a producible of a from this a type. The parameter list is again a list of producibles for all the elements that make up a. And when we want to produce a. It's a some type so we know that we don't need all the elements to be created. We can just create any one value and we are done because for some we just need one value to be present. So we randomly pick up an element from this list. We type cast it to produceable of a because this will work and then we produce it. So we basically can pick any element from this list of reducibles call produce on it and then we get an a back and that is because a is a some type. So if you were calling derive some for our customer type enum which is a some type, then the instance list will be a producible of open customer and a producible of close customers. So that's how we basically call derive some if we have to call it for a some type. And the logic is pretty simple just produce any one element of this type a and we get the a back. And again not too complex. If you understand the concept of what a product and some is and what a mirror is. So now we have a way to derive a producible for a some type and a way to derive a producible for a product type. Let's see how we can use these two methods now. And the first thing that we have to do is you have to create that list of producible instances. We were passing that list in both the derive some and in the derived product method we needed that. So how do we create that list of producible instances. So here is how we do it. We write a method called someone all. It's an inline method. It is at compile time optimization. Someone all is a recursive method because it is being called within itself also. It takes a type a as a couple and then destructures it. So this erased value is a construct in the Scala three library to destruct a type basically basically it will destructure a into its sub types and then create the. The types here. Now once we have the destructure types available. We can then summon the producible for the head of that list for the head of the destructure type and then call the method again for the remaining list. So basically in this what we are doing is we are repeatedly applying someone inline a built-in function that searches the given for a type class. And then we append that to the list that we get by calling the method again. So by using this method we will be able to get a list of producibles for all type elements of type a. Now finally is the implementation of producible of a. So we are saying derive a producible of a. If we have the mirror of a available. The first call is the summon call itself. And then once we have the instances of producers for elements of a we can see whether a is a sum or a product. If it's a sum we call the sum method if it's a product we call the product method. And that's it that's pretty simple. So we see those methods together now. The first method is to derive all the instances of elements for producible of a. And the second method is the implementation of deriving a producible of a for a type a. Now, we have given the compiler four hints. We have told the compiler how to produce instances of producible of a if it is of type product. We have told the compiler how to create a producible instance if a is some. We have told the compiler how to summon all the producible instances for the sub types and parts of a. And we have the derived implementation for the type a using all the three hints. So using these four hints now. And just by giving the producible for string and long. We are able to automatically derive a producible of customer type. We didn't have to write a given for the customer type or for the open customer or the close customer case classes also. Just by using the four hints that we gave, and by using these two producibles. The compiler has generated automatically a reasonable of customer type for us. And we see that in use we are calling produce here and the generating an open customer the next time we call it it is generating a close customer. And there are some random values inside the properties themselves. So this is what we wanted to achieve right that without specifying a producible for the bigger type, and just by using the basic types, we get the type class instance so this is type class derivation and we get type class instances for free. So, whatever we did so far, we looked at the example of our enum the customer type, but in the hints that we gave to compiler. We only focused on the producible type class we didn't focus on any specific enum or any specific case class. And what that means is, we can generate a producible a for any type of abstract data type that is a. Just the two enums and the types we saw. Example, suppose we have a enum which is called a street, it takes two type parameters x and y. We have a case node and a case branch, a pretty commentary with two sub branches left and right. And at the leaf, there is a node or a leaf whatever you want to call it, which has two values of type x and y. Now, without doing anything else, we can say, okay, can the compiler please derive for us a producible of three of x and y. And the only thing that we will make sure is that x is producible and why is producible. That is there is a producible instance of x and a producible instance of y. If that is there, then we can derive a producible instance of three of x and y. And we do that here, we, we summon a producible of three, which consists of strings and longs. And because we had given producible of strings and longs this someone call works just fine, we are able to get this instance and then we are able to call the produce method and we are able to generate random three instances of branch and node. The important thing that I want to stress again is that these producible instances need not be given by us, the compiler will automatically derive us for them. So here I'm asking to generate a type class producible for a tree where x is long and why is customer type, and I didn't give any given for customer type but the compiler will derive it for me. And using that it will derive this producible also the super producible. And then I can just call produce on it and I will get random three values again with the right types. So this is the ability to basically let the compiler take care of some boring and ball of late stuff and we just need to define some hints of on how to create producible for someone product types, and the rest is done by the compiler. So that's what I said it's a nice way to let the compiler take care of boring and ball of late stuff. The only thing is that because all those definitions are in line, they are done at compile time all the check is done at compile time. You need to make sure that the compilation is not getting too heavy in searching for all those individual type classes. All we need to be careful about, but otherwise it will give you a very nice way of deriving type class instances, just by using some basic principles of building from the basic blocks so you give you define instances for the basic types and let the compiler derive the bigger types for you automatically. And this was automatic type class derivation this is a new feature in Scala three in Scala two, there were some libraries that would help you do this, and they used macros, but in Scala three this can be done without any use of macros. So this is pretty helpful for especially for those who are working on libraries of their own. And that's what one wanted to see and that's all about type classes and type class derivation. Are there any questions? Are you surprised? Are you enchanted? Or is it like what just happened? So with that, I think if there are no other questions. Thank you so much for this session. It was a great session and something new about Scala. No idea about Scala but it was nice to see what changes are coming up and definitely we'll explore it further.