Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add empty enum as haxe.Unit #11563

Open
wants to merge 2 commits into
base: development
Choose a base branch
from
Open

Add empty enum as haxe.Unit #11563

wants to merge 2 commits into from

Conversation

Simn
Copy link
Member

@Simn Simn commented Feb 8, 2024

This has been discussed a lot in the past, so my apologies if I'm oversimplifying something here. Given that null is a thing, the most straightforward way I can think of to implement a unit type is to have an enum without any constructors, which then can only ever be null.

What am I missing?

@Simn Simn added the discussion label Feb 8, 2024
@nadako
Copy link
Member

nadako commented Feb 8, 2024

I was thinking that maybe having something like abstract Unit(Dynamic) { final Unit = null; } would be nice to "encapsulate" the null usage and make it more explicit, but then again that'd rise the question of whether it's "specified" to be able to pass null where Unit is expected and whether it's supposed to have the same meaning, so that abstraction is kinda leaky without null-safety, so maybe just having actual null is more clear and practical...

@Simn
Copy link
Member Author

Simn commented Feb 8, 2024

I was thinking about trying to hide the null as well, but I struggle to find a good reason for doing so. My favorite part about just using null is actually that it's very easy to remember. Requiring the use of some custom Unit value puts an additional burden on users for no particular reason.

@kLabz
Copy link
Contributor

kLabz commented Feb 8, 2024

How will that work with null safety?

@Simn
Copy link
Member Author

Simn commented Feb 8, 2024

Does null-safety consider enums not-nullable? In that case we'll need a meta to explicitly mark enums as nullable.

@kLabz
Copy link
Contributor

kLabz commented Feb 8, 2024

I think so https://try.haxe.org/#249624dF

@Simn
Copy link
Member Author

Simn commented Feb 8, 2024

Ah nice, it's good that it does that.

But yes, a metadata specifically for enums makes sense to me. Enums are finite sets of values, and it would be good to be able to explicitly state that null is in that set.

@skial skial mentioned this pull request Feb 8, 2024
1 task
@Apprentice-Alchemist
Copy link
Contributor

Imo having null be a valid value under null-safety for a type that is not Null<T> is weird and unexpected.

And it also breaks something like this:

class Event<T> {
  /*
    returns the last event, or null if no event has been dispatched yet
  */
  function getLastValue():Null<T>;

  function dispatch(val: T);
}

var notifier = new Event<haxe.Unit>();
assert(notifier.getLastValue() == null);
notifier.dispatch(null);
assert(notifier.getLastValue() != null); // error, the last value is still null

(Of course this example also breaks if you were to do Event<Null<OtherType>>, so this example would need a NotNull constraint on T to actually work properly)

@Simn
Copy link
Member Author

Simn commented Feb 11, 2024

Imo having null be a valid value under null-safety for a type that is not Null is weird and unexpected.

I disagree specifically for enums. Enums can be understood as sets, and whether or not a value is in a set is a property of the set itself. By extension, valid set constructors are defined on the set itself, and this may include null. Basically, one should understand @:nullable enum E { A; B; } as enum E { null; A; B; }, but without the syntactic and semantic differences.

In contrast, Null<E> is more of an ad-hoc type union, similar to what haxe.Unit | E would be, I think.

Having said that, your example does give me pause. As you say, this would only become relevant if we had a constraint for type parameters to make them not-nullable. This does sound like a good idea regardless of what we're doing here, and I feel like you don't make the most relevant part clear enough: If we had that type of constraint on Event.T, haxe.Unit couldn't even be used with that type because it would be nullable.

Under these circumstances, it seems like a better idea to flip the script: Instead of adding @:nullable to enums, we add @:notNullable and then have a singular constructor on haxe.Unit.

The main problem with that approach is that we have to give that constructor a name. And I'm not even joking here, I always find it giga difficult to remember how such things are named, which is part of the reason I was liking this null idea...

@Apprentice-Alchemist
Copy link
Contributor

Imo having null be a valid value under null-safety for a type that is not Null is weird and unexpected.

I disagree specifically for enums. Enums can be understood as sets, and whether or not a value is in a set is a property of the set itself. By extension, valid set constructors are defined on the set itself, and this may include null. Basically, one should understand @:nullable enum E { A; B; } as enum E { null; A; B; }, but without the syntactic and semantic differences.

Imo that overloads the meaning of null too much.
null is the null pointer, to me it doesn't make sense to give it a second meaning (for some enums) as an enum constructor.

In contrast, Null<E> is more of an ad-hoc type union, similar to what haxe.Unit | E would be, I think.

Having said that, your example does give me pause. As you say, this would only become relevant if we had a constraint for type parameters to make them not-nullable. This does sound like a good idea regardless of what we're doing here, and I feel like you don't make the most relevant part clear enough: If we had that type of constraint on Event.T, haxe.Unit couldn't even be used with that type because it would be nullable.

Under these circumstances, it seems like a better idea to flip the script: Instead of adding @:nullable to enums, we add @:notNullable and then have a singular constructor on haxe.Unit.

The main problem with that approach is that we have to give that constructor a name. And I'm not even joking here, I always find it giga difficult to remember how such things are named, which is part of the reason I was liking this null idea...

enum Unit {
  Unit;
}

seems like the most logical option?

Alternatively, introduce tuples and use the empty tuple as unit type.
Can't forget how something is named if it doesn't really have a name :)

@Simn
Copy link
Member Author

Simn commented Feb 11, 2024

Imo that overloads the meaning of null too much.
null is the null pointer, to me it doesn't make sense to give it a second meaning (for some enums) as an enum constructor.

I might have a rather pattern-matcher-centric view of this because in there I pretty much have to treat null like a constructor to get the logic right. I'd even argue that not matching null on a Null<Enum> should always be an exhaustiveness error, but that's a topic for another day. However, I do understand that this might be confusing for people who think in pointers, so I'm not going to argue this point any further.

enum Unit {
Unit;
}

This would be fine by me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants