Join egghead, unlock knowledge.

Want more egghead?

This lesson is for members. Join us? Get access to all 3,000+ tutorials + a community with expert developers around the world.

Unlock This Lesson

Already subscribed? Sign In


    Refactor Model Validation to Handle Complex Cases in an Elm Form

    Enrico BuonannoEnrico Buonanno

    Previously we created a validation model, however it's too specific and can't handle enough cases as our app scales.

    In this lesson we'll learn how to refactor a validation model and make it more general. We'll also step through the logic of our types and fix any possible issues.



    Become a Member to view code

    You must be a Member to view code

    Access all courses and lessons, track your progress, gain confidence and expertise.

    Become a Member
    and unlock code for this lesson


    Instructor: 00:01 We were able to come a pretty long way using this very simple field type but there are some aspects of the design that are not entirely satisfactory. For example, notice that we assume that the input is a string, and that then as we validate it we turn it into some more specific type A.

    00:20 That's great because we can, for example, validate that the string is an int, and then chain that with some more specific validation, for example that that int is positive.

    00:29 What doesn't work so well is when we have to go the other way around, when we have a valid A and we want to render the string. For one thing, that's not a very nice API to code for. For example, to display the value of the field, we need to provide a function to render that field.

    00:45 Here, we were using identity to string and so on. We could even run into problems as we convert back and forth between A and string. For example, let's assume that the age field instead of natural would be of a float. We could pass string to float as a validator, and then change the field to be an option of the field of float.

    01:19 Let's say I enter 16.50, it calls validation, and now this turned into 16.5. That's because our validation has converted it to a float, but then to render it, we use the two-string function. Also, if we want to validate related fields, we might have one field causing another field to go from valid A to non-validated string, and again we have this problem of going from A to string.

    01:47 In a sense, all these problems will be cured if we only stored this string that is input by the user here, but then if you look at this type now, then we always store the current input no matter what the state of the validation.

    02:01 Then, perhaps it would make more sense to define this type like this as a record that has a value of type string and a validity. This would be our union type validity A. This revised model encompasses the fact that a field has a current value, that's the row value passed by the user for the UI, and a validity that has more to do with the business rule of the validation.

    02:39 If you look at this type validity of A, you can see that it's somewhat similar to a maybe of A, or to a result of string A. It has these three states not validated, which is somewhat like nothing, valid A which is somewhat like a just A or an OK A, and a valid string which is somehow an error state.

    02:59 It's a bit of a combination of a maybe and a result, but with names that are more specific to the domain of validation. There is a small problem in that, if we were to expose the field type like this, then you could have some client code defining the field value like this.

    03:18 For example, with the value of hello and the validity of valid 10. In this case, the value in the field, and the A in the validity have gone out of sync, and we would like to prevent this. By the way, this would be a type alias.

    03:37 How can we prevent client code from creating such inconsistent values? Instead of a record, we can use a single case union type that holds a string and a validity of A. Instead of these field names, we can have similarly named functions that extract this data from a field. For example, let me define row value. Here, I can destructure this as a field with a row value and a validity.

    04:10 I'm just going to return the row value, and similarly validity picks a field with a row value and a validity, and extracts the validity. To make it production ready, we will export field, and this exports it as an opaque type, meaning that the client code will see the field type, but he will not see this field constructor.

    04:36 In other words, it won't be possible for any code outside this module to create a bad value like this. Know that we can do this just for now, for convenience, let's still expose all the values. Another thing is that so far we always assume that the row value is a string, but we could have fields other than inputs.

    04:55 For example, checkbox have a value of Boolean, or date selectors have a value of date. We inaudible this should also be parameterized, so we could call this row and a field will have an additional type parameter. Now, I think I've achieved a way to model fields that are subject to validation that is slightly more complex, but also more efficient and more general.

    05:18 Now, let's see how this new model would affect our code. For one thing, this complicated display value would not be necessary. I can just use row value instead, and the same for the other fields. This means, I can get rid of the display value all together. Let me bring this one up so that everything to do with fields is in one place.

    05:50 Here, I have the additional parameter row and remove this value here, and here is the validity of the field. When I declare fields, I now have an additional type parameter that's a string. To initialize a field, I need to expose a function, let's call it field.

    06:30 These are all functions to work with validator functions, so these remain unaffected, whereas, we have actually some validation logic. Here, we can destructure the field as a field with a value and a validity. I'm actually going to rewrite this more simply to return a field with the value. Let me call this validate, under, I'm going to validate the value, and this yields a result.

    07:06 Then, I need to do this. Let me extract this into another function.

    07:31 Here, I can feed this to validity, and get rid of all this. Then, the apply function shouldn't really have anything to do with fields, but only with validity of the field.

    07:48 Let me fix this typo here, and here my types are wrong, so I go from the row value to an A and the field also has its parameter row. Now I've sufficiently fixed my validation module so that it compiles. Let's move on to the client code, the main file.

    08:48 Here, I need to change how I construct these fields of course, but I'm actually going to simplify this code. Instead of clearing everything manually, I'm going to go back to the initial model, with the only change that the status changes to succeeded.

    09:06 Here, instead of not validated, I use the field function. Here, remember that this is an inaudible for the apply function, and this takes now not of field, but rather a validity.

    09:38 Now, my revised application compiles. In general, the nice thing about a tight type system like Elms is that, if your code compiles, it generally works, but just to make sure, let me try a few things.

    10:07 It looks like everything is working fine.