Comparing symbols with == vs ===

The documentation of Symbol says

Symbols are immutable and should be compared using ===.

Is that for the same reason that it was recommended to use === when checking for missing or nothing?

And following the merge of #38905 which fixed the performance issue with ismissing(...) and isnothing(...), will it still make a difference to use === for symbols?

This fixed one of my problems !!!

Using === instead of ==, that is.

But where did you find this documentation ???

Even knowing the answer I can’t find relevant documentation.

Unfortunately, the Julia manual uses a automatic generator of HTML pages which creates a not very good search bar. Searching ===, (===), or "===" gives no results. This is a known problem.

help?> Symbol
search: Symbol

  Symbol

  The type of object used to represent identifiers in parsed julia code (ASTs). Also often used as a name or
  label to identify an entity (e.g. as a dictionary key). Symbols can be entered using the : quote operator:

[...]

  Symbols are immutable and should be compared using ===. The implementation re-uses the same object for all
  Symbols with the same name, so comparison tends to be efficient (it can just compare pointers).
[...]

Can you give more information on the particular code where it made a difference? Was it a performance issue?

Julia 1.6:

julia> x = Union{Missing,Float64}[1.0]
1-element Vector{Union{Missing, Float64}}:
 1.0

julia> function foo(x)
           s = 0.0
           x1 = x[1]
           ismissing(x1) ? s : s + x1
       end
foo (generic function with 1 method)

julia> foo(x)
1.0

julia> @code_warntype foo(x)
Variables
  #self#::Core.Const(foo)
  x::Vector{Union{Missing, Float64}}
  x1::Union{Missing, Float64}
  s::Float64

Body::Union{Missing, Float64}
1 ─      (s = 0.0)
│        (x1 = Base.getindex(x, 1))
│   %3 = Main.ismissing(x1)::Bool
└──      goto #3 if not %3
2 ─      return s::Core.Const(0.0)
3 ─ %6 = (s::Core.Const(0.0) + x1)::Union{Missing, Float64}
└──      return %6

Julia master:

julia> x = Union{Missing,Float64}[1.0]
1-element Vector{Union{Missing, Float64}}:
 1.0

julia> function foo(x)
           s = 0.0
           x1 = x[1]
           ismissing(x1) ? s : s + x1
       end
foo (generic function with 1 method)

julia> foo(x)
1.0

julia> @code_warntype foo(x)
MethodInstance for foo(::Vector{Union{Missing, Float64}})
  from foo(x) in Main at REPL[37]:1
Arguments
  #self#::Core.Const(foo)
  x::Vector{Union{Missing, Float64}}
Locals
  x1::Union{Missing, Float64}
  s::Float64
Body::Float64
1 ─      (s = 0.0)
│        (x1 = Base.getindex(x, 1))
│   %3 = Main.ismissing(x1)::Bool
└──      goto #3 if not %3
2 ─      return s::Core.Const(0.0)
3 ─ %6 = (s::Core.Const(0.0) + x1::Float64)::Float64
└──      return %6

It is easy to end up with type unstable code by checking for missing with ismissing instead of ===, unless you’re on Julia >=1.7-DEV.
(Note the Body::Union{Missing, Float64} on 1.6 vs Body::Float64 on master.)

2 Likes

So it sounds like there were two reasons: type instability, which was fixed, and “comparing pointers is faster than checking equality”

The implementation re-uses the same object for all Symbols with the same name, so comparison tends to be efficient (it can just compare pointers).

I normally consider interning an implementation detail that shouldn’t be relevant to users. Is that wrong here, or is it possible to automatically convert == into === for Symbols at compile time?

I’ll try to explain, but that’s likely to reveal more misunderstandings on my part.

I was trying to compare symbolic expressions produced by the Symbolics package.

== appears to be produce a new symbolic expression. === seems to check for some sort of structural equivalence (which is sometimes, but not always, useful).

Maple has evalb which has semantics something like, “No really check if they are equal, don’t just create a new expression capturing the postulate that they are equal”.

I’m momentarily under the impression that isequals is like Maple’s evalb.