-
Notifications
You must be signed in to change notification settings - Fork 2
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
Enums #2
Comments
We also spent quite a bit of time designing this for the JVM target to make it efficient. Here's the decompiled code for the base class haxe.jvm.Enum: package haxe.jvm;
public class Enum<T extends java.lang.Enum<Object>> extends java.lang.Enum<java.lang.Enum<Object>> {
public <T> boolean equals(Enum<java.lang.Enum<Object>> other) {
return super.equals(other);
}
public String toString() {
String baseName = (String)Type.getEnumConstructs(Type.getEnum((java.lang.Enum)this)).__get(this.ordinal());
Array parameters = Type.enumParameters((java.lang.Enum)this);
return parameters.length == 0 ? baseName : "" + baseName + "(" + parameters.join(",") + ")";
}
public Enum(int index, String name) {
super(name, index);
}
} And then each actual enum extends that: package haxe.ds;
import haxe.jvm.Enum;
import haxe.jvm.Jvm;
import haxe.jvm.annotation.EnumReflectionInformation;
import haxe.jvm.annotation.EnumValueReflectionInformation;
@EnumReflectionInformation(
constructorNames = {"Some", "None"}
)
public abstract class Option extends Enum {
None;
protected Option(int index, String name) {
super(index, name);
}
public static Option Some(Object v) {
return new Option.Some(v);
}
public static Option[] values() {
return new Option[]{None};
}
@EnumValueReflectionInformation(
argumentNames = {"v"}
)
public static class Some extends Option {
public final Object v;
public Some(Object v) {
super(0, "Some");
this.v = v;
}
public boolean equals(Enum other) {
if (!(other instanceof Option.Some)) {
return false;
} else {
Option.Some other = (Option.Some)other;
if (other.ordinal() != this.ordinal()) {
return false;
} else {
return Jvm.maybeEnumEq(other.v, this.v);
}
}
}
}
@EnumValueReflectionInformation(
argumentNames = {}
)
public static class None extends Option {
public None() {
super(1, "None");
}
}
} While this generates a certain amount of code, it's not particularly difficult to implement and takes care of various related problems like reflection and comparison. |
Oh niiiice!! Thank you for this! Yeahhh, polymorphism seems to be a pretty nice/powerful solution in these types of languages. And since things are stored and casted using sub-classes, C# struct members shouldn't be a problem. This is definitely the way to do things. |
Not sure if there has been any additional work/thoughts on this since the last comment, but just weighing in w/ my own thoughts... Somewhat similar to @Simn's response, here is Microsoft's documentation on creating an Enumeration class: Which recommends it being defined like this: public abstract class Enumeration : IComparable
{
public string Name { get; private set; }
public int Id { get; private set; }
protected Enumeration(int id, string name) => (Id, Name) = (id, name);
public override string ToString() => Name;
public static IEnumerable<T> GetAll<T>() where T : Enumeration =>
typeof(T).GetFields(BindingFlags.Public |
BindingFlags.Static |
BindingFlags.DeclaredOnly)
.Select(f => f.GetValue(null))
.Cast<T>();
public override bool Equals(object obj)
{
if (obj is not Enumeration otherValue)
{
return false;
}
var typeMatches = GetType().Equals(obj.GetType());
var valueMatches = Id.Equals(otherValue.Id);
return typeMatches && valueMatches;
}
public int CompareTo(object other) => Id.CompareTo(((Enumeration)other).Id);
// Other utility methods ...
} And can be extended like this: public class CardType
: Enumeration
{
public static CardType Amex = new(1, nameof(Amex));
public static CardType Visa = new(2, nameof(Visa));
public static CardType MasterCard = new(3, nameof(MasterCard));
public CardType(int id, string name)
: base(id, name)
{
}
} In the past, I have updated this recommendation a bit to use generics so that it can handle enumerations of data types other than Which is instead defined like this: public abstract record Enumeration<T> : IComparable<Enumeration<T>>, IEquatable<Enumeration<T>>
where T : IComparable<T>, IEquatable<T>
{
public static implicit operator T(Enumeration<T> value) => value.Id;
public static IEnumerable<TEnumeration> GetAll<TEnumeration>() where TEnumeration : Enumeration<T> =>
typeof(TEnumeration)
.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly)
.Select(fieldInfo => fieldInfo.GetValue(null))
.Cast<TEnumeration>();
public static TEnumeration Parse<TEnumeration>(string name) where TEnumeration : Enumeration<T>
{
_ = TryParse<TEnumeration>(name, out var returnValue);
return returnValue ?? throw new("Enumeration could not be parsed.");
}
public static bool TryParse<TEnumeration>(string name, out TEnumeration? value)
where TEnumeration : Enumeration<T>
{
value = null;
var maybe =
typeof(TEnumeration)
.GetField(name, BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly)?
.GetValue(null);
if (maybe is null)
{
return false;
}
value = (TEnumeration)maybe;
return true;
}
public T Id { get; private init; }
public string Name { get; private init; }
protected Enumeration(T id, string name) => (Id, Name) = (id, name);
public int CompareTo(Enumeration<T>? other) => other is null ? 1 : Id.CompareTo(other.Id);
public virtual bool Equals(Enumeration<T>? other) => other is not null && Id.Equals(other.Id);
public override int GetHashCode() => Id.GetHashCode();
public override string ToString() => Name;
} And can instead be extended like this (for the public record CardType : Enumeration<int>
{
public static readonly CardType Amex = new(1, nameof(Amex));
public static readonly CardType Visa = new(2, nameof(Visa));
public static readonly CardType MasterCard = new(3, nameof(MasterCard));
protected CardType(int id, string name) : base(id, name) { }
} So, it might not be a bad idea to target C# output that is somewhat structured like this. Although, there are still some downsides to not using the native C# var cardType = CardType.Amex;
string message;
switch (cardType)
{
case CardType.Amex:
message = "You have an AMEX.";
break;
case CardType.Visa:
message = "You have a VISA.";
break;
default:
message = "You have something else.";
break;
} But it can still sort of be used in switch expressions. var cardType = CardType.Amex;
var message = true switch
{
cardType.Equals(CardType.Amex) => "You have an AMEX.",
cardType.Equals(CardType.Visa) => "You have a VISA.",
_ => "You have something else."
} |
Note that Haxe enums are actually algebraic data types, so you need to be able to store the associated values somehow. Also, I think this is the first time I'm seeing this |
Yes, I'd stick to the idea suggested by @Simn here, as the primary goal is accuracy with Haxe, unless there are good reasons to use the version recommended by Microsoft docs (other than just they recommend it), like making code generation simpler when dealing with haxe enum values, comparisons expressions simpler etc... Well, maybe @AndrewDRX suggestion is actually compatible with that idea, we'll see when implementing it |
I thought the example I provided was quite similar and would be fairly compatible, so I was just adding it as extra supporting documentation 😅 But the reason to stick w/ the Microsoft docs is for consistency in how other C# projects would use these Enumeration types. E.g., having things like Ideally, a Haxe output should aim to generate something that is as intuitive and familiar to users of the target language as possible, rather than to users of Haxe. If I were developing a C# project and had to choose between two libraries, I would certainly choose the one that didn't look foreign compared to the other C# code around it. But w/ all that in mind, it is also ideal to have an output library that when used in one language is similar/consistent to using it in another language, which is going to sometimes be contradictory to my previous point. But... I think I would lean toward preferring a library that is consistent w/ the target language rather than consistent w/ the same library output to a different language. Especially since there are certain efficiencies that can be achieved by following a design more closely to how the target language was intended to be written. As for the true switch expression syntax, perhaps in my specific example an if-else statement might be better. I said it can "sort of" be used in switch expressions b/c it is a bit of a code smell. However, there are certain cases where it might be more concise to use the switch expression, such as having an public static class Utilities
{
public static string ErrorMessage(ISomeInterface value) =>
true switch
{
true when value is Type1 => "Message 1",
true when value is Type2 => "Message 2",
true when value is Type3 => "Message 3",
_ => "Generic Message"
};
} Although, still quite arguably a code smell. |
I'm not sure what we're actually discussing here because it is my understanding that C# enums are a different kind of data. Unless I'm missing something, we cannot represent a type like our Option as C# enum: enum Option<T> {
None;
Some(v:T);
} The only part somewhat relevant is that |
I still think it is quite similar, I just didn't elaborate on the public record Option<T> : Enumeration<int> where T : IComparable<T>, IEquatable<T>
{
public static readonly Option<T> None = new(1, nameof(None));
public static readonly Option<T> Some = new(0, nameof(Some));
protected Option(int id, string name) : base(id, name) { }
}
public record Some<T> : Option<T>, IComparable<Some<T>>, IEquatable<Some<T>> where T : IComparable<T>, IEquatable<T>
{
public readonly T Value;
public Some(T value) : base(0, nameof(Some)) => Value = value;
public int CompareTo(Some<T>? other) => other is null ? 1 : Value.CompareTo(other.Value);
public virtual bool Equals(Some<T>? other) => other is not null && Value.Equals(other.Value);
public override int GetHashCode() => Value.GetHashCode();
} And then used like this: public static Option<string> TestOption(string? message = null) =>
message is null ? Option<string>.None : new Some<string>(message); var option1 = TestOption("SUCCESS");
var option2 = TestOption();
var option3 = TestOption("FAILURE");
var option4 = TestOption("SUCCESS");
/* Enum value match Option<T>.Some result */
_ = Option<string>.Some.Equals(option1); //true
_ = Option<string>.None.Equals(option1); //false
/* Enum value match Option<T>.None result */
_ = Option<string>.Some.Equals(option2); //false
_ = Option<string>.None.Equals(option2); //true
/* Actual value match */
_ = option1.Equals(option2); //false
_ = option1.Equals(option3); //false
_ = option1.Equals(option4); //true Is this close to what you had in mind? |
That's close, except that I don't think |
I'm not sure that I agree on that part since leaving it as a Although, that comparison could still be done by checking if the value So, I think it would be best to use the syntax But I was mostly trying to expand on your original response to provide similar C# logic that we could aim for as the output, as a rough guide, nothing definitive. |
Although, thinking about it some more, we could achieve what both of us are thinking by using implicit cast operators to go between Still given the same base With public record Option<T> : Enumeration<int>, IComparable<Option<T>>, IEquatable<Option<T>>
where T : IComparable<T>, IEquatable<T>
{
public static implicit operator Option<T>(Func<T, Some<T>> _) => Some<T>.Default;
public static readonly Option<T> None = new(1, nameof(None));
public static readonly Func<T, Some<T>> Some = Some<T>.Default;
public T Value { get; init; } = default!;
protected Option(int id, string name) : base(id, name) { }
public int CompareTo(Option<T>? other) => base.CompareTo(other);
public virtual bool Equals(Option<T>? other) => base.Equals(other);
public override int GetHashCode() => base.GetHashCode();
}
public sealed record Some<T> : Option<T>, IComparable<Some<T>>, IEquatable<Some<T>>
where T : IComparable<T>, IEquatable<T>
{
public static implicit operator Some<T>(Func<T, Some<T>> _) => Default;
public static implicit operator Func<T, Some<T>>(Some<T> _) => (T value) => new(value);
public static readonly Some<T> Default = new(default(T)!);
public Some(T value) : base(0, nameof(Some)) => Value = value;
public int CompareTo(Some<T>? other) => base.CompareTo(other);
public bool Equals(Some<T>? other) => base.Equals(other);
public override int GetHashCode() => base.GetHashCode();
} And then used like this: public static Option<string> TestOption(string? message = null) =>
message is null ? Option<string>.None : Option<string>.Some(message); var option1 = TestOption("SUCCESS");
var option2 = TestOption();
var option3 = TestOption("FAILURE");
var option4 = TestOption("SUCCESS");
/* Enum value match Option<T>.Some result */
_ = option1 == Option<string>.Some; //true
_ = option1 == Option<string>.None; //false
/* Enum value match Option<T>.None result */
_ = option2 == Option<string>.Some; //false
_ = option2 == Option<string>.None; //true
/* Actual value match */
_ = option1.Value == option2.Value; //false
_ = option1.Value == option3.Value; //false
_ = option1.Value == option4.Value; //true So now With that, I am retiring from this thread for now 😅 |
How to make enums?
Is there a variant type for C#?
Would it be appropriate to just store arbitrary data as
object
, then cast upon request if the index matches up? Would lose support for struct types tho.The text was updated successfully, but these errors were encountered: