Adding members to an existing Enum

If there is an existing Enum with some members, e.g.,

@enum Fruit apple=1 orange=2

How could I later add kiwi=3 to it?

Hi, @egvila!

The short answer is that you can’t. Once an enum is defined, it is closed. You can see it for yourself if you expand the declaration with @macroexpand

julia> @macroexpand @enum Fruit apple=1 orange=2

:($(Expr(:toplevel, :(#= Enums.jl:196 =#), quote
    $(Expr(:meta, :doc))
    primitive type Fruit <: Base.Enums.Enum{Int32} 32 end
end, :(#= Enums.jl:197 =#), :(function Fruit(var"#50#x"::Base.Enums.Integer)
      #= Enums.jl:197 =#
      #= Enums.jl:198 =#
      (1 Base.Enums.:<= var"#50#x" Base.Enums.:<= 2) || Base.Enums.enum_argument_error(:Fruit, var"#50#x")
      #= Enums.jl:199 =#
      return Base.Enums.bitcast(Fruit, Base.Enums.convert(Int32, var"#50#x"))
  end), :(#= Enums.jl:201 =#), :((Base.Enums.Enums).namemap(::Base.Enums.Type{Fruit}) = begin
          #= Enums.jl:201 =#
          Dict{Int32, Symbol}(2 => :orange, 1 => :apple)
      end), :(#= Enums.jl:202 =#), :((Base.Enums.Base).typemin(var"#52#x"::Base.Enums.Type{Fruit}) = begin
          #= Enums.jl:202 =#
      end), :(#= Enums.jl:203 =#), :((Base.Enums.Base).typemax(var"#53#x"::Base.Enums.Type{Fruit}) = begin
          #= Enums.jl:203 =#
      end), :(#= Enums.jl:204 =#), :(let var"#48#insts" = (Base.Enums.Any[Fruit(var"#49#v") for var"#49#v" = Int32[1, 2]]...,)
      #= Enums.jl:205 =#
      (Base.Enums.Base).instances(::Base.Enums.Type{Fruit}) = begin
              #= Enums.jl:205 =#
  end), :(const orange = Fruit(2)), :(const apple = Fruit(1)), :(Base.Enums.nothing))))

It’s a bit messy, but there are a few details worth noticing:

  • there is a check for x <= 2, and the constructor throws an error if this fails;
  • Base.typemax(Fruit) is defined to return Fruit(2)
  • Base.instances(Fruit) returns [1,2]

So the list of valid values is pretty much fixed once the declaration is done.

If you want a name to value mapping that can be expanded, use a Dict{Symbol,Int}, or a similar container.

julia> const Fruit = Dict(:apple => 1, :orange => 2)
julia> Fruit[:orange]
julia> Fruite[:melon] = 3

You can even use PropertyDicts.jl to make the access more elegant:

julia> using PropertyDicts
julia> const Veggie = PropertyDict(:tomato => 1, :cucumber => 2)
julia> Veggie.tomato
julia> Veggie.carrot = 3

The short answer: don’t, because it is not designed for it, and it’s not good design for a type to dynamically increase its set of instances.

However, you can dig into its implementation (see the @macroexpand printout above) to accomplish it. Bear in mind what I’m about to show does not properly alter the entire implementation, only far enough to pull off 1 more instance.

julia> module A
       @enum Fruit apple=1 orange=2

julia> A.Fruit(x::Integer) = Base.bitcast(A.Fruit, Int32(x)) # remove inhibitions on input

julia> A.Fruit(3)
<invalid #3>::Fruit = 3

julia> Base.Enums.namemap(A.Fruit)[3] = :kiwi # add name to the underlying Dict

julia> @eval A const kiwi = Fruit(3) # add const name to enum's module
kiwi::Fruit = 3

kiwi::Fruit = 3

This is a pretty bad idea, and the underlying integer type, defaulting to Int32, limits how many instances you can make.

1 Like

In particular, it’s not composable — since you are changing that enum globally, it will affect all other code using that type.

Probably you want some other data structure that is designed for runtime mutation, such as a dictionary (Dict). But we can’t give you more advice without knowing of the context of your problem.

Thank you all for the great answers.

1 Like

Thinking back on this, I didn’t really justify it. After all, what’s wrong with runtime changes in a dynamic language?

The first point is that enumerated types in particular are not supposed to change what instances they have. They do differ across several languages, but they all are constant names with distinct values of a context-specific type, and you write the names throughout your code instead of designating sentinel values of an integer type that cannot distinguish contexts. There’s no reason to dynamically add names because you can and should list all the names used in the code at once.

But those are almost all static languages anyway, and it’s possible you don’t want the usual enumerated type. Then let’s address types with a dynamic set of instances, or rather what that means. You can cast the proper number of bits to a type to make an “instance” in a sense, but it takes more than that to make a semantically valid instance; see in my example how the awful redefinition of the type constructor first resulted in <invalid #3>. In this example, you need to redefine many methods (ironically, not namemap, just needed to mutate the Dict it returns). Now all those methods and all the methods that use them need to be recompiled, not to mention needing to design the program to evade the world age compilation lag e.g. invokelatest. If you’re routinely making this change (like if you’re reading fruit from files and adding new ones), you’re wasting a lot of time on compilation. The only way not to pay that cost is to not compile, but then you have a stereotypically slow dynamic language. Other dynamic features like redefinable struct fields or conditional methods for closures are also absent to make compilation feasible.

So we can keep shifting the question to if it’s possible to make a type with a dynamic set of instances where we don’t need to redefine methods. namemap gives a hint, actually; a type constructor could check the entries for allowed instances and error otherwise, and you would need a method to alter the entries to allow more instances. The other methods, if you even want them, would calculate their values at runtime instead. Bloating a module from the outside with arbitrary constant names is useless for existing code, so this type won’t be much of an enum. It’s really repurposing the module into a mapping of symbols to instances, which is much better implemented as a Dict{Symbol, Fruit} instead. Together with the Dict{Int32, Symbol} from namemap, you can access existing instances with a symbol or an integer. Why you would use such a type if it can’t serve as a fixed set of constant names isolated to a module’s code, I have no idea.

1 Like