Encapsulating enum access via dot syntax

Wouldn’t it make sense to access enums via dot syntax, together with maybe/optionally not to pushing all enum members into the scope? I currently have two issues:

  • I am not able to create different enums with the same member names (which could have different meanings per enum context).
  • At exporting an enum, the members don’t get exported. So I either have to write <Module>.<Enum member> or export all enum members.
    • The module prefix doesn’t seem nice/logical to me as the context (enum name) gets lost, and the module name may not have anything logically in common with the enum member name.
    • Exporting all members trashes the workspace/scope and blocks variable names.

This could be solved via <Enum>.<Member> access syntax, so that I can just export the pure enum names, access all members without difficulties and still have a clean workspace. This would also be more similar to the C style usage of enums.

4 Likes

The workaround I’ve seen for access like this is to stash them inside a tiny module:

baremodule Fruits
using Base: @enum
@enum Fruit Apple Pear Banana
end

julia> Fruits.#<tab><tab>
Apple  Banana  Fruit   Pear

Now in 0.7, we also have getproperty overloading, so perhaps another solution will emerge. Since this is “just” a macro it’s possible to build upon the base implementation for the semantics you want.

4 Likes

It would be really nice to be able to have Fruit be a type yet have Fruit.Apple, etc. work. What is unlikely to be possible (at least as far as I can tell) is to have that work and have using Fruit cause Apple, etc. to be available as short names, nor would import Fruit: Apple work. This is because there’s no way (currently) for a non-module to masquerade as a module.

Here’s a macro I’ve thrown together and use in a few projects:

macro scopedenum(T, args...)
    defs = Expr(:block)
    append!(defs.args, collect(:(const $(x.args[1]) = Enum($(x.args[2]))) for x in args))
    names = Dict(x.args[2]=>String(x.args[1]) for x in args)
    str2val = Dict(String(x.args[1])=>x.args[2] for x in args)
    push!(defs.args, quote
        function name(e::Enum)
            nms = $names
            return nms[e.value]
        end
        Enum(str::String) = Enum($(str2val)[str])
        Base.show(io::IO, e::E) where {E <: Enum} = print(io, "$(Base.datatype_module(E)).$(name(e)) = $(e.value)")
    end)
    blk = esc(:(module $T; struct Enum{T}; value::T; end; Enum{T}(e::Enum{T}) where {T} = e; $defs; end))
    return Expr(:toplevel, blk)
end

The basic usage is

@scopedenum Fruit APPLE=1 PEAR=2 BANANA=3
Fruit.APPLE
Fruit.PEAR
# access enum value
Fruit.APPLE.value
# make an APPLE from string
Fruit.Enum("APPLE")
# restricting type signatures
f(x::Fruit.Enum) = # do stuff w/ x
4 Likes

Ah, interesting. So this puts the type inside of the module. I guess you can’t have a @scopedenum called Enum but that’s a pretty minor limitation :smile:

Thanks for your suggestions!

@mbauman
I get an error “getindex not defined” and have to add that, too (v0.6.2). Downside is, I then have more extensive definitions and <Module>.<Enum>.<Value> (like Technologies.Technology.gan) as return value, which seems a bit superfluous.

@quinnj
Although I have to add the numbers manually, that seems more practical. It behaves not the same as a regular enum, but I will give it a try and will see if there are potential downsides.

using Fruit should explicitly never pull pure Apple into the workspace, as this is exactly what I want to avoid. Instead Apple then should always be not defined and instead solely be a parameter to the Fruit-dot-syntax (⇒ Fruit.Apple). So all sorts of fruits should always be available via dot syntax, with import Fruit: Apple not being needed (but maybe optional). This would not be backwards-compatible to current Enums, of course. (I hope I haven’t misunderstood your text.)

For me, Fruit could also just be of type Enum (or NewEnum, etc.). That may prevent dispatch, but would be at least a good intermediate solution.

Also named tuples have that kind of dot syntax I’d like to have for Enums. In fact the implementation of named tuples behaves more like the Enums I’d like to have, than Julia’s real Enums do… :confused:

1 Like

If someone has a problem getting the above code to work in Julia v1.0 – you have to replace datatype_module with parentmodule. :slight_smile:

Note that I’ve since revamped my @scopedenum code to act a little more like regular enums:

module ScopedEnums

