5 ms·
I hope they will find a better solution, as this proposal will break encapsulation. Let’s say you model allowed transitions via business methods and thus restr
by ivan_gammel 4y ago
I hope they will find a better solution, as this proposal will break encapsulation.
Let’s say you model allowed transitions via business methods and thus restrict certain state changes. You can either serialize or deserialize state or perform an allowed transition. If wither becomes a language feature, it will allow forbidden state changes that cannot be caught by validation in constructor, which will allow deserialization of states A, B, C, but will not know that A->C is forbidden, thus making the following possible:
var r = new R(A);
r = r with { state = C; } // passes, resulting in untraceable error much later
where alternative could be:
r = r.transitionTo(C); // throws exception
- deleted 4y ago[deleted]
- logicchains 4y agoThe proposal is meant for records, and records are used for "plain old data", explicitly without any encapsulation.
- ivan_gammel 4y agoNope. If records were structures, they would not be allowed to have methods or overloaded constructors or even default constructor. The most obvious example of encapsulation in records is validation of state.
- kaba0 4y agoThey still don’t have encapsulation, all members are readable on purpose. Withers will likely go through the same default constructor so constraints can be upheld, though.
- ivan_gammel 4y ago>They still don’t have encapsulation, all members are readable on purpose. Public state does not mean no encapsulation. The purpose of encapsulation is to bundle state and behavior and hide them both behind an interface, but that interface can offer read access to state. The key here is behavior. >constraints can be upheld My example above demonstrates a constraint that cannot be implemented in a constructor.
- plumarr 4y agoIt can't but that's not due to the proposed solution for withers. The cause is Records themselves. Record are immutable on purpose and you want to add a mutation constraint whish will not play nicely. The proposed constraint will be triggered in this case : var r = retrieveFromDb(); (value is A) r = r.with(C) -> throw an exception But this only work if B is generated by the wither. If I deconstruct the record manually and reconstruct with C manually, it'll work. So this offer no garantee that this transition will never occur. I would even argue that this constraint can't be implemented at the level of the class, at least if the class is only a data carrier without external dependencies.
- ivan_gammel 4y ago>>My example above demonstrates a constraint that cannot be implemented in a constructor. >It can't but that's not due to the proposed solution for withers. This has nothing to do with withers. It can't be implemented simply because it is a constraint on a specific transition of state. There's no transition of state in constructor. >Record are immutable on purpose and you want to add a mutation constraint whish will not play nicely. The purpose of immutability is not to create constant objects, but to prevent side effects from sharing mutable objects: state modifications are possible, they are simply reflected in a modified copy of object. That also means that my argument stands also for entities modeled as classes with final fields, it has nothing to do with specifics of records. Indeed, the fact that we have a constructor from which we can build any valid state and that we can deconstruct an immutable object means that we can bypass transition validations, but that will require some extra effort from developer and explicit demonstration of intent compared to simply using `with` block. Compare this: var rA = new R(I1, I2, I3, A); // deserialization, e.g. from persistent state // verbose, explicit intent to create a copy in state C var rC = new R(rA.i1(), rA.i2(), rA.i3(), C); // error occurs later this: // no semantics, no validation var rC = rA with { state = C; } // error occurs later and this: // clear semantics, validation of transition var rC = rA.onSomethingHappened(C); // exception thrown now
- Ironlink 4y ago
- wtetzner 4y agoI don’t believe this is true. From my reading of the document, all with expressions go through the primary constructor.
- ivan_gammel 4y agoWell, that is exactly the problem, as I already explained a few times in this thread. Constructor cannot validate state transitions, so the “with” statement or the block surrounding it will have to do it, breaking encapsulation and likely requiring multiple copies of this code.
- wtetzner 4y agoBut if that’s the case, then a record is the wrong data type for what you’re doing anyway, since you can call the primary constructor at any point with those same un-validatable values. It’s not “with” that’s problematic. If a record doesn’t work for what you’re trying to do, just use a class. Though I will say that this is why I generally think ML’s (i.e. OCaml, Standard ML) approach to encapsulation with modules is generally superior to using classes for encapsulation.
- ivan_gammel 4y agoHave a look at the proposal. Half of it is dedicated to regular classes, which makes sense, because record is just a syntactic sugar in Java at the moment. Also this: https://news.ycombinator.com/item?id=34398982 https://news.ycombinator.com/item?id=34398982
- pron 4y agoWithers invoke the constructor. E.g. r = r with { state = state + C; }; compiles to: r = new R(r.state() + C); All state transitions of ADTs can be checked in constructors.
- ivan_gammel 4y agoState transition cannot be checked by constructor because it doesn’t know previous state.
- pron 4y agoAh, I see what you mean. It's not about that proposal, but about the fact that record constructors cannot be private (at least not for a public record). That's because records are meant as product types, or nominal tuples, which don't break encapsulation but exist to represent the notion of an unencapsulated tuple. That's their job: to be unencapsulated data with everything that enables. Now, you may ask why they don't also do other things, and I guess its possible that in the future we'll allow private record constructors, but there's less need for that, because Java already has a construct for encapsulated state -- ordinary classes.
- ivan_gammel 4y agoRegardless of what records were meant to be, they are designed in a way where they do offer encapsulation (which by definition means not only the state, but also the behavior of an object, and it does not mean that we always hide the state - only that we control the interface to it). Records can have business methods and can have non-trivial constructors, so they are basically a syntax sugar for special form of classes, not tuple structures. If you can use them to encapsulate certain forms of behavior, e.g. by declaring methods for state transitions, then with{} block will be a change in their interface. Do I expect that new version of language will change interfaces of my data structures? Hell, no! This feature must be designed as opt-in solution, following the example of Iterable and foreach loop and requiring explicit declaration of wither interface. What kind of declaration could that be? Imagine uniform declaration of intent for records and classes like in this example: public record Point with (int x, int y) {} public class Order { public Order() with (OrderStatus status, Instant timestamp) { … } public @(OrderStatus status, Instant timestamp) { … } } Here we explicitly say that Point generally supports „with“ block for all fields and Order supports deconstruction to status and timestamp and construction of a new object with the same fields. This way existing code retains the interface but can be easily modified to support the new syntax.