In some cases, we can’t avoid the side effects, but we can structure them in such a way to reduce their impact on the overall structure of the system. In other words, we still get to have the side effects, but the extent at which they get to exhibit their behavior is constrained in such a way that makes our lives easier.
Idempotence is one such constraint that can help us turn what would otherwise be an unmanageable imperative algorithm into a manageable one.
In mathematics, idempotence is the property of a function which evaluates to the same result when applied to its
result as a parameter, i.e. where f(x) == f(f(x))
is true for any x
.
In computer programming, we can refer to the mathematical definition as idempotence with respect to the return value and we can list a couple of examples.
Constant functions, i.e. functions that ignore any of their parameters and always return a fixed value are idempotent in this sense:
let five = function(any, parameters, are, ignored){
return 5;
}
five(3)
//=> 5
five(five(3))
//=> 5
five(five(five(five(five(7)))))
//=> 5
Identity functions, i.e. functions that just return their parameter unchanged, are also idempotent in the mathematical sense:
let identity = function(param){
return param;
}
identity("something")
//=> "something"
identity(identity("something"))
//=> "something"
identity(identity(identity(identity("something"))))
//=> "something"
And so are functions that converge to some value:
let abs = function(number){
if(number < 0){
return -number;
}
return number;
}
abs(-6)
//=> 6
abs(abs(-6))
//=> 6
abs(abs(abs(abs(-6))))
//=> 6
All of these functions are idempotent in the mathematical sense. However, in computer programming we usually use a slightly different definition of idempotence, especially when we’re analyzing functions with side effects.
A function with side effects has two sorts of inputs:
- its input parameters
- the state of the outside world influencing the function
And two sorts of outputs:
- its return value
- a set of all changes it applied to the outside world, i.e. its side effects.
Now imagine we turn this upside down and consider only the state as the primary “input” and only the side effects as the primary “output” of a function. With that in mind, if we try to define idempotent functions we now get this:
Functions which are idempotent with respect to their side effects are such functions that always result with the same side effects applied to the outside world, regardless of how many times it was called with the same parameters.
Database interaction is a good example to illustrate idempotence, since the four elementary operations conveniently represent one of each typical cases. For the sake of simplicity, we will simulate a database with a simple in-memory object where keys serve as id’s and values as database rows.
const db = {}
let create = function(value){
//for readability, we're using a random integer
//in a real-world example, we'd prefer something better, like a UUID
const id = Math.trunc(Math.random() * 1000000000);
db[id] = value;
return id;
}
let get = function(id){
return db[id];
}
let remove = function(id){
delete db[id];
}
let update = function(id, newValue){
db[id] = newValue;
return newValue;
}
Here we have a few typical database operations:
create
- stores avalue
with a generatedid
and returns theid
for later referenceget
- given anid
, returns the associated value from the databaseremove
- given anid
, removes the associated value from the databaseupdate
- overwrites the record identified byid
with anewValue
We can try some basic usage examples:
//we can store a value in the db
//and get its generated id as the return value
let messageId = create("Hello there")
//bear in mind that the id is random, so you'll (likely) get a different value than this one
messageId
//=> 414426113
//then we can look up this value by id
get(messageId)
//=> "Hello there"
//we can also abuse the fact that we're using a plain JavaScript object
//and evaluate the whole "database" to see all its contents
db
//=> {414426113: "Hello there"}
//we can overwrite it with a different value
update(messageId, "Guttentag")
//and when we look it up again, we'll see the new value
get(messageId)
//=> "Guttentag"
//or we can remove the record
remove(messageId)
//so that when we look it up again, it's no longer there
get(messageId)
//=> undefined
Based on what we already know about function purity, we can see that:
- Functions
create
,remove
andupdate
have side effects. - Function
get
does not have side effects, but it depends on the global mutable state. - So, none of the functions are pure.
Now let’s see if they are idempotent with respect to side effects. We will call each function multiple times with the same parameters and then see if the number of times we called it influenced the side effects it caused.
Let’s start with create
:
create("Hello")
//=> 556193419
//and the database state is
db
//=> {556193419: "Hello"}
//if we call it again, with the same parameter
create("Hello")
//=> 551053811
//and the database state is now
db
//=> {556193419: "Hello", 551053811: "Hello"}
//the database now has two entries!
//and if we try to call it one more time:
create("Hello")
//not surprisingly, now we'll have three entries
db
//=> {556193419: "Hello", 551053811: "Hello", 876937018: "Hello"}
So, create
keeps generating new side effects every time we call it with the same parameters, from which we
can conclude that this function is not idempotent.
Let’s see how get
will behave when we call it with one of these id’s as the parameter:
//we continue with the previous database state
db
//=> {556193419: "Hello", 551053811: "Hello", 876937018: "Hello"}
//when we run it
get(551053811)
//=> "Hello"
//this function has no side effects
//so the db state is unchanged
db
//=> {556193419: "Hello", 551053811: "Hello", 876937018: "Hello"}
//and if we do it again, it will just do the same thing again
get(551053811)
//=> "Hello"
//still unchanged
db
//=> {556193419: "Hello", 551053811: "Hello", 876937018: "Hello"}
//and again
get(551053811)
//=> "Hello"
//and still unchanged
db
//=> {556193419: "Hello", 551053811: "Hello", 876937018: "Hello"}
//and again
get(551053811)
//=> "Hello"
//yep, unchanged
db
//=> {556193419: "Hello", 551053811: "Hello", 876937018: "Hello"}
So, it generates no side effects, regardless of how many times we call it, which means that get
is
idempotent. We already mentioned that all pure functions are also idempotent and from this
example we can generalize that even impure functions may be idempotent as long as they don’t generate
side effects.
How about remove
?
//continuing with the existing database state
db
//=> {556193419: "Hello", 551053811: "Hello", 876937018: "Hello"}
//we can remove the first entry
remove(556193419)
//and the state is now changed by the removal of that entry
db
//=> {551053811: "Hello", 876937018: "Hello"}
//if we try removing the same entry again
remove(556193419)
//the state will now be unchanged
db
//=> {551053811: "Hello", 876937018: "Hello"}
//which makes sense - we don't need to be removing something that's already removed
//we can keep calling the function again and again as many times as we want
remove(556193419)
//and the state will remain unchanged
db
//=> {551053811: "Hello", 876937018: "Hello"}
We can see that remove
is idempotent, since we get the same side effect regardless of how many times we call it -
exactly one entry with the given id is removed.
We’ll see that update
also fits the idempotence criteria:
//continuing with the same database state
db
//=> {551053811: "Hello", 876937018: "Hello"}
//if we update the first entry with a different value
update(551053811, "Bonjour")
//the state will be updated to reflect the change
db
//=> {551053811: "Bonjour", 876937018: "Hello"}
//and if we try updating it with the same value again
update(551053811, "Bonjour")
//the state remains the same
db
//=> {551053811: "Bonjour", 876937018: "Hello"}
The functions in this example are equivalent to another example that is often used to illustrate idempotence - HTTP methods:
POST
- not idempotent, equivalent to ourcreate
function.GET
- idempotent, in principle does not have side effects, equivalent toget
.DELETE
- idempotent, equivalent toremove
.PUT
- idempotent, equivalent toupdate
.
Now we know what idempotence is, which brings us to the next big question - Why are they important?
If we must have side effects, it is useful if they at least play by some rules which makes them predictable. Idempotent functions are predictable in the sense that we know that the side effects we get from the first function call are all the side effects we’ll ever get for the same parameters. That, in turn, means that any subsequent calls are guaranteed to produce no side effects at all.
Effectively, idempotent functions are safe to retry.
Imagine that we’re calling some HTTP API endpoint, e.g. one that lets us create, update, delete and get notes. We can send a request to update a note like this:
PUT /api/note/{id}
updated note contents
Now, let’s say that we sent this request over a congested network, the request times out and we are now not sure if the update was completed or not. I realize that there is a lot of mechanisms that we can use provide visibility over request completion, but just bear with me for the sake of an example and assume that network issues can cause a request to fail in a way that makes it hard for us to know if the request completed or not
Since this method is idempotent, we can just try it again without any concern:
PUT /api/note/{id}
updated note contents
If we’re lucky this time, it might complete fast enough to give us a response confirming so and we will know for certain that the note has been updated. We won’t know in which run it was updated though, but that’s not a big deal. If it’s been completed the first time, the second request will do nothing. And if the first request failed to reach the server, it was the second request that actually updated the note.
In any case, it was the fact that this method is idempotent that made it safe to retry. Inversely, non-idempotent methods are not safe to retry, e.g. this POST endpoint will create a new note:
POST /api/note
some new note
Running it again will just create another note with the same contents. If we then run it three more times, we’ll get three new notes and so on.
So, if we are not sure if a POST request completed or not, we can’t just re-run it. Because if we did, and it turns out that the first request was also successful, we’ll end up with two duplicate notes. One does not simply retry POST requests!