I am confused about the notion of immutability. Consider the following structure of a simplistic calculator:
Class diagram
Here are the interfaces:
interface IOperationalInterface {
int Sum(int a, int b);
}
interface IAuditInterface {
int SumInvocations();
}
IOperationalInterface.Sum
should compute the sum of two integers and IAuditInterface.SumInvocations
should return the total number of received Sum
calls.
Here is a trivial implementation:
class CalculatorImpl : IOperationalInterface, IAuditInterface {
private int invocations = 0;
public int Sum(int a, int b) {
invocations++;
return a + b;
}
public int SumInvocations() {
return invocations;
}
}
Is CalculatorImpl
immutable? Obviously not, because its state changes with each invocation of the Sum
method. Is the Sum
operation pure? According to Wikipedia it is not, since it changes the state of a mutable object, and that side-effect is observable through the IAuditInterface
. Obviously the SumInvocations
operation is also not pure, since it may return different results.
In summary, CalculatorImpl
is mutable and all of its methods are impure.
However, from the viewpoint of CalculatorClient
, who talks to it only through the IOperationalInterface
, it appears to be immutable and the Sum
operation appears to be pure in the sense that it could not observe any side effects through that interface.
On the other hand, from the perspective of AuditClient
, it is completely different: it is obvious that the object implementing the AuditInterface
is mutable and its SumInvocations
operation is impure, and this follows directly from the specification of the IAuditInterface
.
So, it is possible to partition an interface/specification of a mutable class such that some parts of it will appear mutable and some not. In this case, considering only the Sum
operation and leaving out the requirement that the invocations should be counted, we get something that does not have any side-effects.
Now, on implementing the CalculatorClient
one can take into account the fact that the object behind the interface appears to be immutable. At least one could not tell the difference.
So my question is: does it make sense to talk about "immutability" of interfaces or is it a bad idea? How else can I communicate the fact that there will be no observable side-effects through that interface? And if it's bad, what could go wrong?
UPDATE
Thanks for your answers/comments! I see now that there is no way to say that the IOperationalInterface
is pure; the conditions of purity are much too strong to apply in this case. However the question remains whether there is a weaker notion (maybe "immutability"?) which is applicable.
4 Answers 4
However, from the viewpoint of CalculatorClient, who talks to it only through the IOperationalInterface, it appears to be immutable and the Sum operation appears to be pure in the sense that it could not observe any side effects through that interface.
Whatever you can observe through the interface is irrelevant to the concept of purity. If Sum
logged results to a file, it would still be impure even though you wouldn't be able to observe any changes through the interface.
Purity is a very strong condition. If you say that something is pure, I'll take you at your word and assume nothing bad will happen if I run it from multiple threads, cache the results and only call it once for any given set of inputs, or that I can call it 1,000,000 times without exhausting the OS's file handles. Those assumptions can backfire when using CalculatorImpl
.
There's nothing wrong with partitioning the interfaces the way you have, but you should be precise. The specification you want for IOperationalInterface
is probably closer to: "The return value of these methods must depend only on their arguments, but their execution may have side effects." You can be more specific about the side effects and say that the methods must not perform any kind of I/O. But I wouldn't say that it's pure, because you're very clearly not using it that way.
-
Ok, then maybe there is a weaker notion than purity? For example, something like "the operations always return the same result for the same arguments and the order of invocations does not matter"?proskor– proskor07/31/2014 13:51:45Commented Jul 31, 2014 at 13:51
-
@proskor Yeah, that sounds much better. There's probably no name for that concept but that's fine. You might want to specify whether the number of invocations matters though. Will something break if I decide to cache the results?Doval– Doval07/31/2014 13:53:16Commented Jul 31, 2014 at 13:53
-
3@proskor There is, it is called Determinism. You can say
IOperationalInterface
is Deterministic.RMalke– RMalke07/31/2014 16:25:49Commented Jul 31, 2014 at 16:25 -
@RMalke Good idea, but aren't all functions deterministic (except, maybe, random number generators and IO)?proskor– proskor07/31/2014 16:48:14Commented Jul 31, 2014 at 16:48
-
@RMalke: Does "deterministic" have a useful meaning here? I was thinking about that word, but one could quite convincingly argue that the object itself is also part of the function's input (and, particularly since it's modified, output).cHao– cHao07/31/2014 16:49:45Commented Jul 31, 2014 at 16:49
You're sort of getting hung up on definitions without worrying about why you care if a function is pure or not. A pure function gives more freedom to the caller at the expense of restrictions on the implementer. A function with side effects gives more freedom to the implementer at the expense of restrictions on the caller.
Most everyone is familiar with the restrictions on the implementer of a pure function, but many are not familiar with the freedoms it grants the caller. It frees the caller to cache results and not call the function a second time. It frees the caller to create as many copies of the object as they want, perhaps on different threads or even different systems, to speed up a computation. It frees the caller to not have to worry about keeping the object in scope, because they can always recreate an identical one later.
If you're not going to grant those types of freedoms to the caller, it does no good to label an interface immutable. You're putting restrictions on the implementer without any commensurate benefits to the caller, so you may as well lift the implementer restrictions.
-
This is what I meant by saying that on implementing the CalculatorClient one can take into account the fact that the object behind the interface appears to be immutable. For example, assuming that the operational interface is "immutable", it is easier to make the client implementation thread-safe. However if the interface is "mutable", it is more difficult or even impossible. But again, the question is whether there is such thing as an "immutable" or "pure" interface.proskor– proskor07/31/2014 13:45:23Commented Jul 31, 2014 at 13:45
-
But you can't take it into account, because it isn't really immutable, and isn't easier to make thread safe, because
invocations++
isn't guaranteed to be atomic, and you don't have the freedom to make and destroy copies at will, because you'll lose the count. Something that appears to be immutable but really isn't is worse than not being immutable at all.Karl Bielefeldt– Karl Bielefeldt07/31/2014 14:39:23Commented Jul 31, 2014 at 14:39 -
To explicitly answer your question, yes pure interfaces exist, at least as a gentleman's agreement if not enforced by compiler, but there's no such thing as a pure interface with an impure implementation. As soon as you create a side effect, you would be breaking the contract of the pure interface and therefore technically no longer implementing it.Karl Bielefeldt– Karl Bielefeldt07/31/2014 14:39:46Commented Jul 31, 2014 at 14:39
-
@proskor: The appearance of immutability doesn't do much at all for thread-safety, if the implementation actually has side effects. Thread safety requires that mutations either not occur at all, or be done in such a way that concurrency can never cause the system's state to be incorrect. (In your example, since
invocations++
is not atomic, two threads using aCalculatorImpl
-- even if only as anIOperationalInterface
-- can cause the count to be incorrect.)cHao– cHao07/31/2014 15:28:38Commented Jul 31, 2014 at 15:28 -
1@proskor: What you seem to want would probably be better described as "consistency" or "self-consistency". "Immutable" and "pure" are strong words, not used lightly (and almost exclusively reserved for implementations). The former applies to the whole object (not just the interface), and the latter makes promises about a function's (lack of) effects on any part of the entire system.cHao– cHao07/31/2014 16:38:05Commented Jul 31, 2014 at 16:38
The implementation is clearly not immutable, so let's talk about the "pureness" of the Sum
function.
It can only make sense to talk about the implementation, not the interface. For instance, what if I created a new implementation of the interface with this Sum
implementation?
public int Sum(int a, int b) {
invocations++;
if(invocations >= 1000)
{
return a + b + 1;
}
return a + b;
}
The interface hasn't changed, but the method is no longer "pure".
In a purely technical sense, even your naïve implementation isn't pure because eventually invocations
will overflow and you'll get an exception when you call Sum
instead of the expected result.
-
This is not only impure, this is simply incorrect. The specification for
Sum
was that it should compute the sum of two integers, and this implementation cleary does not comply to it. The possible overflow is a technical implementation issue, it has nothing to do with the problem: it is possible to restrict the specification or change the implementation accordingly such that overflow does not occur.proskor– proskor07/31/2014 11:54:57Commented Jul 31, 2014 at 11:54 -
@proskor - your example was just an example, as was mine. Your comment about the contract of
Sum
is correct, but your comment about the overflow issue is not correct. How can you have aSum
function with a contract that says you can only call it a certain number of times? Some (n)th call will eventually throw an exception, so it's not a pure function (it won't give you the same result every time you call it). At any rate, this is all about implementation, not interface, which is my point.Scott Whitlock– Scott Whitlock07/31/2014 12:05:36Commented Jul 31, 2014 at 12:05 -
What I meant is that the count of invocations could overflow, i.e. after the (n)-th call it could be reset to 0 instead of throwing an exception. The
Sum
operation remains a pure function. But even that is a marginal issue.proskor– proskor07/31/2014 12:09:53Commented Jul 31, 2014 at 12:09 -
2@proskor - Ok, what if my implementation logged the
Sum
inputs and result to a file, and then I got some kind of out-of-space or file-not-found kind of exception? Sure, I could wrap it in acatch
block. The point is that the purity of it depends entirely on the implementation. I could actually makeSumInvocations()
pure by always returning42
. It wouldn't be correct, but neither would your implementation if used in a multi-threaded environment. Again, it has nothing to do with the interface, only the implementation.Scott Whitlock– Scott Whitlock07/31/2014 12:17:21Commented Jul 31, 2014 at 12:17
I believe what you want is a variant of Design by Contract (C# interfaces are a poor man's version of them). In C# it could be achieved via custom attributes and a static analyzer extension (e.g. for Roslyn or ReSharper):
[Pure] //This is just a sample name, not to be confused with System.Diagnostics.Contracts.PureAttribute
interface IOperationalInterface
{
int Sum(int a, int b);
}
Your custom static analyzer extension would then check if implementation(s) are side-effect free across all the interface methods. This might get complicated depending on the level of guarantee you would like to achieve (e.g. simply checking there are no mutating operators used versus walking any method invocation chains).
If you find this approach viable for your scenario, then consider checking existing contract-related tools for .NET (non of them works well though):
JetBrains: http://www.jetbrains.com/resharper/webhelp/Reference__Code_Annotation_Attributes.html Microsoft: http://research.microsoft.com/en-us/projects/contracts
-
Yes, this is what I had in mind, however I am not sure whether the notion of a "pure" or "immutable" interface is sound. There are opinions that this is not the case, but I am not convinced yet. :)proskor– proskor07/31/2014 13:16:40Commented Jul 31, 2014 at 13:16
-
@proskor The concept is sound. Pure/immutable interface is an interface with an added contract of no side effects guarantee.Den– Den07/31/2014 13:20:53Commented Jul 31, 2014 at 13:20
Explore related questions
See similar questions with these tags.
a
andb
, any two invocations ofSum(a, b)
must return the same value"), that's valid. But "this method must not have any observable side effects" is invalid, IMO.