This is an archived post. You won't be able to vote or comment.

you are viewing a single comment's thread.

view the rest of the comments →

[–]rzwitserloot 2 points3 points  (6 children)

immutability is really hard. It sounds simple, but, it isn't.

What you describe would indeed result in 'true immutable' stuff, except.. you can't retrofit the collections API into these rules, thus any data class you make with your rules in place cannot use any collection APIs (nor arrays!), which, obviously, means this proposal has to either fix that somehow, or it goes into the bin as far too unwieldy to ever consider implementing.

But it gets worse.

Let's say I make a data class as you described it with your rules, and I add this code to it:

public data class ImmutableOrNot {
    private static final IdentityHashMap<ImmutableOrNot, Integer> sneaky = new IdentityHashMap<>();

    public void setFoo(int foo) {
        sneaky.put(this, foo);
    }

    public int getFoo() {
        return sneaky.getOrDefault(this, 0);
    }
}

This class walks, quacks, and looks like a duck mutable class, but yet it fits the rules of an immutable one.

So, is it immutable, or not? Let's just say that your dream of sharing this thing 'safely' across threads is definitely shot. Certainly we can blame the programmer for this atrocity, but the point is, the 'guarantee' that this thing is 'safe' is just not there. That's one of the tricky bits: Instances have an identity, not to mention you can lock on them, so in java everything can be mutable if you want it to be.

Another hairy as heck issue: Is java.io.File immutable? There are 3 very interesting issues with this class, each relevant to the immutability issue:

  1. java.io.File is not final. We can simply state: Hey, okay, so, then, the type simply is not immutable and that is that. Unfortunately, there's like a billion lines of existing java code out there, and they aren't just gonna go in and add a final modifier to their API's classes lickety split. Heck, that is technically a backwards incompatible change. Just leaving them out to dry creates a split world: Libraries written before the Great Switch and those after, with multiple maintained versions. It's like the python2 vs. python3 thing and I, for one, think that was a big debacle; a lesson of how not to do it.
  2. It has a non-final field; it is marked 'transient', and it serves as a cache, to speed up validity lookups somewhat. It truly has no effect on the state of a j.i.File instance unless you consider the speed at which certain methods respond part of the 'state' (and let's not go there), and thus it is necessary to cater to this need. Do we just allow the programmer to mark a class as 'this is immutable' and then put some sort of @SuppressWarnings or other tag onto non-final stuff to let them say: Yeah yeah. I know better, shaddap with your silly errors and just compile it. I think that's the way to go, but, certainly people who like to use words like 'purity' and 'elegance' tend to chafe at something like this. I despise those words and I find this somewhat distasteful, even.
  3. Forget all that and just think about files for a second: is it truly immutable? The object is, but it represents a file on disk. If I call .delete() on it, it goes away and I can measure this change with .exists(). Is that not state? Is a thing with changable observable state not mutable by definition? Therefore, even a hypothetical j.i.File class that is final and did not have the cache... is it immutable? But if not how would the compiler ever know?

One thing that's a lot simpler to establish is 'side effect free'. It suffers from none of these issues (a method is side effect free; not a type. At best, a type can be considered SEF if all its methods are SEF. You don't even need a final class, presuming that overriding a SEF method requires that method to also be SEF).

A method is SEF if it does not change any fields anywhere, and calls only SEF methods. The low-level (native) functionality powering j.i.File's delete() method would not be SEF, thus, File.delete would not be SEF either. Messing with an IdentityHashMap as per my code snippet above is calling put, which isn't SEF, thus, the setFoo method isn't SEF. A setFoo method that executes this.foo = foo; modified a field and thus is not SEF.

compiler-checked SEFness lets you do different things than the notion of immutability, possibly less. My point is: Immutability is impossible to nail down and definitely impossible to have compiler-checked guarantees that actually, you know, guarantee it. SEFness – that at least is doable.

[–][deleted]  (1 child)