using JSON2

export @scopedenum

macro scopedenum(T, args...)
    blk = esc(:(
        module $(Symbol("$(T)Module"))
            using JSON2
            export $T
            struct $T
                value::Int64
            end
            const NAME2VALUE = $(Dict(String(x.args[1])=>Int64(x.args[2]) for x in args))
            $T(str::String) = $T(NAME2VALUE[str])
            const VALUE2NAME = $(Dict(Int64(x.args[2])=>String(x.args[1]) for x in args))
            Base.string(e::$T) = VALUE2NAME[e.value]
            Base.getproperty(::Type{$T}, sym::Symbol) = haskey(NAME2VALUE, String(sym)) ? $T(String(sym)) : getfield($T, sym)
            Base.show(io::IO, e::$T) = print(io, string($T, ".", string(e), " = ", e.value))
            JSON2.read(io::IO, ::Type{$T}) = $T(JSON2.read(io, String))
            JSON2.write(io::IO, x::$T) = JSON2.write(io, string(x))
        end
    ))
    top = Expr(:toplevel, blk)
    push!(top.args, :(using .$(Symbol("$(T)Module"))))
    return top
end

end
2 Likes

For anyone finding this off a google search like I did, here’s a slightly updated of @quinnj very helpful snipped that adds

  • overloaded propertynames so REPL tab completion works
  • iteration of enum members
  • JSON3 StructType mapping rather than JSON2
macro option(T, args...)
    blk = esc(:(
        module $(Symbol("$(T)Module"))
            using JSON3
            export $T
            struct $T
                value::Int64
            end
            const NAME2VALUE = $(Dict(String(x.args[1])=>Int64(x.args[2]) for x in args))
            $T(str::String) = $T(NAME2VALUE[str])
            const VALUE2NAME = $(Dict(Int64(x.args[2])=>String(x.args[1]) for x in args))
            Base.string(e::$T) = VALUE2NAME[e.value]
            Base.getproperty(::Type{$T}, sym::Symbol) = haskey(NAME2VALUE, String(sym)) ? $T(String(sym)) : getfield($T, sym)
            Base.show(io::IO, e::$T) = print(io, string($T, ".", string(e), " = ", e.value))
            Base.propertynames(::Type{$T}) = $([x.args[1] for x in args])
            JSON3.StructType(::Type{$T}) = JSON3.StructTypes.StringType()

            function _itr(res)
                isnothing(res) && return res
                value, state = res
                return ($T(value), state)
            end
            Base.iterate(::Type{$T}) = _itr(iterate(keys(VALUE2NAME)))
            Base.iterate(::Type{$T}, state) = _itr(iterate(keys(VALUE2NAME), state))
        end
    ))
    top = Expr(:toplevel, blk)
    push!(top.args, :(using .$(Symbol("$(T)Module"))))
    return top
end
2 Likes

This version also allows for enums without explicit values:

macro myenum(T, args...)
    counter = 0
    function key_value(x)
        if hasproperty(x, :args)
            key = x.args[1]
            value = x.args[2]
        else
            key = x
            value = counter
            counter += 1
        end
        return key, value
    end
    _args = [key_value(x) for x in args]

    blk = esc(:(
        module $(Symbol("$(T)Module"))
            using JSON3
            export $T
            struct $T
                value::Int64
            end
            const NAME2VALUE = $(Dict(String(x[1])=>Int64(x[2]) for x in _args))
            $T(str::String) = $T(NAME2VALUE[str])
            const VALUE2NAME = $(Dict(Int64(x[2])=>String(x[1]) for x in _args))
            Base.string(e::$T) = VALUE2NAME[e.value]
            Base.getproperty(::Type{$T}, sym::Symbol) = haskey(NAME2VALUE, String(sym)) ? $T(String(sym)) : getfield($T, sym)
            Base.show(io::IO, e::$T) = print(io, string($T, ".", string(e), " = ", e.value))
            Base.propertynames(::Type{$T}) = $([x[1] for x in _args])
            JSON3.StructType(::Type{$T}) = JSON3.StructTypes.StringType()

            function _itr(res)
                isnothing(res) && return res
                value, state = res
                return ($T(value), state)
            end
            Base.iterate(::Type{$T}) = _itr(iterate(keys(VALUE2NAME)))
            Base.iterate(::Type{$T}, state) = _itr(iterate(keys(VALUE2NAME), state))
        end
    ))
    top = Expr(:toplevel, blk)
    push!(top.args, :(using .$(Symbol("$(T)Module"))))
    return top
