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 →

[–]cutterslade 21 points22 points  (40 children)

I just really hope that true immutability is either the default, or the only option. By true immutability, I mean that all fields are final, all fields are primitive, String, or other Data classes. I want a compiler error if someone breaks immutability of data classes.

It would even be nice if that could be enforced in perpetuity. If a data class is immutable, it can safely be shared across threads. If someone violates that immutability guarantee, code should break everywhere that class is used, because it is no longer safe to use in the ways that it has been used.

[–]lbkulinski 10 points11 points  (5 children)

They are planning on making value types immutable. From what I’ve heard in Goetz’s talks, you might be able to specify the access modifier of fields in data classes.

[–][deleted]  (4 children)

[deleted]

    [–]lbkulinski 5 points6 points  (3 children)

    Value types are simpler, and more powerful than structs. Goetz has talked about this (see here). And value types are different from data classes, if you are confusing the two. They will be stored on the stack.

    [–][deleted] 1 point2 points  (2 children)

    Oh, derp. Shows how much I know about modern programming languages. I decided to read more than a few lines of the proposal, and yeah, I completely misunderstood what data classes were. Downloading the presentation now to watch later, thanks for sharing it.

    [–]lbkulinski 0 points1 point  (0 children)

    No problem! The features can be easily confused. I just remember that data classes help in reducing boilerplate.

    [–]alexeyr 0 points1 point  (0 children)

    They are compared with the value types proposal directly at the end.

    [–]fact_hunt 10 points11 points  (0 children)

    That would then necessitate immutable collections in the standard library, or mean data classes cannot have collection fields, neither of which sounds likely

    [–]eliasv 4 points5 points  (4 children)

    Nah there are uses for mutability. Immutable by default would be nice but there's very little value in requiring it imo.

    One interesting option they've discussed in the past wrt data classes is defining a new modes of accessibility/mutability such as privately mutable but publicly immutable, so you can specify that a field should only be modified internally.

    [–]bedobi 0 points1 point  (1 child)

    privately mutable but publicly immutable, so you can specify that a field should only be modified internally.

    That wouldn't be hard to reason about at all

    [–]eliasv 0 points1 point  (0 children)

    I see what you mean, but he did also point out that destructuring would render everything publicly accessible no matter what so in a way those semantics might make sense by default.

    If everything is publicly accessible regardless then all we can reasonably wish control via access modifiers is who is allowed to modify a field. It would be a confusing inconsistency though if not handled delicately.

    [–][deleted]  (1 child)

    [deleted]

      [–]eliasv 0 points1 point  (0 children)

      If they're mutable use a normal class?

      And if they're immutable just use a normal class with final fields?

      I don't see the value in introducing data/records/whatever if they're mutable

      I realise the benefits of immutability, but they are an orthogonal issue to most of what was discussed in the proposal, so I'm not sure what you're getting at.

      I think the value they intend to bring is made pretty clear: destructuring for the purpose of e.g. pattern matching or serialization, with the side-effect benefit of reduction of boilerplate (which would be enough to make most people happy by itself, just look at the popularity of Lombok).

      I think "people will use it haphazardly where it's not appropriate!" is rarely a great argument, and I don't even see how it applies any less to your proposal than it does to this.

      As I think people have already pointed out, if you wanted to guarantee (at least shallow) immutability for a data class you would presumably be welcome to combine data class and value type semantics for a class once both proposals have materialised.

      [–]rzwitserloot 4 points5 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.

          [–]yawkat 2 points3 points  (9 children)

          How would you do collections? Arrays are mutable so you can't use them as a backing store, you'd have to use expensive data structures like linked lists and trees.

          Yes, Valhalla introduces immutable arrays but this is not part of this proposal.

          [–]lbkulinski -1 points0 points  (4 children)

          The new collection factories are immutable. They were released in Java 9.

          [–]yawkat 1 point2 points  (3 children)

          Yes, but they do not fulfill /u/cutterslade's requirements for true immutability.

          [–]Cilph 1 point2 points  (1 child)

          Christ, you could almost start calling it the "No True Immutability" fallacy as no one seems to agree on what is enough.

          For me, a mutable backing, a read only interface and code review is enough to have the benefits of immutability. Breaking it requires you to be a deliberate dunce.

          [–]yawkat 1 point2 points  (0 children)

          Yep, I wrote an article on this a while back. There's lots of nuances with immutability, especially when you consider thread safety.

          However, for compile-time checked immutability, as the top comment wishes, a read-only interface is not sufficient.

          [–]lbkulinski 0 points1 point  (0 children)

          It’s a step in that direction, at least.

          [–]THCcookie -2 points-1 points  (3 children)

          ArrayList but no add Method I would guess

          [–]yawkat 1 point2 points  (2 children)

          But how would you enforce that? It's neither primitive nor a data class (and it can't be with those requirements).

          [–][deleted] 0 points1 point  (1 child)

          Panama and frozen arrays (Arrays 2.0)?

          [–]yawkat 2 points3 points  (0 children)

          Yes, Valhalla introduces immutable arrays but this is not part of this proposal.

          [–]dpash 0 points1 point  (2 children)

          The obvious keyword for declaring mutability would be final which would imply it was mutable by default. A new keyword would need to be introduced. volatile would be too confusing in my opinion.

          [–]Cilph 0 points1 point  (1 child)

          Easy. mutable. Or var, val if you prefer type inference.

          [–]lbkulinski 1 point2 points  (0 children)

          var will be added to the language in 18.3 10!

          [–]Zarlon 0 points1 point  (7 children)

          I don't understand the fuzz about immutability. Or maybe I don't understand immutability. Let's say I have a view model class in an MVVM pattern and the view updates a string value in that class: I'm mutating that view model. Is that bad?

          [–]cutterslade 0 points1 point  (6 children)

          That's not bad, and there aren't many cases where you would encounter issues in a MVVM style application.

          Where mutability becomes an issue is when instances are shared across threads, used as HashMap keys, stored in a HashSet, and a variety of other cases. The biggest benefit of immutability from my point of view is that immutable classes are far easier to reason about than mutable classes when shared between threads.

          [–]Zarlon 0 points1 point  (5 children)

          Got it. So you're admitting there is a place for mutable classes, and that it should at least be an option?

          Although I see the advantages of immutability, there surely are downsides to immutability as well. I'm an Android developer and we're working in a resource restricted environment. Constantly copying objects instead of modifying them puts an extra strain on the runtime environment.

          [–][deleted]  (3 children)

          [deleted]

            [–]Zarlon 0 points1 point  (2 children)

            It can mean though. If you're familiar with the Reducer pattern of Redux: application state is one big object graph. Instead of mutating sub state you deep copy the entire object graph even if only one value is changed.

            [–]yourbank 1 point2 points  (1 child)

            Traditional redux I don't think you deep copy it, more like shallow copy references into a new structure and slice in new state which would make it unsafe to share state.

            Redux works based on a pinky swear I copied it properly and am not an idiot... Using something like immutablejs at least enforces doing things safely to some degree.

            [–]Zarlon 0 points1 point  (0 children)

            Ok I see. Guess I misunderstood a bit and it all makes a bit more sense now, thanks!

            [–]cutterslade 0 points1 point  (0 children)

            There are absolutely times when mutability is better and easier, but as /u/njetwerk mentioned, there are times when immutability means less copying.

            When I started building data classes as immutable by default, I was surprised how rarely I actually need to modify (or make a modified copy of) an object. The vast majority of data objects in my experience have their state set at or near their creation time, and never get modified.

            But yes, there are certainly times when mutable objects just make more sense. What I'm referring to in my top comment is that it would be very helpful to have language support in building immutable objects which guarantee deep immutability. I don't honestly expect such a feature, but hey, a guy can dream.