What Is Purity?
A pure function is one that has no side effects. This means it does not concern itself with anything outside of itself. Let's take a look at some examples.
public void Log(string message) { Console.WriteLine(message); }
This Log method takes in a string and writes out to the console. This function is doing something outside of itself. One clue is that is returns void. Any function that returns void must be concerning itself with something external otherwise it does absolutely nothing. This function is not pure.
What about:
public MenuItem GetMenuItemById(long id) { return _dbContext.MenuItems.FirstOrDefault(_ => _.Id == id); }
This method takes in a long and returns a MenuItem. But this function must go to some external source to get the response. This function is not pure.
What about:
public OrderResponse SubmitOrder(Order order) { .... return httpClient.Post("/purchase", content); }
Again, no this function is making an http call to another service. It is not deterministic. What if the http service has a bug, or is down, or returns non-deterministic data?
What about:
public List<T> AddToList(T item) { myList.Add(item); return myList; }
This function, too, is modifying something outside of itself. Where is myList
and what else uses it? We don't know from looking at just this method. We'd have to look at all the other code to see what ramifications modifying this list could have. This example is one that causes developers to have to hold a lot more information in their head when modifying code. Where does myList
come from? What else uses myList
? If I modify myList
, what other effects might that have? We have to think a lot harder when writing code like this.
What about:
public DateTime SetExpirationDate() { return DateTime.Now.AddDays(30); }
This function is impure, too, because it will return a different response every time it is called.
All of these functions are talking to external services or doing indeterminate things. So what is a pure function?
A pure function, when given an input, will always give the same output, regardless of anything external. This makes our code much easier to reason about. We only have to focus our attention on the function in front of us.
What does a pure function look like?
public int Add(int num1, int num2) { return num1 + num2; }
This is an example of a pure function. It takes two integers and returns the sum of those two integers. It doesn't matter how many times we run this, if we provide the same two numbers we will always get the same response. For any given input we alays get the same output. It is deterministic.
So can we make an impure function pure? We sure can. Let's take the SetExprationDate
example above. If we write it so that we pass in the date and add 30 days to it, we then have a pure function:
public DateTime SetExpirationDate_PureExample(DateTime date) { return date.AddDays(30); }
Now we have a pure function that is deterministic. The AddtoList
function is easy to write in a pure fashion, too.
public List<T> AddToList_PureExample(List<T> myList, T item) { myList.Add(item); return myList; }
Now that the list is passed into the function, too, we know we are not mutating global state. This is now a pure function that if we run with the same inputs, we will always get the same output.
What about things like database contexts and HTTP clients?
While we can't make the database call and the http call itself pure because they depend upon external dependencies, we can make our functions pure to the extent that we are able. How could we do this? By passing the impure dependency into our pure function. Remember a pure function is one that for any given input, it will always give the same output.
public IQueryable<MenuItem> GetMenuItemById_PureExample(long id) { return _dbContext.MenuItems.Where(_ => _.Id == id); }
Now our GetMenuItemById
method is pure in that it does not concern itself with anything external. By returning an IQueryable, we are returning a delayed function. Any LINQ method returning an IQueryable is pure because it is returning a query, not the actual result. LINQ methods such as ToArray
, ToList
, First
, FirstOrDefault
, etc are all impure on a query because the result can be dependent on an external indeterminate source. LINQ methods such as Where
, 'OrderBy, and
Select` are pure because they are deterministic.
Why
So now that we have looked at several examples of impure and pure functions. Why do we care about this? As eluded to earlier, by focusing on pure functions we don't have to hold a lot of additional code in our head when making changes. We know that our function is isolated from everything else. In fact, a good test of whether a function is pure or not is whether it can be made static
. Static functions must be pure and not concern themselves with any details of external things.
With pure functions we can be more sure about what our program is doing. Pure functions are also easier to test because we don't need to worry about setting up and mocking a lot of other dependencies. Pure functions should be the building blocks of our program. Think about it....what in your program really is impure? Database contexts, HTTP calls, logging, reading and writing files, etc. Everything else in between can be made pure. Mapping data objects, creating queries, creating objects, all can be made pure. It's the boundaries of our program that are impure. Everything inside can be pure. Our pure functions can be our Lego blocks to building the correct program. How do we do this? In future articles we will look at how we can change our thinking about how we build our applications using pure functions to create more concise, easily testable, easily readable, easily extendable code we can be more confident in.