This has come up on slack in the past but I couldn't find an issue about it so I'm opening one.
In a package API, when the user can choose among a set of options, some authors prefer enums over symbols. The awkward part of using enums though is that exporting all of them clutters the global namespace a bit too much: it'd be nicer to be able to export just the type and access the instances with getproperty
.
Example of desired behavior:
module StatsMakie
export BarPosition
@enums BarPosition stack dodge superimpose
end
using StatsMakie
BarPosition.stack
With the extra advantage that the user can discover all options via tab-completion on BarPosition
.
At the moment this is not possible, even though there was a suggested workaround (which is along the lines of what StatsMakie currently does):
module StatsMakie
module BarPosition
@enums _BarPosition stack dodge superimpose
end
export BarPosition
end
I have some code along these lines that I use in a personal project that I've meant to polish up and publish, since I know it's come up before. It's pretty small right now, works really well for my use-case. Currently, it only supports storing the inner value as an Int64
, but I'd actually like to make it take any number (and type) of arguments, similar to Java enums (since we're creating a full struct anyway). Also currently, you have to pass the intervalue value arguments explicitly, so @scopedenum Fruit apple=1 banana=2
. Happy to hack on something w/ somebody if they'd like.
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
Triage thinks deciding this now would be premature, since we might want to do a more extensive redesign of enums in the future.
I'll also add that I'm personally mostly against this, since it prevents using .
to access properties of types themselves. For example in the compiler and reflective code we use T.abstract
, T.size
etc. All such uses would become invalid and would have to be rewritten to use getfield
.
We could just forbid enum member names that collide with fieldnames(DataType)
(check in the macro during enum creation).
Then we could also remove the global identifiers (stop polluting the global namespace) and therefore permit two different enums to have members with the same name (imo more relevant than a small documented blacklist of forbidden enum member names), would also gain tab-completion for enum members, and code that uses enums would become more readable.
This would be breaking, though.
In terms of ergonomics, python enums / IntFlags are really well-designed (especially the python IntFlags
).
Most helpful comment
I'll also add that I'm personally mostly against this, since it prevents using
.
to access properties of types themselves. For example in the compiler and reflective code we useT.abstract
,T.size
etc. All such uses would become invalid and would have to be rewritten to usegetfield
.