This article will define a “Pure Function” and an “Idempotent Function”. It will also explain the advantages of each type of function using examples in C# .Net.
A method is a “Pure Function” when it always returns the same results when given the same inputs. This method is a “Pure Function”:
This method is not a “Pure Function”:
“Idempotent Functions” have two slightly different definitions.
- In much of computer science, an “Idempotent Function” is a function that always returns the same result when given the same input and can include calls to methods that retrieve data from a database and also calls like HTTP GET and HTTP DELETE. Methods that can be called repeatedly that will return the same result are “Idempotent”.
- In “Functional Programming”, “Idempotent functions” are more like “Pure Functions” that go a step further. In “Functional Programming”, if you take the output of an “Idempotent function” and call that function again using the output of the last call as the input for the next call, you will get the same result again. Here is an example of using an “Idempotent Function” named abs:
var x = abs(-3);
var y = abs(x);
The result of calling abs(-3) is 3. And that is the same result as calling the abs(abs(-3)).
abs(abs(x)) = abs(x)
I previously wrote an article that I titled “Advantages of Making Your Methods Idempotent and Static in C#“, but I misunderstood idempotent. My article was really about “Pure Functions”, not “Idempotent Functions”. So in this article I attempt to make amends for putting something untrue on the Internet. I want to think Phil Atkins in Cambridge, UK for patiently and persistently helping me to realize the error in my original article.
Advantages of Pure Functions
When adding methods to classes, many developers spend little time deciding if the method should be a pure function. Making use of the properties in the class causes the method to not be a pure function, as in this example of the method named ReturnNumberOfDaysSincePersonWasBorn:
Here is the ReturnNumberOfDaysSincePersonWasBorn method re-written to be a pure function:
And here is another version of the method that is not a pure function. This time, the method is not a pure function because it is changing the value of a variable (Age) scoped outside of the method.
Here are the advantages to methods that are pure functions:
- The methods are easier to maintain. They are easier to maintain because a developer needs to spend less time analyzing the impacts of changes to the method. The developer does not have to consider the state of other inputs that are used in the method. When considering a change to a method that is not a pure function, the developer must think about the impact on properties used in the method that were not passed in.
- The methods are easier to test. When writing a unit test, it is easy to pass values into the method and write the test to verify the correct output. But writing tests on methods that are not pure functions take longer and are more complicated because more setup or injection of values is necessary for the test.
- The methods are easier to re-use. You can call the method from other places in the module, or call the method from several different modules easily when the method is a pure function.
- The methods are easier to move to other classes. If you have a pure function in one class and you want to move it to a utility class to be used by other modules, it is easy to do so.
- The methods are more likely to be thread-safe. Pure functions don’t reference variables in shared memory that are being referenced by other threads. Caveat: variables such as objects that are passed by reference could still experience threading problems when used inside of static methods that are pure functions.
Resist the Temptation!
When you encounter a case like the one below, where Method3() needs the BirthDate for a calculation, it can be tempting to change the methods from static to be non-static and to reference the BirthDate property in Method3(). The other alternative is to pass the BirthDate into Method1(), and from Method1() to Method2(), and from Method2() to Method3(). Although we don’t like to make those changes, doing so allows keeps the method as a pure function and keeps the advantages provided by pure functions.
In C#, if you are unable to mark your method “static”, then it is probably not a pure function. If the method makes database calls, API calls, updates properties outside of the method, or uses variables that have scope outside of the method, then it is not a pure function, but it may still be considered idempotent according to the non-functional definition of idempotent.
Advantages of Idempotent Functions
If you are not a functional programmer and you define idempotent functions as “functions that always return the same result for specific inputs”, then the benefits of idempotent functions are the same as the benefits of pure functions, except when they involve retrieving or update data in locations outside the function. I rarely think about idempotent functions from a functional perspective, but idempotent functions do provide the following benefits to intelligent code compilers.
- In some languages, if a compiler recognizes that calling abs(abs(abs(abs(x)))) will always return the same results of abs(x), it can substitute abs(abs(abs(abs(x)))) in the written code with the more efficient abs(x) in the compiled code.
- In some languages, if a compiler recognizes that a function is idempotent, it may be able to cache the result of the call to the function and provide the cached value in place of making calls to the function again with inputs previously used.
In functional languages, the primary advantages of idempotent functions do not apply to the developer writing and maintaining code, but rather to the compiler and performance of the program.