H2,
On 15 Aug 2018, at 9:47 AM, h...@abula.org wrote:
OCS, your reference to Objective-C is interesting, as is your
expectation for your code to encounter null values often.
In my experience, a null is (should be, at the very least) a
first-class citizen, value which simply means “there is no object
there”. In normal code it happens all the time, e.g., if you check a
map with a key which happens not to be there, or if you use _find_
with a condition which happens not to match any of the items in the
list, etc.
I may be old school, but I think the semantics of null values is
interesting, as is the semantics of safe navigation.
Actually, all this NPE stuff (and its consequences) is pretty
new-school :)
The aforementioned Objective C (of eighties!) has a couple of very
simple rules:
- wherever an expression returns an object, it may return a _nil_;
- whenever a message[*] is sent to a _nil_, it is a very quick and
efficient empty operation, whose return value is again _nil_.
[*] in ObjC, you use an object exclusively[**] through sending
messages: you send a message without any argument for a property
getter, you send a message with one argument for a property setter,
you send a message with an arbitrary number of arguments for a method
call, etc.
[**] but for the access to instance variables (more or less
“fields”), which is sort-of similar to plain-C structs, and
happens from the declaring class code only, usually is limited to the
property accessor methods and never used outside of them.
Whilst I do completely appreciate that tastes and experiences do
differ, well, myself, in thirty-odd years of using this paradigm daily
and after an uncounted zillions source lines in projects some of which
have 25-odd-year lifespan, were made for NeXTSTEP ages ago and are
maintained for macOS of today, I am yet to find any drawback of this
approach. Cases when a bug has been caused by unintended and uncaught
sending a message to a _nil_ are extremely rare (myself I can recall
about three of such cases in all those years), whilst the advantages
for code readability, conciseness, and robustness are tremendous.
Safe navigation addresses the matter of nested if statements,
perfectly in my opinion.
Absolutely. Nevertheless, there's absolutely no point why it should be
limited to methods — compare e.g., the following case (very simple
and thus a bit artificial to keep concise):
===
interface TreeNode {
TreeNode createChildIfPossible(String name); // if possible, creates
and returns a new named child; null if not possible
}
void makeSubtreeIfPossible(TreeNode tn) { // here, we do not care
whether successful; just want to make as big a subtree as possible
def foo=tn?.createChildIfPossible('foo')
foo?.createChildIfPossible('bar')?.createChildIfPossible('baz')
foo?.createChildIfPossible('bax')?.createChildIfPossible('baz')?.createChildIfPossible('last')
}
===
Thanks to ?. we have dodged the necessity of super-ugly ifs, but
still, the result is sort of at the ugly javaish side. Can we get
groovier? Well of course we can:
===
class TreeNode {
def leftShift(object) { this.createChildIfPossible(object) }
}
void makeSubtreeIfPossible(TreeNode tn) {
def foo=tn?<<'foo'
foo?<<'bar'?<<'baz'
foo?<<'bax'?<<'baz'?<<'last'
}
===
Oh, oops! We can't do that, for we do not have “safe navigation”
for operators, and thus, if we want to use the << for adding a child,
we would have to get back to ugly Javaish ifs, which I would not dare
to show lest some reader might get sick :)
What's more, it is explicit in terms of where it applies - you can
combine safe navigation with "unsafe navigation", allowing NPE's to
be thrown and nulls to propagate where appropriate.
Agreed, and where one needs just an _occasional_ null-propagation,
it's perfect (or, as the example above shows, _would be_, if it could
be used with all the operators instead of just with the method call,
property access and indexing ones).
Nevertheless, there are cases where one needs the null-propagation not
just occasionally, but very often (if not exclusively) in a piece of
code — do not all the ?'s in the examples above look ugly? And even
if you like them, they very definitely make the code highly fragile:
it is very easy and quite probable to simply forget one or two of
them, getting thus one unwanted, uncaught and potentially disastrous
NPE (essentially in random based on the data, so testing might not
help to find&fix the culprit). You would rather have to add a
try/catch harness, which in this case creates a boilerplate code. We
should do without boilerplate in Groovy, in my opinion!
On the other hand, this would be clear, concise, completely
intention-revealing, and completely robust:
===
@ImplicitSafeNavigation(true) void makeSubtreeIfPossible(TreeNode tn)
{
def foo=tn<<'foo'
foo<<'bar'<<'baz'
foo<<'bax'<<'baz'<<'last'
}
===
Unless the same explicit control can be exerted over arithmetic /
other expressions, I think those two concepts cannot be compared.
Seems to me it's just one concept, not two of them.
It has been available from the very beginning for two kinds of
expressions: a method call and a property getter.
Lately, it has been extended to another kind of expression, namely,
the indexing.
It would be only consistent and reasonable to extend the thing to all
expressions without an exception.
Regardless whether that happens or not, still, it is only one and the
same concept; we are not debating any other one, but just an extent of
its availability. Especially in Groovy, where... oh, see please below
:)
I also think any assumption about the semantics of null values are
problematic. In particular, assuming that any null value in an
expression should be propagated is not obvious to me.
To me, it seems very obvious, completely natural and quite intuitive
— especially in Groovy where (unlike many other languages) the
“expression operators” are essentially just a convenience syntax
sugar for plain method calls (i.e., “foo<<bar” is nothing but a
convenience shorthand for equivalent but ugly
“foo.leftShift(bar)”, “foo+bar” for “foo.plus(bar)”, and
so forth).
Thanks and all the best,
OC
Den 2018-08-15 04:18, skrev ocs@ocs:
mg,
On 15 Aug 2018, at 3:26 AM, mg <mg...@arscreat.com> wrote:
Fair enough (I am typing this on my smartphone on vacation, so keep
samples small; also (your) more complex code samples are really hard
to read in my mail reader). It still seems to be a big paradigm
change
I might be missing something of importance here, but I can't see any
paradigm change; not even the slightest shift.
The only change suggested is that one could — in the extent of one
needs that, which would self-evidently differ for different people
—
decide whether the “safe” behaviour is explicitly triggered by
using the question-mark syntax, or whether it is implicit.
since regular Java/Groovy programs typically have very little null
values
The very existence of ?. and ?[] suggests it is not quite the case
—
otherwise, nobody would ever bother designing and implementing them.
so am not convinced this is worth the effort (and as Jochen pointed
out, there will still be cases where null will just be converted to
"null").
Are there? Given my limited knowledge, I know of none such.
“null?.plus('foo')” yields a null, and so — for a consistency
sake — very definitely should also “null?+'foo'” and
“@ImplicitSafeNavigation ... null+foo”, had they existed.
What I would suggest instead is considering to introduce nil,
sql_null, empty, ... as type agnostic constants in addition to the
existing null in Groovy. That way you could use e.g. nil in your
code, which by definition exhibits your expected behavior, but it
would make the usage more explicit, and one would not need to
switch/bend the existing null semantics...
That's a nice idea; alas, so that it is viable, one would also have
to
be able to set up which kind of null is to be returned from
expressions like “aMap['unknownkey']“ or “list.find {
never-matches }” etc.
Thus, instead of my “@ImplicitSafeNavigation(true)” you would
have
to use something like “@DefaultNullClass(nil)” — and instead
of
“@ImplicitSafeNavigation(false)” you would need something like
“@DefaultNullClass(null)”.
Along with that, you would need a way to return “the current
default
null” instead of just null; there would be a real problem with a
legacy code which returns null (but should return “the current
default null” instead), and so forth.
That all said, it definitely is an interesting idea worth checking;
myself, though, I do fear it would quickly lead to a real mess
(unlike
my suggestion, which is considerably less flexible, but at the same
moment, very simple and highly intuitive).
Thanks and all the best,
OC
-------- Ursprüngliche Nachricht --------
Von: "ocs@ocs" <o...@ocs.cz>
Datum: 15.08.18 00:53 (GMT+00:00)
An: dev@groovy.apache.org
Betreff: Re: suggestion: ImplicitSafeNavigation annotation
mg,
On 15 Aug 2018, at 1:33 AM, mg <mg...@arscreat.com> wrote:
That's not how I meant my sample eval helper method to be used :-)
(for brevity I will write neval for eval(true) here)
What I meant was: How easy would it be to get a similar result to
what you want, by wrapping a few key places (e.g. a whole method
body) in your code in neval { ... } ? Evidently that would just
mean that any NPE inside the e.g. method would lead to the whole
method result being null.
Which is a serious problem. Rarely you want „a whole method be
skipped (and return null) if anything inside of it happens to be
null“. What you normally want is the null-propagation, e.g.,
def foo=bar.baz[bax]?:default_value;
... other code ...
The other code is _always_ performed and _never_ skipped (unless
another exception occurs of course); but the null-propagation makes
sure that if bar or bar.baz happens to be a null, then default_value
is used. And so forth.
To give a simple example:
final x = a?.b?.c?.d
could be written as
final x = neval { a.b.c.d }
Precisely. Do please note that even your simple example did not put
a whole method body into neval, but just one sole expression
instead. Essentially all expressions — often sub-expressions,
wherever things like Elvis are used — would have to be embedded in
nevals separately. Which is, alas, far from feasible.
Of course the two expressions are not semantically identical,
since neval will transform any NPE inside evaluation of a, b, c,
and d into the result null - but since you say you never want to
see any NPEs...
That indeed would not be a problem.
(The performance of neval should be ok, since I do not assume that
you expect your code to actually encounter null values, and
accordingly NPEs, all the time)
This one possibly would though: I _do_ expect my code to encounter
null values often — with some code, they might well be the normal
case with a non-null an exception. That's precisely why I do not
want NPEs (but the quick, efficient and convenient null-propagation
instead) :)
Thanks and all the best,
OC
-------- Ursprüngliche Nachricht --------
Von: "ocs@ocs" <o...@ocs.cz>
Datum: 14.08.18 23:14 (GMT+00:00)
An: dev@groovy.apache.org
Betreff: Re: suggestion: ImplicitSafeNavigation annotation
mg,
On 14 Aug 2018, at 11:36 PM, mg <mg...@arscreat.com> wrote:
I am wondering: In what case does what you are using/suggesting
differ significantly from simply catching a NPE that a specific code
block throws and letting said block evaluate to null in that case:
def eval(bool nullSafeQ, Closure cls) {
try {
return cls()
}
catch(NullPointerException e) {
if(nullSafeQ) {
return null
}
throw e
}
}
Conceptually, not in the slightest.
In practice, there's a world of difference.
For one, it would be terrible far as the code cleanness, fragility
and readability are concerned — even worse than those ubiquitous
question marks:
=== the code should look, say, like this ===
@ImplicitSafeNavigation def foo(bar) {
def x=baz(bar.foo)?:bax(bar.foo)
x.allResults {
def y=baz(it)
if (y>1) y+bax(y-1)
else y–bax(0)
}
}
=== the eval-based equivalent would probably look somewhat like this
===
def foo(bar) {
def x=eval(true){baz(eval(true){bar.foo})?:bax(bar.foo)}
eval(true){
x.allResults {
def y=eval(true){baz(it)}
if (y>1) eval(true){y+bax(y-1)}
else eval(true){y–bax(0)}
}
}
}
===
and quite frankly I am not even sure whether the usage of eval above
is right and whether I did not forget to use it somewhere where it
should have been. It would be _ways_ easier with those question
marks.
Also, with the eval block, there might be a bit of a problem with
the type information: I regret to say I do not know whether we can
in Groovy declare a method with a block argument in such a way that
the return type of the function is automatically recognised by the
compiler as the same type as the block return value? (Definitely I
don't know how to do that myself; Cédric or Jochen might, though
;))
Aside of that, I wonder about the efficiency; although premature
optimisation definitely is a bitch, still an exception harness is
not cheap if an exception is caught, I understand.
(It feels a bit like what you wants is tri-logic/SQL type NULL
support in Groovy, not treating Java/Groovy null differently...)
In fact what I want is a bit like the Objective-C simple but very
efficient and extremely practical nil behaviour, to which I am used
to and which suits me immensely.
Agreed, the Java world takes a different approach (without even the
safe navigation where it originated!); I have tried to embrace that
approach a couple of times, and always I have found it seriously
lacking.
I do not argue that the null-propagating behaviour is always better;
on the other hand, I do argue that sometimes and for some people it
definitely is better, and that Groovy should support those times and
people just as well as it supports the NPE-based approach of Java.
Thanks and all the best,
OC
-------- Ursprüngliche Nachricht --------
Von: "ocs@ocs" <o...@ocs.cz>
Datum: 14.08.18 17:46 (GMT+00:00)
An: dev@groovy.apache.org
Betreff: Re: suggestion: ImplicitSafeNavigation annotation
Jochen,
On 14 Aug 2018, at 6:25 PM, Jochen Theodorou <blackd...@gmx.org>
wrote:
Am 14.08.2018 um 15:23 schrieb ocs@ocs:
H2,
However, “a+b” should work as one would expect
Absolutely. Me, I very definitely expect that if a happens to be
null, the result is null too. (With b null it depends on the details
of a.plus implementation.)
the counter example is null plus String though
Not for me. In my world, if I am adding a string to a non-existent
object, I very much do expect the result is still a non-existent
object. Precisely the same as if I has been trying to turn it to
lowercase or to count its character or anything.
Whilst I definitely do not suggest forcing this POV to others, to me,
it seems perfectly reasonable and 100 per cent intuitive.
Besides, it actually (and expectably) does work so, if I use the
method-syntax to be able to use safe navigation:
===
254 /TMP> <q.groovy
String s=null
println "Should be null: ${s?.plus('foo')}"
255 /TMP> /usr/local/groovy-2.4.15/bin/groovy q
WARNING: An illegal reflective access operation has occurred
... ...
Should be null: null
256 /TMP>
===
which is perfectly right. Similarly, a hypothetical “null?+'foo'”
or “@ImplicitSafeNavigation ... null+foo” should return null as
well, to keep consistent.
(Incidentally, do you — or anyone else — happen to know how to get
rid of those pesky warnings?)
Thanks and all the best,
OC