Solving the drawbacks of @enum

So, @enum is causing me major problems:

  • I can’t use the names I want to use because it puts the names directly in the namespace. I need to go back and forth between strings and enums easily.
  • Override Base.show for enums doesn’t appear to work.
  • I don’t want to put it in a little module by itself to namespace it because I can’t use that same name for both the module and the enum which causes confusion that I have to use X for type but Y.z for values.
  • I found this online: Encapsulating enum access via dot syntax But that results in allocations just to reference MyEnum.value
  • I don’t need nor want the values in the type system, so that’s not an option.

Is there a good solution for enums that I’m missing?

2 Likes

Can you explain this more? How are strings related to enums?

In this particular case, the enums match up with sets of values going to/from an external system as strings in JSON.

I was using that @scopedenum solution I linked to for a little while, but then ran into it contributing to a performance issue because I had to run various logic/calculations some millions of times.

I need to go back and forth between strings and enums easily.

Maybe this does what you need?

@enum Foo x y
string(x)
string(y)

eval(Symbol("x"))
eval(Symbol("y"))

I was using that @scopedenum solution I linked to for a little while, but then ran into it contributing to a performance issue because I had to run various logic/calculations some millions of times.

This seems surprising because they should just be treated like constants, no? Have you posted about this performance issue elsewhere?

@enum’s are just numeric constants, but the @scopedenum macro does some magic with a struct inside a module, so apparently something is going on. I just ran into it yesterday and haven’t reported it anywhere.

Right, there are solutions for that. However, the problem is the other bullet points and I have two enum’s that have the same value: EnumA x and EnumB x and need to use them both in the same code, but because enum’s aren’t namespaced, you can’t. It’s that I kept trying different solutions to make enum’s work and every time I tried something else I ran into another problem such that it wouldn’t work.

It seems like the baremodule version hits your needs, with the only downside of needing different names for the type and module. But that seems minor? Or am I missing something?

You can make a module without a reference to itself in Julia 1.8, which might let you make a type named M in a module named M.

2 Likes

As for how minor it is, depends on how you look at it. Code needs to be as readable as possible for maintenance, but… conventions are required to be known in some cases, apparently this is one of those situations.

I was asking this question in case there was new information that solves all these issues that I hadn’t found yet but was known by the community (I’m new here). But, I think you’re right, the baremodule approach seems to resolve the most number of issues. And… I just now stumbled across this: https://github.com/kindlychung/SuperEnum.jl which appears to make the bare module approach easy to do.

1 Like

There is also an enum in MLStyle, but it uses the type domain iirc.

Algebraic Data Types — MLStyle.jl Documentation

EnumX.jl has been recently released, it might suit your needs (note that I’m not the author of the package).

Thanks. I did see that announcement, though I notice it doesn’t solve the third bullet point: The namespace and the type name are different: MyEnum vs. MyEnum.T. I understand that might be impossible to solve, though, so it looks nice. I’ll give it a try. As for what it does above SuperEnum, the EnumX README says SuperEnum “doesn’t give you Base.Enum s” though I’m don’t know the ramifications of that.

So I’ve made some updates to SumTypes.jl which I believe solves your problems posted here.

I can’t use the names I want to use because it puts the names directly in the namespace. I need to go back and forth between strings and enums easily.

SumTypes.jl now lets you avoid polluting the name space:

julia> using SumTypes

julia> @sum_type Fruit begin
           apple
           banana
           orange
       end hide_variants=true
Fruit

julia> apple
ERROR: UndefVarError: apple not defined

julia> Fruit'.apple # note that I used Fruit' not Fruit
apple::Fruit

julia> let (;orange, banana) = Fruit'
           orange
       end
orange::Fruit

Override Base.show for enums doesn’t appear to work.

Should work fine with SumTypes:

julia> Base.show(io::IO, f::Fruit) = @cases f begin
           apple => print(io, "apple")
           banana => print(io, "banana")
           orange => print(io, "orange")
       end

julia> Fruit'.apple
apple

I don’t want to put it in a little module by itself to namespace it because I can’t use that same name for both the module and the enum which causes confusion that I have to use X for type but Y.z for values.

I agree, so I didn’t do that.

julia> typeof(Fruit'.apple) === Fruit
true

I found this online: Encapsulating enum access via dot syntax But that results in allocations just to reference MyEnum.value

Don’t have that problem here:

julia> @btime Fruit'.apple
  1.258 ns (0 allocations: 0 bytes)
apple

julia> @code_typed Fruit'.apple
CodeInfo(
1 ─ %1 = Base.getfield(x, f)::Fruit
└──      return %1
) => Fruit

I don’t need nor want the values in the type system, so that’s not an option.

I use values, not types:

julia> Fruit'.apple isa Type
false
5 Likes

This is a cross post from https://discourse.julialang.org/t/retrieving-an-instance-of-an-enum-using-a-string/61183/7 . That may be of interest here too

@enum LogLevel begin
    LL_INFO
    LL_DEBUG
end

Please also note from

Base.tryparse(E::Type{<:Enum}, str::String) =
    let insts = instances(E) ,
        p = findfirst(==(Symbol(str)) ∘ Symbol, insts) ;
        p !== nothing ? insts[p] : nothing
    end

using Test
@testset "now" begin
    @test tryparse(LogLevel, "LL_INFO") == LL_INFO
    @test tryparse(LogLevel, "ll_info") == nothing
    @test tryparse(LogLevel, "INFO") == nothing
    @test tryparse(LogLevel, "info") == nothing
end

Those possibles evolutions :

Base.tryparse(E::Type{<:Enum}, str::String; prefix::String="") =
    let eq(x, e) = lowercase(prefix*x) == lowercase(e) ,
        # eq could be statically dispatchable according to some enum surface lang traits
        insts = instances(E) ,
        p = findfirst(insts .|> string) do e; eq(str,e) end;
        p !== nothing ? insts[p] : nothing
    end

@testset "meanwhile" begin
    @test tryparse(LogLevel, "LL_INFO") == LL_INFO
    @test tryparse(LogLevel, "ll_info") == LL_INFO
    @test tryparse(LogLevel, "INFO"; prefix="LL_") == LL_INFO
    @test tryparse(LogLevel, "info"; prefix="LL_") == LL_INFO
end

particulaly considering Solving the drawbacks of @enum

EnumX.jl is really cool! However, there are times when it may not be necessary to assign names to instances of the enums at all.

As an example, I’ve quickly implemented this (unregistered) package called AnonynousEnums.jl. Check it out and let me know your thoughts. I’m considering registering it.

The motivation behind this package came from the challenge of generating Julia enums from capnproto schema. I wanted to make it easier for users by eliminating the need to worry about where and how the instances of the enums are defined uniquely. This approach helps keep the API as simple as possible, as users won’t have to be aware of the generated types. Additionally, I prefer treating this as an implementation detail that I’m free to modify in the future.

Edit: just did a PR on the General Registry

1 Like