Hacker News new | ask | show | jobs
by cbsmith 1312 days ago
> When I write code, it's common to have references to immutable classes thrown around with wild abandon, heedless of ownership, threads, or good taste, because the data just can't change.

If there's anything I wish languages with implicit reference semantics would adopt, it's implicit immutability. I wish Java would be so much nicer with keyword that is half way between "final" and "volatile" that means, "yes, you can actually mutate this" and then make final semantics the default for fields & variables.

1 comments

Agreed.

Could you Imagine a Java where you have a `Map` and a `MutableMap` and that's what you put at your API? I'd make it SO much clearer how safe any individual API is to call.

Scala has had this for ages. You can have it today. Even in Java either through the Google collection library or through a library that mimics fp style programming. The name eludes me for the moment.
Guava, but it's not fully typesafe.

Mutable is not part of the Java meta-language used for typing.

ImmutableMap implements Map

https://guava.dev/releases/23.0/api/docs/com/google/common/c...

and throws exceptions on mutation.

https://guava.dev/releases/23.0/api/docs/src-html/com/google...

In general this doesn't work, the history rule says mutable types are not proper subtypes of immutable ones (and the converse is obvious). If you want to capture mutability in your type system, it needs to be orthogonal to subtyping (like C/C++ const).
C++ still has this problem - std::unordered_map<std::string, std::string>` and `std::unordered_map<std::string, const std::string>` are basically unrelated types - you can't const-cast the templated const away. (I may be misunderstanding here)
> you can't const-cast the templated const away.

That seems like a good thing. If you're handed a map to const values you can't just go "imma gunna mutate them anyway".

Yup, it's definitely a bit of a code smell if you do. The issue is more the reverse, though - I can't make a mutable map, then hand it by pointer/reference to something that says it wants an immutable map later.
Not necessarily a bad thing either, things can get odd if you're not the only owner and it's mutated under you unexpectedly.
Normally (when containers are not involved) this is exactly the point of a const cast.
No, the primary point of a const cast is "I need to pass this to an API that expects a non-const pointer even though it won't mutate it".

Casting const away and then mutating is a footgun as you are in undefined behavior territory as soon as your pointer/reference is not just const itself but points to a const variable.

> the history rule

I'm unfamiliar with this rule (and not finding anything good to google). Can you elaborate?

I can't really think of a scenario where an immutable datastructure isn't a subset of actions against a mutable datastructure.

I had to look it up too, it apparently is a constraint for subtypes defined in Liskovs substitution principle [1]. From Wikipedia:

> History constraint (the "history rule"). Objects are regarded as being modifiable only through their methods (encapsulation). Because subtypes may introduce methods that are not present in the supertype, the introduction of these methods may allow state changes in the subtype that are not permissible in the supertype. The history constraint prohibits this. It was the novel element introduced by Liskov and Wing. A violation of this constraint can be exemplified by defining a mutable point as a subtype of an immutable point. This is a violation of the history constraint, because in the history of the immutable point, the state is always the same after creation, so it cannot include the history of a mutable point in general. Fields added to the subtype may however be safely modified because they are not observable through the supertype methods. Thus, one can define a circle with immutable center and mutable radius as a subtype of an immutable point without violating the history constraint.

[1]: https://en.wikipedia.org/wiki/Liskov_substitution_principle#...

Just curious, isn't it obvious from a logical standpoint? I don't see how one could consider a mutable type to be a subtype of an immutable one. On the other hand, an immutable subtype of a mutable one seem plausible?
Immutable from a mutable is just as implausible. You cannot remove a method from a subtype which results in mutating methods being disabled through other means (like throwing exceptions).

This is also a violation of LSP and the open close principle.

Consider a `List` with an `add` function. What would you do with that `add` function to make an `ImmutableList` subtype?

Not quite.

Conceptually, you can have constness in your subtype-system (as long as you are sticking with interfaces (methods), as Liskov's subtyping model does, and aren't inherting potentially-mutable fields).

MutableMap and ImmutableMap are both subtypes of a hypothetical ReadableMap. ImmutableMap is the same as ReadableMap, but has an informal contract that subclasses shouldn't add mutability.

Kotlin has this
Kotlin has this, but the Map is (usually) a MutableMap under the covers, because it's Java bytecode at the lower levels. You have to go out of your way to footgun yourself, but it's still possible.