haxe icon indicating copy to clipboard operation
haxe copied to clipboard

Abstract null comparison / assign

Open ncannasse opened this issue 4 weeks ago • 13 comments

I'm using an abstract over an Int64 to represent some objects. This is working well, but since Int64 is not nullable, I cannot implement either null check ( == null / != null ) or null assignment ( abs = null ). I think these should be allowed as overrides on abstracts, at least on static platforms where null is not a valid value for these.

This would allow the following:

if( value == null )  {...} // compiles to if( AbsImpl.isNull(value) )
if( value != null ) {...} // compiles to if( !AbsImpl.isNull(value) )
value = null; // compiles to (inlined) AbsImpl.setNull(value) which allows setting value

Now if we compare a Null'able abs with a not nullable, we would need to be able to check isNull() as well, as a not null value which is isNull() should be strictly the same as a null value.

var value : Abs;
var nullValue : Null<Abs>;
if( value == nullValue ) { ... } // compiles to if( nullValue == null ? value.isNull() : (value == (nullValue:Abs)) )
if( nullValue1 == nullValue2 ) { ... } // ... a bit more complicated as we need to these both nullValue1/2 for both "real" null and .isNull() 

ncannasse avatar Nov 28 '25 11:11 ncannasse

I'm confused by the premise here, why do you want to implement null-checks on a type that's not nullable?

Simn avatar Nov 28 '25 12:11 Simn

The real-world usage is to replace a CDB Kind (String at runtime) by a CDB GUID (Int64 unique identifier at runtime). However we want to be able to encode the null value as 0, and keep the existing code doing null checks when migrating from String to Int64 abstracts.

ncannasse avatar Nov 28 '25 14:11 ncannasse

@Simn what do you think ? could it be possible to add in the (very) near future ? I have some big code refactoring in progress that would highly benefit from that.

ncannasse avatar Nov 28 '25 15:11 ncannasse

It does feel like a fairly Shiro-specific hack rather than a properly thought-out language feature. There's already a lot of complexity with regards to abstracts and null-values, so I'm not inclined to add more to that.

If you're refactoring things anyway, can't you just introduce a custom neutral value for this use case?

final nullGUID = #if false null #else Int64.make(0, 0) #end;

Then add this to some global import.hx and replace the related occurrences of null with nullGUID.

Simn avatar Nov 28 '25 15:11 Simn

Well that was a real-world example, so ofc it is specific to our usage.

A more general usage is to be able to abstract the notion of nullability with an underlying type that is itself not nullable. For instance, one might want to have an abstract over an optional enumeration of different cases and use -1 to encode the null value. On static platforms this is quite important in terms of performances in order to avoid allocations and possible garbage colleciton for every value change.

ncannasse avatar Nov 28 '25 15:11 ncannasse

Or let's say you have a lot of code that uses Null<Float>, with code logic that tests the Float value or for null. You could simply change with an abstract NullFloat that would store the NaN value for null, and without changing any other code (only the storage) you would get immediate rid of allocations.

ncannasse avatar Nov 28 '25 15:11 ncannasse

I kind of understand what you're saying, but isn't this ultimately just syntax sugar? Instead of doing something like value.isSet() we do value == null, and instead of doing value.unset() we do value = null. That hardly seems like something that justifies compiler support.

Simn avatar Nov 28 '25 16:11 Simn

Abstracts operator overloading are just syntactic sugar anyway 😉

Le ven. 28 nov. 2025, 17:03, Simon Krajewski @.***> a écrit :

Simn left a comment (HaxeFoundation/haxe#12415) https://github.com/HaxeFoundation/haxe/issues/12415#issuecomment-3589809631

I kind of understand what you're saying, but isn't this ultimately just syntax sugar? Instead of doing something like value.isSet() we do value == null, and instead of doing value.unset() we do value = null. That hardly seems like something that justifies compiler support.

— Reply to this email directly, view it on GitHub https://github.com/HaxeFoundation/haxe/issues/12415#issuecomment-3589809631, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAHZXQAQYCJJWFWGUQT4SPT37BW6RAVCNFSM6AAAAACNO5ANGOVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZTKOBZHAYDSNRTGE . You are receiving this because you authored the thread.Message ID: @.***>

ncannasse avatar Nov 28 '25 16:11 ncannasse

I think we'd only need to add support for this?

@:op(A==null)
function isNull():Bool { ... }

As the other case can already be handled with a (albeit currently with some limitations, like checking b == null in there..)

@:op(A==B)
function eqNull(b:Null<GuidInt<T>>):Bool { ... }

kLabz avatar Nov 28 '25 17:11 kLabz

@:op(A==null) seems fine and builds on the existing operator overload design.

But that doesn't cover the value = null case. We don't allow overloading assignment operators, and I'd rather not introduce it just for null-values...

Simn avatar Nov 28 '25 20:11 Simn

So maybe add a @:from(null) ?

Le ven. 28 nov. 2025, 21:19, Simon Krajewski @.***> a écrit :

Simn left a comment (HaxeFoundation/haxe#12415) https://github.com/HaxeFoundation/haxe/issues/12415#issuecomment-3590331506

@:op(A==null) seems fine and builds on the existing operator overload design.

But that doesn't cover the value = null case. We don't allow overloading assignment operators, and I'd rather not introduce it just for null-values...

— Reply to this email directly, view it on GitHub https://github.com/HaxeFoundation/haxe/issues/12415#issuecomment-3590331506, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAHZXQGFRMMHKHP7WCAXLL337CU3XAVCNFSM6AAAAACNO5ANGOVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZTKOJQGMZTCNJQGY . You are receiving this because you authored the thread.Message ID: @.***>

ncannasse avatar Nov 28 '25 21:11 ncannasse

I think that would ironically interfere with the abstract == null case because we type the rhs against the lhs, which would give us abstract == Abstract.fromNullCall().

Simn avatar Dec 01 '25 06:12 Simn

I guess it could work with only from(null) then ? If so, I'm still very interested in finding a solution for this.

ncannasse avatar Dec 03 '25 21:12 ncannasse

The motivation behind supporting this in the compiler (rather than rejecting it as a hack that's too specific) seems to be the general use-cases listed in this thread, but none of those work with #12434:

  • For instance, one might want to have an abstract over an optional enumeration of different cases and use -1 to encode the null value
  • You could simply change with an abstract NullFloat that would store the NaN value for null

What if a different solution like this could be taken:

abstract SpecialInt(Int) from Int {
	@:from
	static macro function fromExpr(e) {
		return switch haxe.macro.Context.typeExpr(e).expr {
			case TConst(TNull): macro -1; // intercept special value cast
			case _: e;
		};
	}
}
function main() {
	final a:SpecialInt = null;
	trace(a);
	trace(a == null);
}

The macro cast is ignored currently in this case, but switching it to a different literal like [] instead of null does already work: https://try.haxe.org/#b32Ce830

I think adjusting the cast behaviour to make it possible to intercept the null literal might be a better solution for this problem.

  • It gives the user the tools needed to implement the syntax sugar/behaviour they want
  • It is flexible and supports all suggested use-cases in this thread (null can be remapped to any internal value, e.g. -1 or NaN)
  • It doesn't require a new hacky meta or a new feature
  • It doesn't introduce null inconsistencies to valid values from user types

tobil4sk avatar Dec 12 '25 11:12 tobil4sk