Julia Language Using symbols as lightweight enums


Although the @enum macro is quite useful for most use cases, it can be excessive in some use cases. Disadvantages of @enum include:

  • It creates a new type
  • It is a little harder to extend
  • It comes with functionality such as conversion, enumeration, and comparison, which may be superfluous in some applications

In cases where a lighter-weight alternative is desired, the Symbol type can be used. Symbols are interned strings; they represent sequences of characters, much like strings do, but they are uniquely associated with numbers. This unique association enables fast symbol equality comparison.

We may again implement a Card type, this time using Symbol fields:

const ranks = Set([:ace, :two, :three, :four, :five, :six, :seven, :eight, :nine,
                   :ten, :jack, :queen, :king])
const suits = Set([:♣, :♦, :♥, :♠])
immutable Card
    function Card(r::Symbol, s::Symbol)
        r in ranks || throw(ArgumentError("invalid rank: $r"))
        s in suits || throw(ArgumentError("invalid suit: $s"))
        new(r, s)

We implement the inner constructor to check for any incorrect values passed to the constructor. Unlike in the example using @enum types, Symbols can contain any string, and so we must be careful about what kinds of Symbols we accept. Note here the use of the short-circuit conditional operators.

Now we can construct Card objects like we expect:

julia> Card(:ace, :♦)

julia> Card(:nine, :♠)

julia> Card(:eleven, :♠)
ERROR: ArgumentError: invalid rank: eleven
 in Card(::Symbol, ::Symbol) at ./REPL[17]:5

julia> Card(:king, :X)
ERROR: ArgumentError: invalid suit: X
 in Card(::Symbol, ::Symbol) at ./REPL[17]:6

A major benefit of Symbols is their runtime extensibility. If at runtime, we wish to accept (for example) :eleven as a new rank, it suffices to simply run push!(ranks, :eleven). Such runtime extensibility is not possible with @enum types.