Deep-equal

We have deepcopy, which recursively copies the object down to its “atoms”. Has anyone written the corresponding deepequal, such that deepequal(deepcopy(x), deepcopy(x)) == true?

1 Like

Under what circumstance would deepequal yield different results to equal? For deepcopy, presumably you would want to change some component and not affect the object you copied from.

But I can’t think of a reason why you would need a deepequal.

:man_shrugging: Whenever you want to know that two structures are equivalent?

julia> isequal(Ref(1), Ref(1))
false

That would be deepequal.

I think it’s a bit like deepcopy. Not super clean semantically, but can be very useful for testing/debugging/rapid development.

1 Like

FWIW, there’s a classic Lisp article on why multiple copy / equality implementations are needed: https://www.nhplace.com/kent/PS/EQUAL.html

2 Likes

Not that I know of. FWIW, I think that generally == or isequal should be used by most user code, and types should just define the relevant methods.

That said, I often define custom equality operators (eg for unit testing), it is pretty easy to do recursively. Cf fieldnames for struct, use in a generated function.

4 Likes

There doesn’t exist one, but you can easily define your own if you really need it. The following function is one I’ve found useful in the past to extend == for my own types:

function deepequal(a, b)
    typeof(a) == typeof(b) || return false
    N = fieldcount(typeof(a))
    for i in 1:N
        getfield(a, i) == getfield(b, i) || return false
    end
    return true
end

I also just cooked up the following which is more generic, but haven’t tested it very thoroughly, and really don’t know if it’s really worth it to write as a @generated function…

@generated function deepequal(a, b)
    # check the types
    a == b || return :(false)
    N = fieldcount(a)

    # fallback to regular comparison for primitive types
    N == 0 && return :(a == b)

    # unroll the loop, because it's cool!
    quote
        Base.@nexprs $N i -> (getfield(a, i) == getfield(b, i) || return false)
        return true
    end
end

edit: actually, for what you want both functions should be recursive. The comparisons should be deepequal(getfield(a, i), getfield(b, i)) rather than ==

2 Likes

Thanks! I thought of writing my own, but the deepcopy internals are surprisingly complex. Eg. Your deepequal would fail with circular structures, and it might fail with arrays/dicts. I was hoping someone had provided it in a package or something.

2 Likes

Note that Ref(1) == Ref(1) falls back to === by default. You can check this with julia> @edit Ref(1) == Ref(1). Most regular containers check their content already (e.g. [1] == [1]), so if you define == on your type to check its own fields as well, it should work out fine. Just make sure to special case circular dependencies or disallow your type to contain itself.

1 Like