#1135 Mixin for equals / hash based on fields

qualidafial Fri 2 Jul 2010

Fantom rules.

While working on a pet project (a solver for a block puzzle game) I needed to implement equals and hash on all my model classes. This mixin did the trick and is ridiculously simple.

mixin Model {
  override Bool equals(Obj? that) {
    if (this === that) return true;
    if (null === that) return false;
    if (this.typeof != that.typeof) return false;

    return typeof.fields.all |Field field->Bool| {
      field.get(this) == field.get(that)
    }
  }

  override Int hash() {
    return typeof.fields.reduce(51) |Int base, Field field->Int| {
      Int fieldHash := field.get(this)?.hash ?: 0
      return base * 37 + fieldHash
    }
  }
}

This probably doesn't belong in a core class, but could we put this in the docs somewhere?

rfeldman Fri 2 Jul 2010

Relatedly, anyone know how speedy Fantom's reflection is? I know Java's is notoriously slow, but I haven't run any speed tests with Fantom.

If it's not actually slow, I would personally love having something like this as the default implementation of those methods in Obj. It's always seemed to me like this is what you'd want equals() to do by default, and likewise with hash().

ivan Fri 2 Jul 2010

rfeldman

Seems the reflection is quite fast, but about 10 times slower than normal call. Just wrote a test:

getInt    : 1229
refectInt : 11992
getStr    : 1408
reflectStr: 12666

Testing code:

class SampleData { Str s := ""; Int i := 4 }

class ReflectionTest
{
  Void main()
  {
    count := 100_000_000
    data := SampleData()
    getInt := measure |->| { count.times { i := data.i } }
    reflectInt := measure |->| { count.times {  i := data.typeof.field("i").get(data)  } }
    getStr :=  measure |->| { count.times { s := data.s } }
    reflectStr := measure |->| { count.times { s := data.typeof.field("s").get(data) } }
    echo("getInt    : $getInt.toMillis
          refectInt : $reflectInt.toMillis
          getStr    : $getStr.toMillis
          reflectStr: $reflectStr.toMillis
          ")
  }

  Duration measure(|->| func)
  {
    start := Duration.now
    func.call
    return Duration.now - start
  }
}

ivan Fri 2 Jul 2010

qualidafial,

I used the same approach, but sometimes it may be useful to exclude some fields from hash/compare, so I used approach like this (writing by memory):

mixin State
{
  abstract Obj?[] state
  override Int hash() { state.hash }
  override Bool equals(Obj? other) 
  { 
    other.typeof.fits(typeof) ? 
      other->state == state : false
  }
}

class MyData : State
{
  Int int; List list; Str str;
  override Obj?[] state() { [int, list, str] }
}

brian Fri 2 Jul 2010

I'd say in general reflection is fast enough for stuff like that unless you have some critical code. Reflection has gotten a lot faster in the newer VMs compared to the old days.

Plus, there are future optimizations that will likely bring it pretty close to statically compiled calls. We can generate code in Java runtime to avoid some of the overhead in Java reflection. Plus eventually, I hope we can use MethodHandles (if 1.7 ever ships and gets popular).

rfeldman Fri 2 Jul 2010

Thoughts on adding something like these to Obj? Is there any downside?

KevinKelley Fri 2 Jul 2010

@rfeldman: fails on graphs, doubly-linked lists, etc., with infinite-loop. But it's a real nice pattern for the same sorts of things where serialization works.

brian Fri 2 Jul 2010

Thoughts on adding something like these to Obj? Is there any downside?

They can't go on Obj because Obj has well defined semantics that equals is based on reference equality unless a class explicitly overrides hash/equals.

qualidafial Fri 2 Jul 2010

In general my policy is to implement equals and hash only on const classes. For mutable classes you usually want instance equals.

qualidafial Fri 2 Jul 2010

Could we put this class somewhere in the examples? This mixin, although not as performant as manually written implementations, is a good jumping off point for fast prototyping. Depending on your application load it could be good enough for deployment.

Login or Signup to reply.