`SymPy.Sym`s as keys in Dicts

It seems that Tuples containting SymPy.Syms don’t work as keys in julia’s standard dicts as expected. See the following MWE and its very counterintuitive behaviour.

julia> using SymPy

julia> t = symbols("t")

julia>  = Dict()
Dict{Any, Any}()

julia> get!(d, (sin(t),), 0)

julia> get!(d, (sin(t),), 1)

julia> s = sin(t)

julia> get!(d, (s,), 2)

julia> get!(d, (s,), 3)

julia> haskey(d, (sin(t),))

julia> haskey(d, (s,))

Is this a bug that should be reported on github, or is this behaviour expected? I want to use more complex SymPy expressions as keys to get some Memoization to work (see this issue on github).

The issue is subtle. Hashing a tuple ends up calling objectid which differs for s and sin(t) (as s === sin(t) is false). Hashing s and sin(t) separately falls back to hash(PyObject(s)) and hash(PyObject(sin(t))) which have the same hash. I think a change (a hash(x::PyObject, h::UInt64) method?) to PyCall would be needed here to get the same hash, though I’m not saying that is what the behaviour should be.

The related SymPyPythonCall package does hash the two tuples identically, so it does work there, so we have:

julia> @syms t; s = sin(t)

julia> d[(s,)] = s; d[(sin(t),)] = s^2

julia> d
Dict{Any, Any} with 1 entry:
  (sin(t),) => sin(t)^2

Under SymPy, the last command would print:

julia> d
Dict{Any, Any} with 2 entries:
  (sin(t),) => sin(t)^2
  (sin(t),) => sin(t)

Thanks for the explanation! It turned out that the definition I needed to override was not only for hash(::PyObject, ::UInt64) but also for hash(::Sym, ::UInt64) to get it all working. So:

Base.hash(x::SymPy.PyObject, h::UInt64) = Base.hash_uint(3h - hash(x))
Base.hash(x::SymPy.Sym, h::UInt64) = Base.hash_uint(3h - hash(x))

I realise this is type piracy, but for little secret private code that I won’t share with anyone I am happy to have a bit of naughty type piracy.