end

Example:

julia> @myenum Foo a b

julia> Foo.a
Foo.a = 0

julia> Foo.b
Foo.b = 1

julia> Foo(0)
Foo.a = 0

julia> Foo(1)
Foo.b = 1

1 Like

Note that overloading getproperty on a Type is currently considered type piracy, and can break things in all sorts of interesting and wonderful (aka bad) ways. (though that might change use `getfield` to access type fields in reflection by JeffBezanson · Pull Request #39573 · JuliaLang/julia · GitHub)

2 Likes

The thing is that while interfacing with existing C code, having enums would be really nice. An equivalent solution would be to be able to subtype Dict. Is that possible?

This version also allows for a begin end block:
Thank you @edit @enum :sweat_smile:

macro scopedenum(T, syms...)
    counter = 0 
    function key_value(x)
        if hasproperty(x, :args)
            k = x.args[1]
            v = x.args[2]
        else
            k = x
            v = counter
            counter += 1
        end
        return k,v
    end

    if length(syms) == 1 && syms[1] isa Expr && syms[1].head === :block
        syms = syms[1].args
    end

    syms = Tuple(x for x in syms if ~(x isa LineNumberNode))
    _syms = [key_value(x) for x in syms if ~(x isa LineNumberNode)]

    blk = esc(:(
        module $(Symbol("$(T)Module"))
            using JSON3
            export $T
            struct $T
                value::Int64
            end
            const NAME2VALUE = $(Dict(String(x[1])=>Int64(x[2]) for x in _syms))
            $T(str::String) = $T(NAME2VALUE[str])
            const VALUE2NAME = $(Dict(Int64(x[2])=>String(x[1]) for x in _syms))
            Base.string(e::$T) = VALUE2NAME[e.value]
            Base.getproperty(::Type{$T}, sym::Symbol) = haskey(NAME2VALUE, String(sym)) ? $T(String(sym)) : getfield($T, sym)
            Base.show(io::IO, e::$T) = print(io, string($T, ".", string(e), " = ", e.value))
            Base.propertynames(::Type{$T}) = $([x[1] for x in _syms])
            JSON3.StructType(::Type{$T}) = JSON3.StructTypes.StringType()

            function _itr(res)
                isnothing(res) && return res
                value, state = res
                return ($T(value), state)
            end
            Base.iterate(::Type{$T}) = _itr(iterate(keys(VALUE2NAME)))
            Base.iterate(::Type{$T}, state) = _itr(iterate(keys(VALUE2NAME), state))
        end
    ))
    top = Expr(:toplevel, blk)
    push!(top.args, :(using .$(Symbol("$(T)Module"))))
    return top
end

Example 1:

julia> @scopedenum TransportState begin
           DISCONNECTED=1
           SERVER_DISCONNECTED=2
           CONNECTING=3
           CONNECTED=4
           DISCONNECTING=5
       end

julia> TransportState.DISCONNECTED
TransportState.DISCONNECTED = 1

julia> TransportState.CONNECTING
TransportState.CONNECTING = 3

Example 2:

julia> @scopedenum TransportState begin
           DISCONNECTED
           SERVER_DISCONNECTED
           CONNECTING
           CONNECTED
           DISCONNECTING
       end

julia> TransportState.DISCONNECTED
TransportState.DISCONNECTED = 0

julia> TransportState.CONNECTING
TransportState.CONNECTING = 2

Example 3:

julia> @scopedenum TransportState DISCONNECTED=1 SERVER_DISCONNECTED=2 CONNECTING=3 CONNECTED=4 DISCONNECTING=5

julia> TransportState.DISCONNECTED
TransportState.DISCONNECTED = 1

julia> TransportState.CONNECTING
TransportState.CONNECTING = 3

Example 4:

julia> @scopedenum TransportState DISCONNECTED SERVER_DISCONNECTED CONNECTING CONNECTED DISCONNECTING

julia> TransportState.DISCONNECTED
TransportState.DISCONNECTED = 0

julia> TransportState.CONNECTING
TransportState.CONNECTING = 2
4 Likes