scale.bythebay.io: Andrew Kuhnhausen, Type Classes for the Rest of Us
Recording: scale.bythebay.io: Andrew Kuhnhausen, Type Classes for the Rest of Us
you Wow it's like a lot more people here than when I was staring this computer just a second ago where'd you all come from ok let's let's do this alright hi I'm Andrew I work at a company called Domino data lab we build a platform for data science teams our platform or our stack is mostly Scala a lot of object oriented Scala where I touched the code base there's a bit more functional stuff hidden in there so I guess before I begin this is the functional track so how many of you are familiar with type classes already how many have written their own type classes cool all right so like maybe 33% of you have written your own type classes so I apologize to you this might be boring I hope not but today I'm gonna walk through what I feel like is a fairly canonical use case for type classes go through where you want to use them where you probably don't want to use them and where you might have to use them but before I begin just wanted to have special thanks to Travis brown the his his blog post type classes and generic derivation provided basically the narrative outline for this talk and then Rob Norris who is just a profoundly wonderful like teacher and speaker and so I hope to give this talk in in that vein and that spirit and I gave this talk earlier at a Scala meetup where Jakob pointed out a flaw and hopefully I'd fixed that so let's go over the goals here so this pattern is is well known in functional programming Scala community comes from high school basically is there to address a lot of the complexities around generics and replace things like like inheritance and so the main point here is we want to make the compiler do some work for us so let's let the Scala compiler work for us hopefully also you'll gain some confidence and in seeing this pattern how to apply it where to apply it and and then see some applications that maybe you might not be filming familiar with with type classes so the canonical use case here is I'm going to take is serialization or D serialization and so I'm going to start with some very simple datatypes here there's a person address account with strings in sand doubles wrapping them and we want to have an API for D serialization in this case there's just a simple text formats is it also call the sukkah parser but it's a comma separated value where if we have you know person Fred and their age we should be able to deserialize it okay as a person but we want to have some constraints here the main constraints are that we should never throw non fatal exceptions so things like out of memory errors are perfectly fine but things like a number format exception wouldn't be we also should never get a runtime failure so we should basically have a form of error in the form of a value and and then we want to remove as much boilerplate as possible as well so those are our constraints so let's let's try this out with just a naive first go so we have a are desirous or de-serialize I and de-serialize err which is parameterized on some generic a we use the apply method which gives us some nice and tactic sugar and and basically string dot split and apply these these things to to those types that we had earlier account in address and it's actually a really nice API we can you know create for every every type we want we can create a deserialize ER and apply it to this string and we get a useful type back not just any for example you know we could account or address which is great but there are three big problems here we allow non fatal errors to be thrown so if we were to try to deserialize something with a trailing comma we might get a match error maybe a number format exception so that doesn't meet our our first constraint we also have to keep track of each Roy road to see réaliser instance for every type we want there might be a whole bunch of these till we get to our serious business deserialize err which is where we really get to the heart of our our logic and then we have to write up a lot of boilerplate and that's also you know closely related to to item number two so let's let's do something a little bit better let's take the first problem and apply option to it so now we simply you know represent failure as an option type so it either succeeds if it can or returns none if it if it doesn't work and it allows us to return that you know if failures of value of none right and now it's impossible to make our DC réaliser crash with a recoverable exception so if the inputs invalid we return to none but unfortunately we still need to keep track of every single deserialize err so one way that a lot of you know java friendly libraries or ways we've done this in the past before we had a more powerful compiler like in scala we might use reflection and it's actually really easy to write a reflection thing this is from from travis you basically match on on what's incoming and and then just instantiate as instance of and it gets rid of all the boilerplate it's really nice we can just throw anything at it but it has a big problem this is really annoying we can totally write this line where we deserialize a foo without actually telling how to dis you realize that so we're not kidding and this is gonna happen at runtime we're just gonna have a none we have no idea because a compile time we didn't know that we didn't have a deserialize err for that and we have a whole bunch of other things around run time issues one of the libraries we use a domino data lab for our stuff is called so lat and along with salat which is a runtime parse or serialization library we have a wiki article called surviving salat and I'm sure that many of us have these sorts of articles but it's an article basically dealing detailing all this weird behavior and what to what to guard against very defensively in your code so let's go back to our safer deserialize okay let's try to get back to a compile time approach so we want to adapt it so we don't have to keep track of everything of each of those deserialize errs and we want to change it so that we don't to make sure that if we just you know create some type and then try to dis serialize that type without actually creating a deserialize ER that that fails and so we can do this by we give our companion object of say for DC to DC or Eliezer this apply method where we pass in a DC réaliser now we can write this fairly nice syntax and compile okay but not have that compile that's that's gonna fail so there's a couple of problems we can define multiple deserialize errs for the same type that can be that could be a problem also you know because we want to deserialize your first single format and then we know that we want this so so another problem here is that it's really annoying to pass pass around these deserialize errs so we know we want one deserialize err for a specific format so we really have this one-to-one mapping some like function from a to a deserialize err of a so what if we had some like we codified this as a method so if we did this at runtime we could check like okay if there's some instance of account then we return this account you serialize ER and we just keep adding on here that would be neat that I mean that's also known as like a map so what if we did have some like magic map where we could pass in a mapper where you know we found the instance of the type you know a to the deserialize ER and then we got it and then we threw some exception if we couldn't find it that'd be pretty nice also we'd be able to if we made this some global mapper we could add D serializers on the fly for when you know some other feature maintainer of our code or user of our library needs to add a DC réaliser that'd be really nice we just put it in there and and we can pull it out so what if we could do that at compile time that would be really neat and have the compiler do all of this mapping for us so this is our safer deserialize err this is the one that returns the option that we have to pass around all of the things let's make a few just minor modifications let's move those deserialize errs into the companion object and then let's sprinkle some implicit keywords in there so the only difference here implicit not implicit implicit not implicit okay it's the only difference and now we get the usage that we want we get the the deserialize err like that and we can't compile when we haven't defined a serializer or a deserialize err and we get it a compiled and now we've discovered type classes so what our type class is there's like a lot of like writings about type classes this is my distillation of it it allows us to associations with types without modifying the definition of those types Scala implements type classes via implicit values implicit arguments and then a because other languages have actual first-class support for house r4 for type classes like like Haskell and Idris we use the implicit and kind of mimic what type classes in Scala so this is the structure of a type classes this is sort of your like recipe so safe to serialize is the type class it's just this trait without any sort of of any implementation whatsoever it's just a like an interface or a contract and then we say that when we implement an instance of the type class for a type so we come up with this this type argyll barbel and we create a an instance of this of this type and we put on a and implicit now we basically have your instance of the type class of safe to serialize err it doesn't have to be a vowel it can be some object or class or whatever just some so long as you have this implicit argument that allows you to resolve an instance of this type class of a let's see so the and then what's also nice with type classes is that like we should like we showed before if you create some type and then you try to deserialize it but you don't have a deserialize err for that type you get a compile time error and all you need to do is provide an instance of that type class for that type and then it compiles one of the really neat things from Scala that you don't get in Haskell is that in hospital type classes are global we actually get some some kind of hairy and complicated scoping but but we get to have multiple instances of a type class for a given type so in this case I've actually made a test instance of a deserialize ER for a dress that's special just for for this scope so our safer to serialize a dress will correctly do the address using the safer to serialize ER if i use the test deserialize ER i get back none because that's my my implementation and test so so that's nice further one of the neat parts in scala as well as you can basically get this inline syntax as well that gives you more of a like a know quality where this as method can be defined so you can take any type class and you sort of property and apply it using the syntax and now get in line syntax for for your deserialize ER so type classes are really great but as you saw there's a lot of boilerplate one of the libraries out there that is really nice is simulacrum which gives you more of a first-class support for type classes you can use this annotation and it basically generates all of that extra code and operations and things so that you can you know this compiles as well using simulacrum other things with type classes to remember type classes generally you want them to be lawful and so that when other people create instances of your of your type class that they know that that their instance works so there's a set of libraries called discipline and Scala check that allow you to enforce or or give the the user of your library the ability to test their instance with the type class to make sure that it follows your laws and you can compose them and share share these lots it's it's really nice so that's another added benefit so so here's a non-exhaustive list of places where type classes make sense can be used for method overloading to replace you know generic interfaces for generating data we talked about serialization anytime you see those big match blocks with tons of types that look like that mapping function where you're basically dynamically dispatching to a similar behavior that's another case where you might be able to do that and so as Rob Norris said in my experience I've never regretted replacing a superclass with a type class I on the other hand have regretted that so I have an example of of exactly where I didn't that didn't go so well but first let's let's talk about operator overloading this is this is sort of how you might do sub classing or wrapping classes if you wanted to add functionality to something like string or int you might create this wrapper class and so if you're like doing business and you're you want to do business on a string or you want to do business on an INT you write these these classes and then you pass in some superclass that you can call that that on and then if you need to override it again and do like serious business on us on a string then you can you know that's this this is all fairly familiar from the O paradigm you can flip that and do that with type classes so what type classes you create your trait again your instances for these types and then now and this like nice little apply syntax thing makes it makes the syntax nice in Scala so and you can still have your serious business you know in a different area than your normal business and so you know using the inheritance method of overloading you can call it that way or you can use this nice type typed you know inference from business can see that hello is a string so it pulls out the string instance of the business type class and then prints that out or if you need to do real serious business you can you can do that and it knows that this double it can find the double version of the business and print that out that all happens at compile time again compile times really good now let's take a look at where type classes don't fit so well so instances so like inheritance for example if you know all of the if you control all of the objects or the the types that you're using and it's a small discrete amount type classes are a little too heavy-handed I think you should just use regular inheritance if you can't express a law in your type class is probably not a type class again this is this is just another example of like if you had an interface it makes a lot more sense than sort of this because you only have your domain objects its I have 18 oh okay all right I guess because I started late all right okay so we're totally done okay yeah I'll put these slides online unfortunately started like five minutes late setting it to finish okay [Applause]