Introduction to Functional Programming in F# – Part 9
Introduction
In this post we are going to see how we can improve the readability of our code by increasing our usage of domain concepts and reducing our use of primitives.
Setting Up
We are going to use the code from Part 1.
Solving the Problem
This is where we left the code from the first post in this series:
It's nice but we still have some primitives where we should have domain concepts (Spend and Total). I would like the signature of the calculateTotal function to be Customer -> Spend -> Total. The easiest way to achieve this is to use type abbreviations. Add the following below the Customer type definition:
We have two approaches we can use our new type abbreviations and get the type signature we need: Explicitly creating and implementing a type signature (which we did in Part 6) or using explicit types for the input/output parameter. Type signature first:
Followed by explicit parameters:
Either approach gives us the signature we want. I have no preference for the style as both are useful to know, so we will use the explicit parameters for the rest of this post.
There is a potential problem with type abbreviations; As long as the underlying type matches, I can use anything, not just Spend. Imagine that you had a function that takes three string arguments. There is nothing stopping you supplying the wrong value to a parameter.
Of course we can write tests to prevent this from happening but that is additional work. Thankfully there is a way that we can stop this in F# - The single case discriminated union!
It is convention to write it this way but it is also:
You will notice that the calculateTotal function now has errors. We can fix that by deconstructing the Spend parameter value in the function:
If you replace all of your primitives and type abbreviations with single case discriminated unions, you cannot supply the wrong parameter as the compiler will stop you.
The next improvement is to restrict the range of values that Spend can accept since very few domain values will be unbounded. We will restrict Spend to between 0 and 1000. To do this we are going to add a ValidationError type, prevent the direct use of the Spend constructor and add a new module to handle the domain rules and the creation of the instance.
The name of the module must match the name of the type. We need to amke some changes to get the code to compile. Firstly we change the first line of the calculateTotal function to use the new value function:
We also need to fix the asserts. To do this, we are going to add a new helper function:
As a piece of homework, try to replace the validation we did in Part 8 with what you have learnt in this post.
The final change that we can make is to move the discount rate from the calculateTotal function to be with the Customer type. The primary reason for doing this would be if we needed to use the discount in anther function:
This also allows us to simplify the calculateTotal function:
Whilst this looks nice, it has broken the link between the customer type and the spend. If you remember, the rule is a 10% discount if an eligible customer spends 100.0 or more. Let's have another go:
You will probably need to move some of your types to before the Customer declaration in the file.
We now need to modify our calculateTotal function to use our new function:
To run this code, you will need to load all of it back into FSI. Your tests should still pass.
Is this approach better? It's hard to say but at least you are now aware of some of the options.
Final Code
Conclusion
In this post we have learnt about the single case discriminated union to enable us the restrict the range of data that we support for a parameter and that we can add helper functions/properties to discriminated unions.
In the next post we will look at object programming.
If you have any comments on this series of posts or suggestions for new ones, send me a tweet (@ijrussell) and let me know.