Devreal

funconf 2013: James Earl Douglas, Hands-On Category Theory

funconf 2013: James Earl Douglas, Hands-On Category Theory

Recording: funconf 2013: James Earl Douglas, Hands-On Category Theory

so this is actually really cool to see everybody here this is going to be a slightly different kind of presentation very experimental so i hope you'll bear with me this is the singular slide from this presentation and i'll spend the rest of the time sitting here uh banging away at code so if you can't see me uh i'm still here i'll just listen to my voice and watch the terminal so this is going to be hands-on category theory uh that's maybe a little bit lofty of a goal uh we're actually going to focus very specifically on uh how to do functional validation so you may be familiar with validation from scala z or other types of validation frameworks where you can take values such as parse json values or something and then try to pass them into your your server side code for example and then do things with them and you might have invalid or valid conditions that come out of that so we're going to sort of dive into how we can build up a library that lets us do all that from scratch but before we get there if anyone wants to follow along or write the code as i write the code you can go to this github project scala cats under my github account and you'll see basically the exact same tutorial i'm about to walk through but it's all spelled out and so quickly before i jump into the terminal uh just to get a better idea of where we're going uh we want to be able to write code that looks like this so we want to be able to write a parser that can return a possibly valid or invalid value and then write a pure function that only operates on pure values and knows nothing about validation but be able to map that function onto parsed values and apply it to additional parse values and so to get there we're going to look at things like functors semigroups and applicative functors we're also going to cover a few feel free to stop me at any point there's going to be a lot of interesting scala features that we sort of touch on but i won't spend much time covering in depth but things like algebraic data types type lambdas higher kind of types implicit conversions type classes and so forth cool so let's put this to the side yeah question so the fact that you're using uh some some special symbols for for division and multiplication oh uh so the so the question is about these symbols these are this is not meant to mean multiplication or division these are just uh operators that have kind of a funny syntax and the syntax is inspired by haskell and some other places but uh we'll we'll see later that this actually means map and this means apply from functors and applicatives all right so i'll just put this to the side we might come back to it if i need to copy paste any code and i want to make sure the font is going to work for um us let's actually remove that start from scratch okay so is that visible at all no okay let's uh try changing our color scheme here something a little bit more readable um anyone know osx and why that didn't work is there an apply button oh gotcha okay how's that a little better cool all right so let's start at the start imagine that we want to build something like a web service where we're trying to bridge two worlds between our our clients who are uh living across some http channel and our server where we have full control over all the code we write and in an ideal world we have very nice clean pure referentially transparent server code but our our web world is uh not so constrained and so since our clients are going to be accessing our services over http there could be all kinds of problems with their method invocations they could send bad data or invalid data or malicious data or even no data and so these are all cases that we have to handle but we want to figure out some way to cleanly bridge the gap between these two worlds so that this the process that we end up taking for validation doesn't sort of leak into the server code that doesn't care about validation so let's start with a very simple imaginary method that we want to expose as some kind of a service to some unsafe world so we'll write an absolute value function so absolute value has the following type so absolute value is a function that takes in it and returns an it and for implementation we'll just steal it from scala okay and hopefully i can have at this resolution a couple of windows side by side we'll see if it works i may want to do them vertically actually let's try this oops let's see how that works for us um so in parallel i'm going to be editing this text file scala cats dot scala and then interpreting it in the scallop replica so there's our file and we'll run scala and so i will periodically be loading this file uh to make sure it it compiles into the to also interact with it cool so there's our abs function so we can test it out with a negative number uh and so that seems to be working um so now let's write so imagine that we want to expose this to a world where data is potentially invalid so let's write some sort of parser that can take data from that world and then return something that's a little bit safer to use so we're going to write a parser so this takes a string and it needs to return something that can be either valid or invalid so we'll use scala's either type for this and either if you're not familiar with either either is an abstract class that has exactly two implementations called left and right and there's really nothing special or magical about them they're they're just boxes that hold a single value uh and are distinguishable uh by by type it's either a left or it's a right uh and that's pretty much where uh the the specialization ends by convention when we're using either to represent things that can fail or things that can be valid or invalid we tend to use left to wrap invalid values and write to wrap valid values but again that's just convention and it really doesn't matter but for this demonstration we will use that convention so on the left if if our input fails to parse as an integer we'll return some string containing a message about the failure otherwise we'll return the successfully parsed integer and we'll do this with a simple try catch so we'll say x.2 inch um and so this is returning an integer but up here we see our function returns an either so we have to wrap this uh in a write and x.2 and could potentially throw an exception if for example we try to call 2int on a string that is not parsable as an integer it will throw some kind of number format exception so let's just catch any kind of exception and say uh if any exception is thrown or any throwable is thrown we'll just say well we don't know what to do thanks all right so let's make sure my syntax is correct so now we've got two functions abs and a to i uh and so i can call a2i like this pass it a string and since the string 1 0 parses as the integer 10 i get back a write containing that 10. if i were to pass it something that is not parsable we get back a left containing the message 10 is not an integer or at least as far as x.2 it understands so now we have a way to take input from the user where that's represented by a direct call to this a to i and get some sort of some sort of output that might be valid or might be invalid but we don't have to worry about any exceptions being thrown or anything like that we also have a service function which is a pure function into int that we want to put together so we could do this with a pattern match we could call a to i and get back in either like we did here and then pattern match on that and if it's a write pass it to the abs function and return the result or if it's a left just return the fact that it failed um that's a little bit more uh verbose than we'd like we what we're really after is a single line way to apply one to the other so what we actually need is a functor and so you're probably whether or not you know you're probably familiar with what functors are if you've ever done something like this so if we have lists of some integers and you call the map function on that list and you pass it some function that takes an integer and returns something so we'll just say x plus one the the instance will take that function x 1 and apply it to every element in the list and then return a new list containing those elements and even though scala doesn't have a trait called functor it's basically the same shape and so we're just going to make it more explicitly declared here so let's start with a trait called functor and it's got a couple type variables which i'll come back to in just a minute so a functor is anything that defines the following method for our purposes so it's a map function that takes uh some function from a to b and returns a new f of b so what's going on here we have a trait that's abstracted over two types a is pretty simple a is just some arbitrary type f has this syntax which means that f is some unary type constructor it's a type that itself needs another type to emit a type so for example uh basic scala collections like list or option or set or whatever are each things are each type constructors that take a single type so i couldn't uh really have a list of something like this because you know if we want our if we want to forget about type erasure and java 4 compatibility there is no such thing as a list it's always a list of something so we would say a list of integers is and it'd be nice if i got my syntax correct so if i say val x is a list of integers and then i give it some value right so in this case list is corresponds to our f because it is a type constructor that takes a single type variable uh so then when we define our map function we're basically saying that there's some type constructor list for example that contains things and in this case it will be a thing of type a that we can pass a function from a to b and then our output will be whatever that type constructor was but with the b type so in this example here we have a list of integers and so you can imagine f is list and a is int and we're passing in a function which is just int to int and so our output is list event where b is int um and so as i said we want to uh we're playing with the either data type here so we have our parser function which returns an ether so what we need to do is build uh an either functor and if you're following along on the github page you'll see some diagrams that show sort of how you can imagine the categories of types and the categories of lifted types and then what map is doing when we're transforming a function from one category to the other um it's a little bit too hard to describe with words uh so i'll just skip it and allow you to read that later but let's write our ether functor so i want to just to make this easy to use i'll put it in an implicit conversion so that we can automatically uh add this map function onto any instance of either okay so this is an implicit function that takes any either and the reason i'm using these type variables is we will want to use the a later for our a to b function but z is sort of our error case it's our invalid case so it's not going to change and you'll see you'll see that demonstrated shortly so our implicit conversion takes an either and it returns a new functor of type a and then here it starts to get actually a little bit tricky so as i said we need a functor has these two type variables one of them is a type constructor which itself needs a single type either is a type constructor which needs two types so it doesn't really fit the shape of the problem that we're trying to solve but as i said we with validation our error type doesn't really change over time but our success type might change just like with list.map you could get a new list of strings or a new list of something else that wasn't what you started with so for example we had this function before we could just say x.2 string and a result so we went from list event to list of string and so we'll do something similar with either we know z will never change so what we really want is to constrain sorry you're saying that the failure does not change but if you if you change the types the kind of exception that you're gonna get is gonna be different or you're abstracting we're abstracting failure as a left of whatever we started with so in this case failure is always a left of string and we have some well the exception could trigger the failure but the way we're representing it from our parser is would be a left of string it doesn't change right as long as it once we've got a left we have a failure so there's really nothing we can do further um so we'll always stay a left of string we'll get there yeah yeah so does the failure change in any of the consecutive the failure uh the failure will not change type but you'll see when we get to applicatives that it will change its value and we'll see how that works in just a minute okay so what we really want to do is constrain z so say our f type is always in either of z or something where something can change uh scala doesn't allow this syntax so to get the same kind of thing we need to use a type lambda and the way we do that is declare a new type so we will call our new type either z and give it an arbitrary type variable so in either z is an either of z oops or b and then the way we reference this thing is with this hash z so this is a little bit noisy but what it means is that our f of something is really an either of z or something whatever something might be so a functor defines map so we'll do that so remember map takes a function from a to b and returns whatever uh whatever type we're lifting into a functor so in our case that's in either of z or b so we're basically adding a function map that given an either z a and a function a to b will give us an either z b so if we have a left so the question is can we move the type alias to clean up the code a little bit i think we could put it in an object somewhere um but i'm just declaring it in line here for for demonstration all right so uh we know that our our x isn't either so it has one of two types it's either um a left or right so let's do a pattern match so if we're starting with a valid value a right um then we have some value actually let me make this more clear we have some a to which we can apply the function f so we'll return that in a new write on the other hand if we're starting with an invalid value there's really nothing we can do with it we have a z for some type z and we have a function a to b so we can't apply that function to the z really the only thing we can do is pass that thing directly through so we'll just return a new left of the same thing all right i could write an at sign right i could do something like that same idea let's make sure that this compiles oh yeah okay so now we've got an either functor uh which is a way to add a map function to any arbitrary either question the question is uh for this type lambda we're we're creating an either z um but the thing we're returning is not in either z well it sort of is but not by name it's in either of zb really that's just a side effect of how we've chosen to define our functor trait um so our functor is it could return a functor it could go from functor to functor in our case we want to go back down to the either type uh because that's where we started and that's kind of where we want to end up and then we'll since we have our conversion we can lift that back up to the either functor if we need to to apply map again so that's kind of a non-answer really it's up this is just the one way to implement it scala z does it slightly different where their functor type declares um something like this i think rather than in the trait it's just two ways of skinning a cat define equivalence so i'm not sure if the type system would consider them uh equivalent for like a dot equals or an equals equals uh but semantically they're equivalent i mean it's just a type alias so i think like in scala if you created type alias foo equals string uh scala considers a foo to be a string but i don't know if it considers a string to be a foo i'm not quite familiar with that you can't imagine you can't instantiate the alias okay so maybe i'm not sure but let's press forward there's you can see this thing in action so what we want is an ether that we can create and turn into a functor and then lift up our abs function and then apply the two so let's parse an integer actually let's make it negative so we're actually seeing abs work so there's our ether and if we call map now we will invoke this either functor so that we're pimping the map function onto that ether and then we can pass it so we have a an either of string int so we have an either of string int so our a is an int so we can call map and pass a function from int to whatever we want so we'll pass abs and so the result is to lift this function f of a to b into the either functor so that it can be applied to the value that's inside or not if we started from an unparsable value like this and then we tried to map our absolute value function we would just end up with the same error message and so it's kind of a no op and that's because we matched on this second case um and then one further thing before we move on to applicatives is uh it's sometimes useful and it will be especially so more toward the end of this talk um to be able to do this in reverse so we're starting with an argument uh that is uh an ether and we want to lift it into an either functor so that we can apply uh a function so that we can lift a function and then apply that argument uh to that function uh it's sometimes nice to be able to do that in reverse so we start with the function and we implicitly lift that so that it's ready for uh an argument uh that is contained in an either and then they can be applied so let's write that code i'm sorry so if you go up here at the level of validation yes yes the goal is to do validation but to do the goal is to do validation but to keep the validation logic completely separate from our server logic so i haven't modified the absolute value function to understand validation it just takes an integer it doesn't take in either string end that's really the key and then later we'll be able to compose these things together but still without worrying about validation on the server side so we want to be able to lift any function such as absolute value into something that has this funny looking method uh so all this is doing is the what we did below but we're starting with the function and we're lifting that up before we have the argument to apply let's make sure i type that right cool so uh we had this construct before now we could do just syntactically the reverse of that so i can say absolute value and then map that function onto a parsed value and we get the same result so any questions before we move on to applicatives yeah so when we uh specify that factor we set f and then underscore right in the bracket we just specify there the capitalized yeah yeah right here can we specify instead of the underscore underscore could you specify the a there because the time i see so the question is can we say f of a right here uh no um to say f a would would mean that we're always using an either string int but it's possible that the the result of mapping could change the type of that either this is the type constructor to use so f of underscore is not quite yet a type it's like list yeah but the underscore means any type right the underscore this is just the syntax for saying we want to use the type we want to use f which is a type constructor of a single type variable this is just scholar syntax for saying that so this is just a way to tell scala that f is a type variable but by the way it's a higher kind of type where it takes one type variable uh it could be i don't know templates okay so i'm going to comment some of this out since we're just in the reply don't have nice name spaces and then i'm going to quit the repel and start it again and that's just because i want to clear out the implicit conversions from memory okay so let's look at a slightly more complicated function uh this will be an addition function between two integers so this is a function that takes two ins and returns another int so it should be straightforward okay so the function add two takes an x and a y and it returns the result of x plus y uh if so if we want to follow the same kind of process where x and y are both inputs provided by a client and they they need to be validated they could be invalid values uh this starts to get much more tricky because uh we would now call a to i on both of those inputs and come out with two ethers so we would have x which is some parsed value which might be a right or left and y which is another parse value which also might be a right or a left and so we can't call map uh on one of those uh and apply add two because we lose track of the the second one so a functor won't quite get us where we need to go we need an applicative functor um so an applicative functor builds on a functor by adding a new function called app so let's actually i'm going to go up above here so here's our functor trait uh an applicative uh it looks almost the same and it extends functor and so to build on your question before this is how we tell scala that our f of underscore is this f of underscore so in this case we don't have to pass the underscore it's just an implementation detail of the um the semantics of the syntax could you really oh okay i don't think i don't think you could do this i think that would not compile because it wants a concrete type and so our concrete type is this type constructor i could be wrong i also have no idea what i'm doing so cool so let's build on our functor we already have map because we're extending functor so let's add app and by the way the names of these functions are largely uh historical in nature so app looks almost the same as map with a key difference rather than taking a function a to b it takes a function that's already been lifted into the functor of a to b and still returns an f of b the same kind of type so map and app return the same thing and they start from the same place but the function the argument that they take is either a function in the category of types or a function in uh the lifted category of ethers in this case and we'll see whoops i need a b there we'll see what that looks like in just a sec so there's actually quite a lot of code for me to type here and i'm running low on time so i'm going to cheat and copy and paste it and then i'll go over what i just did oh so actually before i get there we want to uh since we're we're now parsing two input values uh and then potentially producing a result uh there could be potentially two error messages right if both x and y failed to parse we would want to know about both of those failures and so think if you're building a web service and maybe you're interacting with jason or you're taking a form submission from a user you want to be able to validate that whole thing all at once and then know everything about it that failed and not just know a binary state like either it failed or it's good to go so we're going to change our either type a little bit to be either a list of all of the error messages all the things that went wrong or the successful final result and so this doesn't change much this is just we we insert a list there but before we implement our either applicative we also would like to keep this thing nice and generic there's there's nothing in uh inherent in applicatives that should bind us to scala's list api uh so we'd like to generalize the notion of things that can be appended uh so a list is something that can be appended i can take one list and concatenate it with another list with the plus plus operator uh but that's really a general property called semigroup so semigroup is any type that that can be appended to itself and return the same type so a list appended to another list gives you a list for example so if we take uh an a we should return a new a and so let's make a list semigroup so that we can use this append syntax generically instead of list dot plus plus or something very specific so we want to be able to convert a list of a's into a semi group of a list of a's and so that way we can implement append which takes a new let's call this x a new list of a's and returns the concatenation of the two make sure that compiles looks good so what does that look like if we have a list of one two three with scala we could say plus plus list four five six but with this implicit conversion we can also say now append and append will uh take our lists convert it into a semi group of lists which now has an append function that takes another list and returns a list so really append is kind of like a synonym for plus plus in the list case and then you can imagine other things that would be appendable maps or uh arrays or you know any kind of collections uh or even a string might be an appendable sequence of characters but for our purpose we just need lists of strings for our error messages okay so now i think we can yeah now we can build our either applicative and this one's pretty long so let me just copy here save time and then i'll go over what we're doing so if you remember let me go to the top here an applicative is a functor with this extra function app so if we look at our either applicative we define map and that's actually the exact same implementation as we had before but i've commented it out so there's no code sharing but but explicitly we have the same kind of construct we have the same type lambda that we saw before so we need to constrain that z type which is now going to be list of string instead of string as it was before and we're lifting up an ether where it has some error type which in our case is list of string and some data type that we'll be manipulating we also need to add this implicit here we don't want our parser to worry about semi groups directly we just want to deal with things that can be turned into semi groups so our parser returns either either list string or int but for us to be able to use a list of string as a semi group we have to also pass in an implicit conversion for whatever that z type is into a semi group and so that way later if if i uh i'm working with a z down here in this code i can convert it into assembly group and call append okay so i'll skip over map because that has the exact same implementation we had before and let's look at app so what have we got here we have uh a parsed value that the user passed in so that might be a right or left and then we have some function that has been lifted into an either so this could be a left of a list of error messages or a write of a function that we want to apply to the input if it's valid so we actually have four conditions we need to worry about x can be left or right f can be left to right and we need to have the take care of all four combinations so let's first look at x x is our input value that was returned by a parser so if if our parser gave us an error so it gave us a left with a list of error messages um then we know our output is also going to be an error because our our data is either valid or invalid and once it's invalid it will always be invalid so we look at our the function that we've lifted which itself could be an invalid or a valid a left or a right and if it is also a left we need to take the error messages it knows about uh and append those to the error messages that the parse value knows about so that we're not losing any data so we if that's the case we return a new left with all of those error messages appended together and so this is where our semi group implicit conversion gets utilized if we were explicitly using lists throughout this and maybe i should have done that just for simplicity for this presentation but we could have just said l plus plus l2 if these were lists but as it is l is just any generic type that can be converted to a semi group if on the other hand the function that we want to apply is uh some valid it's a write of the function that means we have a right of a function a to b and we have a left of a list of messages there's really nothing we can do we can't combine these in any way so we just pass our x value which is the left of l directly through uh there's there's nothing we can do so we kind of drop the function on the floor so that's if our input value the thing that was parsed is invalid if on the other hand it's valid meaning it's a write we we might be able to use it um so f is the function that has been lifted f could be an invalid value it could be a left uh in which case we're in the same situation we have uh but with the types reversed we have a an int in our case and a function sorry and a left of a list of messages so there's nothing we can do with that int that's meaningful so we just pass through that same list of error messages finally if we have an input value that's good and a lifted function that's also good we can apply it which is what we really want so r2 is our function from a to b it's this guy and so we we take that function and we apply it to the valid input value and then we we evaluate that and then return it all in a right so that's kind of a mouthful and uh a bit of a fire hose but let's see if we can put that to some use um so first let's look at oh we already looked at the list semi group so let's look at the either applicative so we have our add two function which takes two numbers and adds them and we have our parser which takes a string and returns either the parse value or an error message so how can we put all this together to parse two input values and then send those off to our add to function and then maybe get a valid result well it looks a little bit like this we parse a value and then we map it onto the add to function and what does that give us uh that gives us this kind of complicated looking thing it's an either where it might be the list of error messages or it might be a function into int well what the heck is this function that means that if this thing parsed correctly it partially applied add 2 with the value 22 and the result of that would be another function that's waiting for y so that it can add 22 to y and then return the result so the result of calling this is an either that might be a list of error messages or might be more code to to execute and so we can we can send another input value off to that function if it indeed is a function with the app method so let's parse another value and call app and then send it off to whatever this thing returns and so there's the result of 20 plus 22. and so if either one of these were unparsable we would get a list of all of the errors so in this case there's one error a is not an integer and if we had two errors we would get a is not an integer b is not an integer so you can imagine if this were being applied to something more complicated like form validation or json parsing we could collect up a list of everything that's wrong as far as our validator is concerned but we're not quite done yet because this syntax is a little a little bit tricky and you know we have to get our parentheses right and we have to know that um because of the uh the way that app applies uh you know it must take something that is in either of list of string and function so it's kind of confusing to get the order right furthermore the function is that it appears last in our syntax which is maybe kind of annoying it might be nice to have it on the left yeah this is scholar 210 yep let's talk afterward because i'd actually like to see what two nine um chokes on um so we're gonna go back to that funny percent operator that we had before um that let us turn functor around and add another one that lets us turn app around as well so we'll have basically what we had before but slightly adapted to know about semi groups so this this lifts a function g to b such that we can apply we can map it onto some input value with the restriction that z must be able to be turned into a semi group that's about right just double check that compiles yep all right so that's basically the same uh mapper that we had before uh except we've added this implicit conversion so that we can use these semi-group z types so now let's do the same thing for the app operator so this will be a little bit more complicated but not terribly so in the case that we have an either z or a to b like we had midway through that process that we saw before and we have a way to use a semi group then define a function called this thing which is sort of a i don't know an asterisk in in angle brackets and that that takes an input value that has been parsed which might be applicable to this function and let's double check that compiles cool so what does that give us well the last thing we did was this oh sorry let's use some actual numbers the last thing we did was parse two values take the first value and apply it to this thing that's returned by mapping this parse value onto the add function now we can turn the whole thing around and say add two and then onto that function map the value returned by parsing the string 20. and then onto that function apply or app the last value and there we go so we get 42 just like we did in this case there is a key difference though so let's look at again unparsable values so if we have a and b we get a list of error messages a is not an integer b is not an integer if we use this syntax a and b you'll see that the error messages are actually reversed so we have list b is not an integer a is not an integer and that's simply a function of the order in which the application is happening because of these conversions but it is something to to take note of uh and so finally uh you know i mentioned composition at the beginning so now we we have sort of a nice clean way to do both validation and application together and then of course if this were an actual web service i were writing i would have logic that would take the result of this and if it's a valid value if it's a write send it back to the user and if it's a left you know build some error message and send an http status code 400 or something like that but as it is we have a right or left as the output of this but we can also start to compose these things so let's say we want to call add2 and abs together so we want to say add to and then on to that map the result of mapping abs1 or abs onto a parsed value and so this should give us an ether that's ready for the next argument just like we had before so we can say a to i of 22 and there we go and so now we can start to compose these things and it stays somewhat readable as long as you remember that this percent operator which could be more appropriately named means this is something that needs to be mapped and this star operator means this is an already lifted function that needs to be apt and i think that's about that's about as far as i wanted to take that so are there any questions yeah uh so you're talking about scholarships applicative builder so the question is uh scala z's applicative builder lets you build up the applicative and then call app on the result and i haven't i haven't worked with that so i'm not quite sure but it with applicatives you need to call app uh every time you want to apply a function that has been lifted so i'm i'm i'm not quite sure how that works in particular it sounds like it might be similar to um this syntax but i'm not quite sure scala does this slightly differently as i touched on where the functor and applicative traits are defined with an explicit starting type so you would say rather than saying a functor is anything with a map function in scholar z they say a functor is anything with a map function that takes the type constructed value f of a and the function a to b and returns the f of b so it's slightly different and it's actually a little bit better in the way it constrains what you're turning into a functor but i like this as a way to reason about what the functor does which is map over things do you have any kind of production examples of applying this kind of technique so examples of applying this in production uh we use uh we use something similar to this uh to do validation on our web services where we have multiple functions that might succeed or fail that need to be composed like this and by composing them together we let those we we get to lift these pure functions into this validatable world without having to do lots of pattern matching and if right then this if left than that that's all kind of abstracted from our server-side code so i can't point to anything on the web that would explicitly demonstrate this but it is in use basically this is directly applicable as is on any kind of rest api or or anything that takes unsanitized input it would be very very valuable to see a non-trivial example yeah i agree so the question is uh it would be valuable to see a non-trivial example uh i i wanted to take this all the way to a web application but unfortunately uh that would take far more time so so we have this simple trivial example is there a connection between applicatives and concurrency yes one thing that may not have been clear is that this a to i and this a to i invocation are not dependent upon each other they both produce an either as a result so they could actually run in parallel and this whole line could be deferred until all of the results are there we could also make it a little bit smarter i suppose that if the first failure that occurs might end the execution of the others i'm not sure i haven't really played with that but but certainly the evaluation order does not matter of all of these arguments and so that's slightly different than something like a monad where you have to do the the next step uh depends very specifically on the result of this step is associated again the percent operator is associative the this star operator is associative uh because that's our our apply function percent let's see so percent is map which we would really i think we would only call that once in this example uh so it i'm not sure i can't think of an example where we would have two oh so you're talking about uh this one yeah let's see that's a good question so actually not quite um so it's actually this operator that's uh that has associativity i think um and then because remember we have to do all of this and then finally when we have our our lifted values can we apply it to the function so this is actually happening last we're just writing it first because it kind of makes more intuitive sense uh so i think i'll take any other questions uh during the break because i'm getting the red light so thank you everybody