On 15/05/18 22:26, John Rose wrote:
On May 15, 2018, at 5:06 AM, Maurizio Cimadamore
I wonder if we shouldn't also consider something along the lines of restricting
migration compatibility only for _nullable_ value types, where a nullable value
type is a value whose representation is big enough that it can afford one spare
value to denote null-ness. So, if you want to convert existing legacy reference
classes to value types, they'd better be nullable values; this way you don't
lose any value in the legacy domain - nulls will be remapped accordingly (using
a logic specified in the nullable value type declaration).
It seems like we've been somewhere along this path before (when we were
exploring the Q vs. L split) - why would something like that not be workable?
We do something similar explicitly (not globally) with the combinator
MHs.explicitCastArguments, which converts null to zeroes of
primitive types. But not vice versa: Zeroes don't re-box to null.
And it's a localized thing.
I think you are suggesting a def-site opt-in where a class VT somehow
nominates a special value VT.N (perhaps its VT.default default, perhaps
not) with one or both of these behaviors:
assert( (VT)null == VT.N ); // unbox null to N
assert( (Object)VT.N == null ); // box N to null (probably not!)
Yes, that is the spirit.
Well, I'm suggesting something along those lines, but I don't think that
generally you can assume that null pattern and default pattern are the
same thing, semantically; we've been there before, and came away with
the feeling that these were two different things; e.g. null is often
used as a special sentinel value, denoting some missing element. That's
not default (which could be a totally legitimate value within the domain).
For simplicity, let's say N must be VT.default, and that the conversion
is just one way (null to VT). Then the opt-in could be as simple as
mixing in an interface NullConvertsToDefault. See the P.S. of this
(Perhaps you are suggesting something different?)
But doesn't this more complex path only come up in migration cases? E.g.
maybe there's a way to have this w/o totally compromising the
performance model for regularly co-compiled classes?
It might be workable. It's complicated, of course. It would need to inject
special logic into many paths in the JVM which are currently just error
paths. There might be collateral damage on performance. For optimized
code, throwing an NPE is always simpler than patching and continuing.
This is roughly because, in optimized code, control flow merges are harder
to optimize than control flow forks.
(Adding in the symmetric feature, of converting N to null, is probably
very expensive, since it would seem to require lots of new tests of
the form, "are you N?" And there's little value in converting N to
null and then having List.of or some other null-hostile API throw
an error. So then you have a puzzler: The seam between N and
null is not completely hidden.)
AFAIK C# does something like this as a one-time deal, which cannot be
mixed in as an interface:
In C#, at the use-site of a type you can opt into it with an emotional
type like 'int?'. But we could make it opt in at the def-site, too,
if the value type has a spare code-point it's not using. I'm sure
if C# doesn't do this there are excellent reasons for them not to.
Anybody got information on this?
I am hoping to avoid playing such a card. (In case anyone didn't notice,
we are playing with a large deck here, if not a full deck. There are
lots of potential moves we can make.) I want us to win with a small
number of moves.
P.S. Other examples of moves: Adding another type descriptor, having
one array type be polymorphically boxed or flattened, having two
VM-level types per source value type, waiting for reified generics, waiting
for primitive convergence, adding large infrastructure for migration.
Maybe we will be forced to do one or all of these before we can
get anywhere. I hope not; I'm trying to sneak across a meaningful
waypoint (not finish line) simply with L-world.