ElixirConf 2021 - German Velasco - Making invalid states unrepresentable in LiveView

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
[Music] [Applause] [Music] [Applause] [Music] thanks for coming to my talk i want to talk about making invalid states invalid in live view and it'll be a bit of an exploration in the sense that i want to take you in a journey and let you draw your own conclusions now i want to start with talking about what does it mean what does it mean to make invalid states invalid and this is going to be sort of my definition but what it means is that how we model our domain has the ability to constrain the states that we allow in our application and therefore it means that we can model our domain so as to remove certain states that we don't want so you can think of it as trying to remove a whole class of errors by simply modeling our domain in certain ways now this is not a new concept it's it's been around in the functional programming languages so you can see the no camel elm f sharp among many others and it was actually coined by yarn minsky i think the original term now it's gone by other names making invalid states unrepresentable this was the original one making the legal states are representable making impossible states impossible but typically the idea comes with a very powerful type system where you can express the domain in your types and the code or the compiler won't let won't compile if you have an invalid state so that's the original idea now i wanted to bring it to live view even though elixir doesn't have a compiler that does that for us although the compiler is getting better it seems like every day but i wanted to bring it to live view because i wanted to see if we could apply that mentality in how we shape our domain and if we could but by doing that remove certain invalid states and in particular i wanted to bring it to live view because library has persistent state so we are storing this in a process and we have to keep this state in sync across time via events right um and the other thing is that our users see this state right it tends to be reflected in the ui and so any bugs that we see are almost immediately apparent so that's why i want to bring it to live view now what i want to do is walk you through a flight booker example what we'll do is talk about sort of two implementations one is the perhaps one where we haven't considered the domain modeling implications and another one where we do consider that and try to see if we can remove certain uh invalid states so roughly speaking we'll be doing this we're going to be building a you know it'll be built but a flight booker you can book a one-way flight a two-way flight we want to be able to show feedback to our users some errors you know have some validations that you know departure can't be before the return date or sorry after the return date and you know we can book two flights so roughly speaking that's where we're aiming so let's dive into the first implementation the one where we're not considering domain modeling and perhaps this is a naive implementation but what we'll do is we'll map each of the fields you just saw the select the departure and the return inputs uh into three assigns right we could pass a map maybe here but we'll just do it as three assigns um so we have the direction we'll set it as one way when we mount the live view the departure is a date and the return will set as nil at the beginning we're going to render those a normal form we'll actually have a select that will take the direction a text input for the departure a text input for the return value and then we'll handle that submission normal stuff we'll handle the event for book uh we'll take those parameters and we'll pass them to a module and for now we're not going to do any uh error handling or anything like that so we'll get into that later but we'll pass that in and what that function will looks like is we're just going to do a case statement in those params grab the departure we could consider here booking the trip and then we say you know you have booked a one-way trip with this departure date for a two-way we grab the departure we pas we grab the return date and we do the same thing we book the trip we pass a two-way message so that code actually builds this so we can actually have a one-way trip or a two-way trip perfect so we grab this we ship it to production we're celebrating and then income the bucks right so let's take a look at a a few bugs a few example bucks here um you have a customer they call you they say hey i booked a one-way flight but i got charged for a return flight as well that's not good you refund it okay uh you have another customer and there we go you have another customer calls you they say hey i booked a two-way flight but uh it actually i made the flight over there and i was expecting to get a return flight but i never got anything i'm now stranded right but a little bit worse you know and again this is just some potential bugs now one of the reasons why this can happen is that our domain allows it right let's look at our domain right now this is our current although it's implicit domain model and it's implicit because it's implicit in the science we're not really keeping it anywhere other than the assigns in the state in live view so we have a direction it could either be one way or two way a departure that's either a date or nil and a return value that's either a date or nail now because it's a map in order to consider the possible states that this could get into we have to look at all the possible combinations of states so if we list them out these are all the possible combinations we have eight possible combinations so if you have a one-way flight you could have a departure and a return that both have dates or departure nil return date date nil nail nail and now if you consider the two-way flights same kind of thing right you have either two dates nil date date nil or nil nil so our domain allows for all of these eight possible states when in reality six of these are invalid right if you look at the first state you realize that's the one where your customer calls you and said hey i booked a one-way flight by you charged me for a return trip um and if you look at the second from the bottom you'll see that that's the two-way flight where your customers stranded somewhere because they booked a two-way flight but the return flight was nil right so uh there's six in valid states which means there's only two valid states out of the eight and these two valid states are really the only ones we should be allowing there's the one-way flight that has a departure date and no return date and a two-way flight that has two dates so let's think about a more constrained flight booker domain model like how could we do this and let's first express it in natural language we can have a one-way flight with departure date or we can have a two-way flight with a departure and return dates and that ore is important that's part of our domain modeling so the way we could do this is by using a sum type so this is a our domain model that we could it's one possible implementation i should say of the domain model uh that we could represent so we could have a one way uh we're gonna tag a date and here we're using a tuple to have a fixed size we're gonna say it's it fits one way it only has one date or and that's a pipe right there or the divider two way and two dates right and because it's at some type when you list out all the possible states you're adding their states as opposed to checking all the possible combinations right now how do we implement this a possible implementation is that we have a module called flightbooker and we have a function that's one way and it only takes a date we pattern match on that and we tag that value right we have a tag a one way and we put that date and we have a two-way function we tag those two dates uh pardon me we pass only dates and we tag those two dates now we could do a more advanced way of tagging if you don't like tuples um you could use structs but note that they should be two different structs right a one-way struct and a two-way struct you could even enforce keys that kind of thing for the rest of the presentation i'm going to kind of go with tagged tuples because i i like the how this forces us to think about things okay so that's our new domain model now let's talk about the implications of that new domain model in terms of live view so there's i think of it as two things there's two implications one is how we render things to the user and now we need to render this uh less common and perhaps more complex domain model and the other one is how do we handle the events that come with user input so let's take one each one of those at a time we're going to first look at how we render a complex type in this case we're going to just mount it and pass a single assign we're passing that one way with a date and this is how we're going to render it we're going to make a case statement inside the form uh with the booker and we're going to pattern match here so if we have a one-way we're going to grab the departure and we're going to render the select it's already has a value of one way so that's no longer dynamic we pass the departure and we disable the return if we have a two-way flight we have the departure the return and we fill out the select as two-way and we fill out the two text inputs now i will be the first to admit that i i was not a big fan of case statements in my ex templates i don't really like them but the more i looked at this and the more i used this the more it grew on me and i like it because our domain model is forcing us to clearly outline how to handle each scenario this is very declarative you have if you have a one-way if your state is one-way flight then this is how you render it if you have a two-way flight it's really this is how you render it right so it's declarative and it's also exhaustive this represents all the possible states in which our ui could be in um so i really like that it's it's there's a lot to it and the other thing i really like about it is that it forces us to deal with uh potentially invalid states take a look at this right because when we have a one-way flight we don't have a return value right we don't have that uh return we don't even have it as nil so we're forced to choose what to do with this text input for the return in this case we disabled it but the more i thought about it i said i don't even want this input here this doesn't even make sense so we could even remove it right and all of this comes just because we have expressed our domain differently because we only have a departure date there we're forced to reckon what do we do with the return and so we've actually removed an invalid state uh you know we cannot have a one-way flight with a departure date and a return date that return date is no longer valid let's talk about how we handle events in order to handle events because we have a specific domain model we're now going to have to transform our user input and i want to do it through parsing now what do i mean parsing because people use that word in in many different ways what i mean is this we're going to transform less structured data into more structured data or transform raw data from the user into a valid domain and because we're doing this we're sort of transforming dangerous data into valid data and i i like this this is a really good thing because rather than letting the data flow through our system until it gets validated right before we insert it into the database we actually have an anti-corruption layer something that is saying this is unsafe data and at the edge of our system we're going to handle it now you can do this kind of thing without having this kind of domain model but i think the domain model forces us to do this it's an implication of it so let's look at it in practice we have this event where we book we're going to book the trip and we grab the params but before we book the trip we need to parse those params into a booking so again what might an implementation of this look like and this is one of the implementations had we're going to do a case statement on the params we're gonna have a a one-way flight and we have a departure and we're gonna force that into a date and pass it into the one-way function that we saw before if we have a two-way flight with departure and return we're going to force those into dates and we're going to pass those to the two-way function and surprisingly and very interestingly we have this third case which is what happens if we have a two-way flight but it only has a departure and this happens when you toggle from a one-way to a two-way flight and and we trigger a phoenix change like a fee and we do an update uh at that point we haven't yet selected a return flight and that would have been a nil value in a different scenario but here we're forced to handle this because our two-way function requires two dates right our domain is requiring two dates so we need to do something about this scenario so we just create a date it could be today you could assign it to the same as the departure date you know you could handle it in different ways but our domain has forced us to handle this scenario and so we remove another invalid state and that's the you have a two-way flight but no return value right so we've handled that and now if you're looking closely uh we are coercing all these dates in a not a great way but we are doing it um into being dates right and because we're coursing all these values we are maintaining that domain purity and by coercing all of these we're actually removing any of these invalid states for invalid states that could have a nil value right and so without any error handling we have now removed six invalid states now we're not at the end yet because an invalid state right now an invalid date uh crashes the live view process and it restores it to a clean state so that's good in the sense that we're keeping domain purity um but it's not a great user experience because it'd be nicer if we could actually show the user where they where they made a mistake and allow them to fix it so let's add error handling and you've probably been asking this the whole time right you've been saying okay we have nils but we're going to be validating and presenting errors to our users um of course we'll be doing that but i want to present to you that even if you add errors you have the potential for same type of problems so even your errors are part of your domain modeling let's take a look at an example so let's say we can we add this uh into our domain model our original domain model okay the first implementation we had we're going to say that we could have errors and there could be a list of the the field that has an error it could be a departure or a return right and we're going to throw that into an assign along the rest of our assigns again this is the original implementation so when we mount the live view we're not going to have any errors so we pass an empty list but now let's look at what this does to our possible states these were our previous possible states the eight states let's grab one of them and apply the errors and consider all the possible states the errors bring so this one error or sorry this one possible state now becomes four possible states because we have to handle the case when there's no errors a departure error our return error or both errors right and if one of those possible states became four it means that all of our possible states now become 32 possible states right so we had eight and now if we're handling all four kinds of errors uh we now have 32 possible states and this is a bit of a combinatoric explosion right um to be sure you know we'll have conditional checks in several places in the code base removing many of these and preventing many of these but i want you to realize or consider right now not the concretion but the abstraction that the notion that by adding a single assign uh we have gone from eight possible states to 32 right and so you can imagine the explosion complexity in our live view the more we add each individual assigned if we don't consider the implications in terms of our domain model in the possible states and so you could see that we'll cover some of these with if checks and and things throughout our code base but it's easy to miss one of these and end up with an invalid state and introduce bugs right consider just one example you could have a customer call you and say hey i'm trying to book a one-way flight and i have selected one way and i have a departure date but it it tells me i can't book it because it i need to select a valid return date right and so you can see here that the error somehow has a return value and we could even be checking for this in the template and saying if there's a return error you know render this message so we were even checking for that but of course somehow we have the incorrect error in our domain so this is a bug right um okay so out of the 32 possible states there's only six valid states that we actually care about you have a correct one-way flight that has a departure date and there and no return date no errors or a one-way flight that doesn't have a departure date or has an invalid departure date and so we should have an a departure error or we could have a correct two-way flight with two dates no errors or a two-way flight with errors which means you have you know one of those is one of the fields is invalid and the corresponding error is present and this is truly what we wanted right when we added these errors that's what we're trying to get to but our domain allowed for a lot more possible states so how can we model our domain with these errors well we can modify our previous domain we have the flight booker we still have a tag tuple we have a one way and instead of having a date we're going to call it date or error in a two-way and it's either date or error and data right you still have two fields and the date or error is another sum type that's either a date or an error tuple that is um tagging the the original value whatever was that caused the error and we need that because we're gonna return it to the user and show it to them so our possible states with this domain are these right so we have those six possible states we have a either a one-way flight with a date or a one-way flight with an error and the any and a two-way um flight with two dates or an error in one of them or both errors that covers our six possible states let's quickly look at how we render this right again let's go through the implications if you will uh we're do going to continue doing case statements because those are really really nice for rendering some types so you'll see we case on the booker we want we haven't changed really this that much we have a one way with a departure and now the one difference is that we actually use a function component to render the departure input and the same thing with a two-way flight we grab the departure the return and we render those two as function components and we do it as function components because they have some complexity of their own so this is a departure input we're going to case statement on the departure if there's an error now we show the user hey this is an invalid date and we're keeping the original value to put it in the text input and if it's a successful one and you could have called this okay and value that probably would have been okay you know we can just render the correct value in the text input and it's the same thing for the return input we're going to case statement on it if there's an error we're going to show the error if it's a correct value we show the value now let's take a look at transforming the user input so before we were we had this uh parse params into booking that was really important uh we were coercing all the dates here but now our domain model allows for handling these things so this gets simplified into just mostly passing the values to our one-way function and two-way functions of course we still have to remember to pass this return function in the two-way case and what we're doing is we're sort of adding another step of parsing and now this is in our function in our domain we can grab those values and we parse those dates into either a valid date or an error and so let's take a look at how we define those uh if it's a valid date we just return the date if it's a string we're going to do some parsing and if it's anything else we're going to call it an error and let's quickly take a look at the string portion we're going to do the string parsing if it's a valid date we just return the date if it's an error we're going to tag it as an error and that's what this error function is doing it's just tagging those values as an error tuple so we're now parsing those things and now we can just build the one-way and two-way tagged tuples and so that's what these functions are doing notice that we no longer require them to be dates because our domain now allows for uh errors right so it's better expressed and by doing this right by defining our domain in this way we actually remove 26 invalid states right um and that's amazing so so when we added error handling we created a problem in some way if we didn't consider our domain modeling because we actually created a lot more possible states that were invalid and actually considering our domain modeling with those errors improved things so that's amazing but before we think this solves everything i do want to state that we cannot model everything um and for the cases where you cannot constrain certain things with the domain model i do think we need to validate what we cannot constrain and so i wanted to include an example of that and it's a pretty uh rough validation but this is this is what we can do for example for the two-way flight we don't want the departure date to be after the return date right that's clearly an error but it's really hard to model that i try to use a date range and things but you can have a descending date range so it didn't it didn't quite work um so what i did is validated it right so i compare the dates if the departure date is greater than the return date we just return an error show that to the customer and say hey your departure date cannot be after the return date otherwise you successfully book a two-way flight with the departure and return dates okay throughout the conversation i feel like um i've been skirting around the elephant in the room and so i'd like to talk about it now and that is what about ecto right we've been dealing with forms we've been dealing with all these things you've been probably uh being yelling at the screen saying we would use ekta for this um x2 is really well integrated with phoenix forms it's amazing at casting like those casting helpers are wonderful having to do that conversion of dates it's you know it's not something i want to do all the time it has great validation helpers and you can actually use schema-less chainsets as sort of the anti-corruption layer right you don't have to wait to get to the repo layer to use ecto so what about ecto i say use it as much as you can right um but i think it's helpful to think about where does ecto shine ecto shines with maps and structs obviously um [Music] and our domain actually has a sum type where each of the elements of that sum type are maps or structs right they're they're called product types um so we can actually use both we can combine some types with vector chain sets right so let's do that quickly we could modify our domain modeling so we had a one-way with a data error or a two-way with data or error and we can just change those to ecto-change sets right and now uh what worth noting is that each of these changes are actually for a different module right or at least that's how i implemented it so let's look at our functions how this changes when we do the casting and error generation the things we were doing manually we can now actually rely on ecto for that so we can actually do this this parsing of dates we can actually use the structs that we've created you know you define a module in product to change set define a chainsaw function all that stuff and you can validate the departure's there for the one-way validate departure return for the two-way and then we can use that to generate these chain sets right and we just build our structs the normal way uh we're tagging those values right and what that means is that when we render this we're still gonna do a case statement right we're just gonna steal a case on the booker and we have a one way or a two-way but what was really interesting when i went this path is that it made me realize that i actually want two forms not one so on the one way uh when we have a one-way flight and we have that change set we render one form right and this form is the one-way flight form and when we render a two-way form or a two-way chain set pardon me we're in a different form this one has a departure and a return flight and what's very interesting it made me realize that the select doesn't even need to be part of the form right it could be a select drop down but it could just as well be a button or something else right we could do buttons here and what we're doing now is we have a separate uh phoenix click attribute here and by realizing this um it helped me realize that we have two different things one thing is that we're changing our state we're changing from a one-way flight to a two-way flight or from a two-way flight to a one-way flight and then we have forms right and that cleaned up a whole lot of things for example when we handle the change of state um we can now handle them explicitly as events right so we handle the change flight type and if we're changing it to a one-way we simply define the one-way booker and we assign it and if we have a we're changing to a two-way flight we use the two-way function and we define and we assign that to the booker and the really interesting thing is that this allowed me to remove that really large parse params into booking function that was doing a whole bunch of parsing it removed it all together because we now have this explicit state or this explicit place where we're changing state and then the handling of the forms is also very descriptive and declarative there's no more uncertainty as to we sometimes have a two-way that has a departure but no return kind of thing so if we have a one-way form it's going to have a departure we pass that to the one-way flight booker and if we have a two-way form we're going to pass it to the two-way flight booker there we go so i hope this is helpful or at least intriguing so that next time you are using live view you think about how you could model your domain to remove invalid states um i think it's helpful to write down all the possible states because sometimes you you have more than you realize and i would say don't be afraid to render complex types i was i did not like the case statements at first but i realized that they actually render very declaratively and exhaustively right it covers all the possible states that our ui can be in don't let bad data into your domain i think this is something that comes out of this domain modeling so definitely parse raw unsafe values into your domain values and validate why you cannot constrain it's okay right like i'm not saying throwaway validation you have to do this you know you only have to do parsing kind of thing and finally combine all that stuff with ecto when dealing with maps and structs because ecto's casting and validation is really helpful so i hope you found that helpful my name is hermann velasco i did not introduce myself at the beginning i wanted to do it at the end you can find all my info at hermonvalasco.com and i do software consulting so if you're interested in building great software together i'd love to hear from you and thank you very much
Info
Channel: ElixirConf
Views: 1,252
Rating: undefined out of 5
Keywords: elixir
Id: Xu2QtHUbFmc
Channel Id: undefined
Length: 27min 14sec (1634 seconds)
Published: Sun Oct 24 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.