[ANN] AnonymousEnums.jl

Hi there,

I’d like to introduce you to AnonymousEnums.jl, a package designed to simplify enum definitions.

AnonymousEnums.jl offers a solution to the challenge of assigning unique names to each instance of an enum subtype. Unlike other packages like Enumx.jl, which places enums in modules (to ensure uniqueness), AnonymousEnums.jl takes a different approach. It allows you to define instances without setting unique names for the instances. You can use symbols to set a value and test for equality:

julia> @anonymousenum Fruit::UInt8 begin
           apple
           banana
       end
julia> apple = Fruit(:apple)
julia> @assert apple == Fruit(0)
julia> @assert apple == :apple
julia> @assert instances(Fruit) == (:apple, :banana)

This package is particularly useful for typed code generation scenarios. I am personnaly using this for capnproto code generation. It enables me to keep the (qualified) names of enum instances hidden as implementation details, eliminating the need for users to know them.

It allows me to provide a more user-friendly interface, like this:

julia> writer.fruit.type = :apple
julia> if reader.fruit.type == :apple
           # do something
       elseif reader.fruit.type == :banana
           # do something else
       end

instead of:

julia> writer.fruit.type = GeneratedNameForThisEnum.apple
julia> if reader.fruit.type == GeneratedNameForThisEnum.apple
           # do something
       elseif reader.fruit.type == GeneratedNameForThisEnum.banana
           # do something else
       end

Now, I want to address a potential concern. Although I initially had reservations about confusing symbols with instances of enums, I found that it was better to use apple == :apple instead of AnonymousEnums.isequal(apple, :apple). However, I understand that this choice may introduce some confusion. If you have any suggestions or alternative approaches, I’m interested in hearing them.

1 Like

IMO it would be much nicer if you added a new function to your package instead of overloading Base.:(==).

Note that the user can always set a convenient shorthand version for your function, like:

const f = AnonymousEnums.long_function_name

Or you might be able to find an infix Unicode operator that makes sense.

1 Like

Thank you for your feedback.

I have considered @nsajko’s suggestion and will implement it, allowing users to remap it according to their preferences.

While the Unicode operator offers elegance, it may not be universally appreciated. Personally, I don’t mind its usage. However, developers with visual impairments, those using older text editors (as I have heard), or individuals transitioning from other programming languages may prefer the first solution.

Actually… What would you think of using Base.isequal?
It is used here to compare a CompoundPeriod with a Period, by calling the constructor of CompoundPeriod on the Period object. Which is kind of what I’m doing (CompoundPeriod → Enum and Period → Symbol).

The core issue is that both isequal and == have established semantics that are different than what you define in your methods. This can lead to confusion, and, in the case of isequal, the bigger problem is the interfaces that must be respected, see documentation entry. The TLDR is that isequal is used by Dict (and perhaps similar user-defined types) for comparisons, which is not what you want, I’m pretty sure. If it is what you want, then you have to define isless and hash (and <, I think) consistently.

Have you considered Cameron Bieganek’s suggestion? Here’s the relevant documentation (a bit incomplete, though): entry.

1 Like

Maybe you could use one of the arrow symbol pairs:

For example, for one method, and for its opposite.

But you may also want to make sure that whatever symbols you pick are supported by the unicode autocompletion in the REPL and editors.

EDIT: sorry, I wasn’t clear at all. What I meant is that you have two options, you could pick a symmetric-looking symbol and then give it two methods (similarly to the code you have now), like so:

↔(s::Symbol, x::AnonymousEnum) = x ↔ s
↔(x::T, s::Symbol) where {T <: AnonymousEnum} = x == T(s)

Or you could pick two opposite-looking symbols and then give a method to each one, like so:

←(s::Symbol, x::AnonymousEnum) = x → s
→(x::T, s::Symbol) where {T <: AnonymousEnum} = x == T(s)

The symbols I chose are just examples.

Another note: if you’re going to pick an operator instead of just a function, it’d be good to consider what precedence level would be most fit for the purpose (each symbol seems to belong to a fixed precedence level).

1 Like