Ad-hoc equality test for struct after definition

question

#1

I am trying to streamline some unit tests. Most of them work by generating a value and comparing it to a known value. I want something like ==, but I only want to define it in the tests, not the package source, since equality is not really useful for the struct per se, just unit testing (if I wanted to define it in general, I would use something like AutoHashEquals.jl).

So I just want to define an ad-hoc, ==-like comparison operator which does the following:

  1. if arguments have the same type which is a composite type, disregarding parameters, just compare all fields with ==
  2. otherwise compare the values themselves with ==.

Currently I am using

@generated function ≂(x, y)
    if !isempty(fieldnames(x)) && x == y
        mapreduce(n -> :(x.$n == y.$n), (a,b)->:($a && $b), fieldnames(x))
    else
        :(x == y)
    end
end

which works OK, except that I want it to be less strict about parametric types, eg

struct Foo{T}
    a::T
    b::T
end
Foo(1,2) ≂ Foo(1.0,2.0) # should be true, currently false

How can I do this? Also, I don’t insist on a generated function: this is what I could come up with, but better suggestions are appreciated.


#2

Why do you use @generated functions?

This line confuses me, why do you need the && part if you have it covered by the else clause?

EDIT: just deleted my proposal for a faster version , If you remove && x == y your code works as you intended

2nd Edit: I finally understand @generated functions :slight_smile:
if you follow my first suggestion you might compare too different types as equal if they have the same number of fields
with the same name.
This solves the issue:

@generated function ≂(x, y)
    if !isempty(fieldnames(x)) && (x.name == y.name)
        mapreduce(n -> :(x.$n == y.$n), (a,b)->:($a && $b), fieldnames(x))
    else
        :(x == y)
    end
end

in a generated function anything that is not quoted is just its inferred datatype, got it now.


#3

Came here for the same reason. This is pretty cool. Maybe it should be included in Test somehow, the need for comparing only in the tests is pretty general. Like a AutoHashEquals just for Test.


#4

The following behavior seems weird:

struct Bar
    a::Int
    c::Vector{String}
end
x = [Bar(i,string.(1:i)) for i in 1:5]
y = [Bar(i,string.(1:i)) for i in 1:5]
x ≂ y # -> false!
all(x .≂ y) # -> true

Also, why not replace the == in @TsurHerman’s solution with a ≂ to allow for nested structs:

        mapreduce(n -> :(x.$n ≂ y.$n), (a,b)->:($a && $b), fieldnames(x))

I might be wrong.


#5

Try something like

@generated function ≂(x, y)
    if Base.isstructtype(x) && Base.isstructtype(y) && (x.name == y.name) 
        mapreduce(n -> :(x.$n ≂ y.$n), (a,b)->:($a && $b), :(true), fieldnames(x))
    elseif (x <: Array) && (y <: Array)
        :(size(x) == size(y) && all(y .≂ x))
    else
        :(x == y)
    end
end

which of course still does not catch all cases. A properly implemented version could dispatch on type, and only use the generated function where it is need (composite types).

I find these things useful so I may package it up when I have time.


#6

Awesome, I’ll use that for sure.

I 'll check this version later tonight. checked, it works for me!

and thank you.