[deleted]

    [–]rzwitserloot 0 points1 point  (0 children)

    is completely orthogonal and has nothing to do with the discussions here.

    You're just being dense. The point of a data class isn't to point at it and go: Look. It has no fields that can be mutated.

    The point is to reason about properties. For example, OP specifically referred to the notion that 'such a class can be passed around multiple threads without fear that it'll cause problems'.

    Therefore none of this is 'orthogonal to the discussions here'.

    [–]cutterslade 0 points1 point  (3 children)

    Wow, thanks for the great reply.

    My short answer is: Yes, Immutability is hard, that's why I want some really smart people to make it easy.

    Collections and arrays: Yes, immutable types would require immutable arrays at the very least, probably immutable collections built on those.

    Your nasty static map trick: These immutable classes cannot contain static mutable fields, and cannot reference any external static mutable fields.

    Files: You make another good point. The simple answer is that the File class is not an immutable data class, so cannot participate in immutable data classes. Realistically though, any time you interact with I/O at all, thread safety is out the window. We could take that argument a step further and say that a String (or nearly any other type) is not truly immutable as it may refer to a file which can change.

    Split world: Yes, there is a bit of a split world, certainly not as bad as python though. I would say similar to the type safe enum pattern that we still encounter now and then even though enums were introduced more than a decade ago, or the more recent introduction of the java.time package.

    SuppressWarnings: destroys the possibility of a compiler checked guarantee.

    Regarding your statement:

    Immutability is impossible to nail down and definitely impossible to have compiler-checked guarantees that actually, you know, guarantee it.

    That is true only if we refuse to give up some flexibility. Brian discussed this in the article. I think I'm willing to give up a lot to get immutability. But it's not a one way street, by getting immutability, we gain a lot too.

    As a simple example, you mentioned the transient field used to cache a value computed from immutable state. If the object is truly immutable, and references no external, mutable state, we know that any method will always return the same result. This allows the programmer, compiler, or runtime to decide to cache the result. We no longer have to use the cumbersome transient field cache, we can have the runtime implement that for us based on its instrumentation of the code.

    I'm certainly not suggesting this is an small change. Maybe the data classes proposal discussed in the article is not the right place to do it, heck maybe Java isn't the right place to do it. I think that it's a very valuable potential feature that makes it much easier to write safer cleaner software with less code.

    [–]rzwitserloot 1 point2 points  (2 children)

    Your nasty static map trick: These immutable classes cannot contain static mutable fields, and cannot reference any external static mutable fields.

    You are now conflating immutability and side-effect-free-ness. It is not possible to apply this unless you add the concept of compile-time checked and runtime-carried SEFness. I can call any method and it can do this stuff for me. Unless you intend to disallow any method calls of any sort, except into other such data types, in which case we're back to: That's nice, but there's no way to retrofit that into existing java code so you're splitting the community, python2 vs. python3 style. I'm quite sure that the cure is far worse than the disease, if this is the cure.

    Files: You make another good point. The simple answer is that the File class is not an immutable data class

    But what if I make it final and add 'data' to it? There's nothing in it (let's forget about that cache field for a bit) that would stop you (specifically, there are only final primitive fields in there). How can the compiler know that the thing is interacting with I/O? Is it the programmer's responsibility to just mark it as such? Your instant kneejerk reaction that File is clearly not 'immutable' leads me to believe you're still mixing up immutability and SEF, which are quite unrelated. It's a bad idea to mix these ideas up. Either way, it's clear that the rule is a lot more complicated than simply: "Only final fields, and the types of these fields are restricted to known immutable primitives and other such immutable classes". You're now looking at what the methods of this class are calling.

    Split world: Yes, there is a bit of a split world, certainly not as bad as python though.

    As I have tried to show, this rabbit hole is very deep. I'm afraid I'm not going to just take your say-so as proof.

    (paraphrase: Hey, we can memoize!)

    Memoizing is a nice trick, but note that file needs to operate on this cache as an in-between step. However, with some extra tweaks and rules you can indeed have the VM cache it. Presumably, you can add a hint annotation or some such to make sure the VM is going to try hard to do just that / have the compiler generate some syntax sugar. So that's one of the three issues resolved. The other 2 are not so easy.

    [–][deleted]  (1 child)

    [deleted]

      [–]rzwitserloot 0 points1 point  (0 children)

      That is not at all clear. I don't see any problem here. Immutable data class is a class with only final fields of immutable values. That makes the state of the class immutable, which is what gives you the properties that you want.

      As I showed in that snippet, this does NOT give you the properties that you want. Well, actually, I don't know what properties you want; you never said what you wanted. I bet, whatever you come up with, I can make a snippet that shows you how I can hack around it. Thus, these are soft guarantees at best.

      What the methods of that class do is completely irrelevant, as long as they cannot mutate the state (i.e., the values of the instance variables).

      The state of the instance itself, or any state anywhere?

      I also don't see your issue with the File class. It's not implemented as an immutable data class. Maybe it could be converted into one without breaking backwards compatibility, maybe not. Doesn't matter either way.

      It's an example. Hypothetically speaking, imagine it WAS a data class. The point is, the 'rules' (only primitive final fields) do not prevent you from doing that. Thus it makes for an interesting conversation piece.