📜 ⬆️ ⬇️

Designing Types: How to make invalid states ineffable

I present to you the translation of the article Scott Wlaschin "Designing with types: Making illegal states unrepresentable" .


In this article, we will look at the key advantage of F # - the ability to “make invalid states ineffable” using a type system (a phrase borrowed from Yaron Minsky ).


Consider the type of Contact . As a result of the refactoring, it became much simpler:


 type Contact = { Name: Name; EmailContactInfo: EmailContactInfo; PostalContactInfo: PostalContactInfo; } 

Now suppose that there is a simple business rule: "A contact must contain an email address or postal address." Does our type fit this rule?


Not. It follows from the rule that the contact may contain an e-mail address, but not have a postal address, or vice versa. However, in its current form, the type requires that both fields be filled.


It seems the answer is obvious - make the addresses optional, for example, like this:


 type Contact = { Name: PersonalName; EmailContactInfo: EmailContactInfo option; PostalContactInfo: PostalContactInfo option; } 

But now our type allows too much. In this implementation, you can create a contact without an address at all, although the rule requires that at least one address be specified.


How to solve this problem?


How to make invalid states ineffable


Having considered the rule of business logic, we can conclude that there are three possible cases:



In this formulation, the solution becomes obvious - to make a type-sum with a constructor for each possible case.


 type ContactInfo = | EmailOnly of EmailContactInfo | PostOnly of PostalContactInfo | EmailAndPost of EmailContactInfo * PostalContactInfo type Contact = { Name: Name; ContactInfo: ContactInfo; } 

This implementation is fully compliant. All three cases are expressed explicitly, while the fourth case (without any address) is not allowed.


Note the case of "email address and postal address". For now, I just used a tuple. In this case, that's enough.


Creating ContactInfo


Now let's see how to use this implementation with an example. First, create a new contact:


 let contactFromEmail name emailStr = let emailOpt = EmailAddress.create emailStr //          match emailOpt with | Some email -> let emailContactInfo = {EmailAddress=email; IsEmailVerified=false} let contactInfo = EmailOnly emailContactInfo Some {Name=name; ContactInfo=contactInfo} | None -> None let name = {FirstName = "A"; MiddleInitial=None; LastName="Smith"} let contactOpt = contactFromEmail name "abc@example.com" 

In this example, we create a simple helper function contactFromEmail to create a new contact, passing in the name and email address. However, the address may be incorrect, and the function must handle both of these cases. The function cannot create a contact with an incorrect address, so it returns a value of type Contact option , not Contact.


Change ContactInfo


If you need to add a mailing address to an existing ContactInfo , you will have to handle three possible cases:



The auxiliary function for updating the mailing address is as follows. Note the explicit handling for each case.


 let updatePostalAddress contact newPostalAddress = let {Name=name; ContactInfo=contactInfo} = contact let newContactInfo = match contactInfo with | EmailOnly email -> EmailAndPost (email,newPostalAddress) | PostOnly _ -> //     PostOnly newPostalAddress | EmailAndPost (email,_) -> //     EmailAndPost (email,newPostalAddress) //    {Name=name; ContactInfo=newContactInfo} 

This is how the use of this code looks like:


 let contact = contactOpt.Value //      option.Value  let newPostalAddress = let state = StateCode.create "CA" let zip = ZipCode.create "97210" { Address = { Address1= "123 Main"; Address2=""; City="Beverly Hills"; State=state.Value; //      option.Value  Zip=zip.Value; //      option.Value  }; IsAddressValid=false } let newContact = updatePostalAddress contact newPostalAddress 

WARNING: In this example, I used option.Value to get the contents of the option. This is valid when you experiment in an interactive console, but this is a terrible solution for working code! You must always use pattern matching and handle both option constructors.


Why bother with these complex types?


By this time you could decide that we were all too complicated. I will answer with three theses.


First, business logic is complex in itself. There is no easy way to avoid this. If your code is simpler than business logic, you do not handle all cases as it should.


Secondly, if logic is expressed by types, then it is self-documented. You can look at the type-sum constructors below and immediately understand the business rule. You do not have to spend time analyzing any other code.


 type ContactInfo = | EmailOnly of EmailContactInfo | PostOnly of PostalContactInfo | EmailAndPost of EmailContactInfo * PostalContactInfo 

Finally, if the logic is expressed by type, then any changes to the rules of business logic will break the code that does not take into account these changes, and this is usually good.


The last point is revealed in the next article . Trying to express the rules of business logic through types, you can come to an in-depth understanding of the subject area.


')

Source: https://habr.com/ru/post/424895/


All Articles