In my last article about Functional domain modeling, I explored the expressiveness of F# in modeling a domain. I fell in love with the simplicity and expressiveness of the language. In this article I will attempt to explore function purity in F#. In F#, functions are first class citizens because it allows to pass function as an argument to other function, return a function or assign function to a variable. Initially I found it a bit hard to wrap my head around the concept of treating functions as first class citizens. In fact one of the biggest challenge for me was surprisingly not the weird syntax of F#, but to think in terms of functions.
Coming from an OO background and not very comfortable with F#, below was my first attempt to write a use case. I basically tried to mimic a use case in a typical ports and adapters project from one of the C# projects. This use case serves a basic purpose which is to update the dimensions of a product, if it exists, of course.
|type UpdateProductDimensions(dataStore: DataStore)=|
|//Other private methods removed for brevity|
|let updateDimensions(productId, height, length, weight, width)=|
||> Result.map(fun p-> p.updateDimensions(height, length, weight, width))|
||> Result.map dataStore.updateProduct|
|member this.update (productId, (height,length,weight,width)) =|
|| Some p -> updateDimensions(p, height, length, weight, width)|
|| None -> Error(new exn (sprintf "Product %i does not exist." productId)))|
Pretty straight forward. The use case accepts a
DataStore dependency using which the
update method can query the
Products table for existence of a product. If the product is found then the product is fetched from the data store and dimensions are updated. Finally, the product with updated dimensions is persisted in the database using the data store.
Though this use case works, there are several problems with this way of writing code. Lets take a look at the problems:
The code above is not easily testable. Why you ask? Lets look at how many ways things can go wrong with this code.
- What if the constructor arguments are null?
- What if the
productExistsdata store method returns an exception.
- What if
updateDimensions, which in turn calls
updateProductmethods on the data store returns exception?
- What if the product does not exist?
- What if product dimensions does not exist?
- what if ….
and the list will go on ..
So many scenarios to test with just one dependency. What if I add
ExternalService as another dependency to this use case? Imagine the number of ways this third party service call can go wrong. The point is that whenever we deal with external systems, be it database or an external service, we are entering a world of uncertainty and we do not have much control over the behavior of those external systems. It would be nice if the use case and the domain model could completely avoid any type of IO operations or side affects. Sounds like a nice idea, but a use case which does not perform IO is next to useless. Stay with me and we will explore a way to minimize or avoid IO and side affects in the use case and domain model by the end of this article.
One of the beautiful things about F# is its expressiveness. The
update method in our example is all but expressive about its intent. It has a dependency on
DataStore which is not evident when we look at the method signature. This is what I call as a hidden dependency. What if in future someone modifies the
update method and calls a completely different method on the datastore? Since the
update method is not expressive enough, developers can assume a lot of things. Is there a way to avoid these hidden dependencies? Can we make the update method a little more expressive to avoid mistakes by future developers? Again, by the end of this article, we will try to re-write the
update method to make it a little bit more expressive.
Methods in our use case are not referentially transparent. Especially the
update method is not by any means. What is referential transparency? A function is referentially transparent if the function can be replaced with its corresponding value without affecting the behavior of the system. For example,
if, x + y = 10
then, the expression x + y + z = 20 can be written as 10 + z = 20
In our use case the method
update, depending on whether one of the data store methods return an exception, may or may not update the product dimensions. We can never assume that a call to
update method always updates product dimensions. Due to this uncertainty, I will not be able to include
update method in a method chain like this and assume that warehouse system will be always notified after the product dimensions update.
|member this.updateProductDimensionWorkflow(productId, (height,length,weight,width)) =|
There are other smaller problems with this code. But for now, lets focus on fixing the problems that we identified above and making this use case a little bit better.
Here is an attempt to fix some of the problems we discussed above.
|let find (productId:int, products: seq<Product>):Option<Product> =|
||> Seq.tryPick<Product, Product>(fun x-> if x.ProductId = productId then Some(x) else None)|
|let update (productOption: Option<Product>, height, length, weight, width)=|
|match productOption with|
|| Some product -> Ok(product.updateDimensions (height, length, weight, width))|
|| None _ -> Error(new Exception("Product does not exist"))|
First thing worth noticing is that these functions are not inside a
type because there is no common dependency between these two functions. These functions exists on its own (under a module) and only accepts parameters that they can work with. The function name and signature are self documenting and they clearly communicate the intent. There is no scope of nasty exceptions bubbling up because of an unstable dependency. These are pure functions.
Lets see how easy it is to test these functions.
|let ``find_WhenProductDoesNotExists_ReturnsNone`` () =|
|let productId = Fixture().Create<int>();|
|let products = Fixture().CreateMany<Product>()|
|let result = UpdateProductDimensions.find (productId, products)|
|result.IsNone.Should().BeTrue("the product id does not exist")|
Simple and straightforward. There is no need to setup mocks for different scenarios here. You can create test data to your hearts content and test this function out. I have used Autofixture, but Property based tests are better suited for testing these functions.
Coming back to the referential transparency. The
update functions are referentially transparent. For instance, if the
Product exists in the list of products then
find will always return the Product. Otherwise, it always returns
So, we have seen how getting rid of impure operations tremendously simplifies the program. But wait, this new shiny pure functional program does nothing useful. The end goal of this program is to update dimensions in the database. How do we make sure that we stick to function purity but also be able to perform impure operations like persisting data in a database or calling a third party service etc? You need to do the following:
- Push IO (impure) functions at the boundary of your domain.
- Call pure functions from the impure functions and not the other way round.
As long as an impure function calls a Pure function you are good. When a pure function calls an impure function then the whole method chain becomes impure.
You would have guessed where we are going with this kind of separation between pure and impure functions. OK, no points for guessing, moving impure functions at the boundary and calling pure functions from there naturally leads you to clean architecture.
Let’s see how does the call to pure functions from an impure function looks like.
|let main argv =|
|let getDbConnection() =|
|let connection = new SqlConnection("a_real_sql_connection_string")|
|let getAllProducts() =|
||> fun x -> match x with|
|| Ok p-> p|> Seq.toList|
|| Error e -> List.Empty|
|let tryUpdateProductDimensions (productId: int)=|
||> fun products -> UpdateProductDimensions.find (productId, products)|
||> fun product -> UpdateProductDimensions.update product|
tryUpdateProductDimensionsis an impure function because it calls
getAllProducts which in turn calls
allProducts from the
DB module performs a bunch of impure operations. We can call it as the core of impurity (pun intended). For the sake of completeness, here is how I have implemented the DB module.
|let allProducts(connection: DbConnection) =|
|let getProductType productType =|
|match productType with|
|| "Parcel" -> ProductType.Parcel|
|| "WhiteGood" -> ProductType.WhiteGood|
|| "XL" -> ProductType.XL|
|| _ -> ProductType.XL|
|connection.Query<ProductRow>("select * from products")|
||> Seq.map(fun i-> new Product(i.ProductId, i.Name, i.Height, i.Weight, i.Length, i.Width, getProductType i.ProductType))|
||> fun i-> Ok(i)|
|| ex -> Error ex|
|//Other methods removed for brevity|
For a developer who is not fully familiar with F# or the functional programming paradigm may quite easily fall into the pit of writing OO style code with F# just like me in the beginning of this article. In F#, separating impure functions from pure functions requires discipline. As I understand, F# has no in-built magic to prevent you from mixing pure and impure functions. Haskell in that respect is pretty strict because it forces you to wrap impure functions using an IO monadic system. In simple terms an IO monad is an abstract data structure which elevates a value. Once the value is elevated you can no longer access the original value. To access the original value, you must use one of the mapping functions of the monad. I can write a similar IO monad in F#, but is it really worth the effort? The F# compiler would not honor such an IO monad. Though I think that it may be a good idea to enforce it as a coding convention for your projects.
Let me know in the comments section below what you think about the IO system in Haskell. Should F# support something similar? If yes, why and if no why not?