I just realised I sent my last reply to Anthony only... apologies. Please see below.
Cheers, Abraham El mié., 19 jun. 2019 a las 17:43, Abraham Marin-Perez (< abraham.marin.pe...@gmail.com>) escribió: > Hi Anthony, > > There is code before and after compareIfPresent, but yeah, we could have > reordered that code and include an isEmpty check to avoid the problem. > However, let’s take a step back and have a look at what this entails. > > Having documentation is great, needing documentation not so much. Whenever > possible, methods should have a name that describes in plain English what > they do so as to minimise the need to go and check the documentation. This > makes code intuitive. Let’s analyse the plain English for computeIfPresent. > Compute if present. It doesn’t say what happens if not present, but one > would argue that a plain English interpretation would be “it not present, > nothing happens”. > > We’ve already seen that there are some powerful reasons for which this is > not quite the case (as Roger laid out in detail). Reasons that lead us to > the unfortunate situation that the plain English interpretation is not > quite correct, but calling it > “computeIfPresentAndIfNotNothingHappensUnlessTheMapIsImmutable” would be > absurd, so we live with that. The side effect is we have a method whose > behaviour causes confusion to developers and forces them to check the > documentation. > > Once the developer has checked the documentation, then we need to adapt > the code and add some guards that, on face value, shouldn’t be needed. Why > should you need to check whether a map is empty before attempting > computeIfPresent? You shouldn’t, the fact that you do is just a side effect > of the an implementation decision. > > So my point is: we could definitely do what you suggested, but I am not > entirely sure that such measures should be needed. > > Cheers, > Abraham > > Sent from my iPhone > > On 19 Jun 2019, at 11:58, Anthony Vanelverdinghe <anthonyv...@outlook.com> > wrote: > > Hi Abraham > > > > Wouldn’t it have been possible to use Map::isEmpty? Ideally just returning > at the very beginning of your method if `data` is an empty Map. That way > you’d still be able to use computeIfPresent, and use emptyMap() as well. > > > > Kind regards, > > Anthony > > > ------------------------------ > *From:* core-libs-dev <core-libs-dev-boun...@openjdk.java.net> on behalf > of Abraham Marín Pérez <abraham.marin.pe...@gmail.com> > *Sent:* Wednesday, June 19, 2019 9:51:46 AM > *To:* Roger Riggs > *Cc:* core-libs-dev@openjdk.java.net > *Subject:* Re: Suggestion for a more sensible implementation of EMPTY_MAP > > Hi Stuart, Roger, > > First of all, thank you for such a detailed response, this really shows > the big picture. I guess no implementation will be perfect, there will > always be some wrinkles that we need to accept, and the most suitable > implementation will be the one with the fewest or least impacting wrinkles. > Given this, maybe showing the particular wrinkle that I faced can shed > light on the overall development experience that the current implementation > provides. > > For multiple reasons that I cannot explain at this point, I have a method > that looks like the following: > > private void decorate(Map<String, String> data) { > //... > data.computeIfPresent("field", (k, v) -> highlightDifferences(v, > otherValue)); > //... > } > > At one point an emptyMap() was passed to this method, causing an UOE. This > left me with two choices: > > 1. Change code to: > > if(data.hasKey(“field")) > data.compute("field", (k, v) -> highlightDifferences(v, otherValue)); > > 2. Avoid usage of emptyMap() and use new HashMap<>() instead > > > The first option defeats the purpose of having a computeIfPresent method, > and causes confusion among team members (people keep asking why > computeIfPresent isn't used). The second option is pretty much what Roger > mentioned (and what we ended up doing). > > On the other hand, there are some points that you mentioned that I believe > merit some extra debate, please find some comments below inline. > > In general, I tend to agree that a stricter implementation is better since > it usually prevents bugs. However, at least in this particular case, I > believe a stricter implementation also has the side effect of worsening the > development experience and adding confusion (see below comments). Of > course, as I mentioned before, I understand that no implementation will be > perfect, and maybe the current option is the lesser evil, but I wanted to > throw in an alternative for consideration. > > Many thanks, > Abraham > > > El vie., 14 jun. 2019 a las 14:18, Roger Riggs (<roger.ri...@oracle.com < > mailto:roger.ri...@oracle.com <roger.ri...@oracle.com>>>) escribió: > Hi Stuart, > > Not withstanding all the details. It would be useful (and possibly > expected) that an EMPTY_MAP > could be substituted for a Map with no entries. As it stands, the > caller needs know whether any optional > possibly modifying operations will be used and decide whether to create > an empty map or an EMPTY_MAP. > That makes using an EMPTY_MAP a risk and less useful and a cautious > programmer will avoid it. > > $.02, Roger > > > On 6/13/19 8:42 PM, Stuart Marks wrote: > > > > > > On 6/10/19 7:34 AM, Abraham Marín Pérez wrote: > >> I agree that it makes sense for EMPTY_MAP to have the same logic as > >> Map.of() or unmodifiable(new HashMap()), which means that my > >> suggestion cannot be applied to just EMPTY_MAP... but maybe that’s > >> ok: maybe we can change all of them? If we keep the default > >> implementation for Map.computeIfPresent in EMPTY_MAP, Map.of(), > >> unmodifiableMap, etc., instead of overriding it to throw an > >> exception, then: > >> > >> - For cases where the key isn’t present (regardless of empty or > >> non-empt map), the call will be a safe no-op. > >> - For cases where the key is present, the call will still throw UOE > >> via the implementation of put() > >> > >> I believe this would still respect the spec for Map.computeIfPresent > >> while providing a more forgiving implementation (in the sense that it > >> will avoid throwing an exception when possible). > > > > Hi Abraham, > > > > The specification does not mandate one behavior or another. Either > > behavior is permitted, and in fact both behaviors are present in the JDK. > > > > The second and more significant point is raised by your last statement > > suggesting a "more forgiving implementation." The question is, do we > > actually want a more forgiving implementation? > > > > The collections specs define certain methods as "optional", but it's > > not clear whether this means that calling such a method should always > > throw an exception ("strict" behavior) or whether the method should > > throw an exception only when it attempts to do something that's > > disallowed ("lenient" behavior). > > > > Take for example the putAll() method. What happens if we do this? > > > > Map<String, String> map1 = Collections.emptyMap(); > > map1.putAll(otherMap); > > > > The answer is that it depends on the state of otherMap. If otherMap is > > empty, then the operation succeeds (and does nothing). However, if > > otherMap is non-empty, then the operation will throw > > UnsupportedOperationException. That's an example of lenient behavior. > > What's notable about this is that you can't predict the outcome unless > > you know the state of otherMap. > > > > Now, how about this example? > > > > Map<String, String> map2 = Map.of(); > > map2.putAll(otherMap); > > > > This always throws UnsupportedOperationException, regardless of the > > state of otherMap. I'd call this strict behavior. > > > > More recent implementations, including the Java 8 default methods, and > > the Java 9 unmodifiable collections (Map.of et al), have all preferred > > strict behavior. This is because less information about the state of > > the system is necessary in order to reason about the code, which leads > > to fewer bugs, or at least earlier detection of bugs. It's unfortunate > > that emptyMap().putAll() has this lenient behavior, but we're stuck > > with it for compatibility reasons. > > > > Now there is a slight wrinkle with computeIfPresent(). If we have > > > > Map<String, String> map = Collections.emptyMap(); > > map.computeIfPresent(key, remappingFunction); > > > > then as you point out, this can never modify the map, since the key is > > never present. Thus, there isn't any behavior that's dependent upon > > additional state. > > > > But, as Michael observed, there is now an inconsistency with Map.of() > > and Collections.unmodifiableMap(). So, continuing with your > > suggestion, we might change those implementations to allow > > computeIfPresent() as well. Thus, > > > > Map<String, String> map1 = Collections.emptyMap(); > > Map<String, String> map2 = Map.of(); > > Map<String, String> map3 = Collections.unmodifiableMap(new > > HashMap<>()); > > > > and then > > > > {map1,map2,map3}.computeIfPresent(key, remappingFunction); > > > > all are no-ops. Well, maybe. What if we had retained a reference to > > the HashMap wrapped by the unmodifiableMap, and then we added an > > element? Now it contains a mapping; what should happen then? Should > > computeIfPresent() throw an exception because it *might* attempt to > > modify the map? Or should it throw an exception *only* if it attempts > > to modify the map? State-dependent behavior has slipped back in. > > Whilst I find the Collections.unmodifiable* family of methods very useful, > I always thought they were a bit of a misnomer: they don’t really make the > underlying collection unmodifiable, they just wrap it in an > unmodifiable-looking cloak. I believe it is a common expectation that, if > you do keep a reference to the underlying, mutable collection, and you make > changes to it, then those changes are to transpire elsewhere. Consider the > following: > > Map<String, String> map1 = new HashMap<>(); > Map<String, String> map2 = Collections.unmodifiableMap(map1); > map2.get(“key”); // returns null > > map1.put(“key”, “value”); > > map2.get(“key”); // returns “value” > > The second call to map2.get(“key”) would yield a different result to the > first one, exhibiting state-dependent behaviour… but I don’t think anybody > would be surprised by that. In fact, I believe people would be more > surprised of the opposite. In this case, state-dependent behaviour actually > feels more natural and desirable than state-independent behaviour. > > This might be part of a wider conversation, but I think we might be at > risk of mistaking a desire for immutability (which I agree with) with a > desire for state-independence. Objects have an internal state and behaviour > will always depend on it, the question is whether that state can change or > not; if state can indeed change, then it is my believe that behaviour > should reflect that change. > > > > > > We also need to consider the impact on the other compute* methods. > > Consider: > > > > Map<String, String> map = Collections.emptyMap(); > > map.compute(key, (k, v) -> null); > > > > This can never change the map, but it currently throws UOE. Should it > > be changed to a no-op as well? > > > > ** > > > > What we have now is strict rule: calling mutator methods on > > unmodifiable collections throws an exception. It's simple to describe > > and reason about, and it's (mostly) consistent across various > > collections. However, it prohibits some operations that sometimes > > people want to do. > > > > If we were to make the system more lenient, or more forgiving, then > > weird inconsistencies and conditional behaviors pop up in other > > places. This makes the system more complicated and harder to reason > > about, and that leads to more bugs. > > I guess at this point we are approaching the realm of personal opinion, > since I actually see the opposite: more lenient behaviour would make the > code easier to understand. Stricter behaviour in this case means you cannot > just use the type and properties of the variable at hand to deduce > behaviour, you also need to know how the particular object was created. A > more lenient behaviour would remove oddities like the one I had to face. > > > > > s'marks > > > > > > -- > Amazon Page: https://amazon.com/author/abraham.marin.perez < > https://amazon.com/author/abraham.marin.perez> > Twitter: @AbrahamMarin <https://twitter.com/AbrahamMarin> > LinkedIn: http://lnkd.in/TTHC8W <http://lnkd.in/TTHC8W> > > -- Amazon Page: https://amazon.com/author/abraham.marin.perez Twitter: @AbrahamMarin <https://twitter.com/AbrahamMarin> LinkedIn: http://lnkd.in/TTHC8W