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.
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.
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
@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…
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
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
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