[Grace-core] egal, methods, and closures

Michael Homer mwh at ecs.vuw.ac.nz
Wed Oct 19 19:46:39 PDT 2011


Hi,
In trying to implement egal I've run into some conceptual issues where
I'm not sure what the right approach is. In particular, I don't know
when two nontrivial objects should be considered egal or not.

The specification currently defines egal for immutable objects as
being the case if "they are of the same 'shape' and if their fields'
contents are egal", and says mutable objects use identity as the sole
basis of comparison. My question arises when these objects have
methods, and then further when those methods are closures.

There are these objects, which clearly should be egal:
  object {
    def a = 1
    def b = 2
  }
  object {
    def a = 1
    def b = 2
  }
And this, which by definition is egal only to itself (pointer equality):
  object {
    var a := 1
    def b = 2
  }

In between those extremes are more complex objects containing methods.
These two objects seem like they should be egal:
  method create {
    object {
      def a = 1
      def b = 2
      method sum { a + b }
    }
  }
  def x = create
  def y = create
  x == y // true

What about textually-equivalent object literals?
  def a = object {
    def a = 1
    def b = 2
    method sum { a + b }
  }
  def b = object {
    def a = 1
    def b = 2
    method sum { a + b }
  }
  a == b // ??
The Baker paper would have these be egal (in my reading), because the
code of "sum" is EQ. That is much more straightforward when you're
comparing S-expressions, though, and it seems problematic when objects
don't carry their source code around with them.

Or methods returning constants?
  object {
    def a = 1
    def b = 2
  } == object {
    def a = 1
    method b { 2 }
  } // Probably should be false

When closures are involved, even the obvious cases aren't necessarily clear:
  method create(first : Number, second : Number) {
    object {
      def a = first
      def b = second
      method sum { a + b }
    }
  }
  def x = create(1,2)
  def y = create(1,2)
  x == y // Probably still true
But:
  method create(first : Number, second : Number) {
    object {
      def a = first
      def b = second
      method sum { first + second }
    }
  }
  def x = create(1,2)
  def y = create(1,2)
  x == y // Probably false

When they mutate distinct external state they should clearly be
treated as different:
  method create(first : Number, second : Number) {
    var thesum := 0
    object {
      def a = first
      def b = second
      method sum { if (thesum == 0) then { thesum := a + b } ;  thesum }
    }
  }
  def x = create(1,2)
  def y = create(1,2)
  x == y // false
These cases are functionally the same as a var declaration, so it
makes sense to treat them in the same way. Multiple objects mutating
the same value is another question, as is when the mutation itself
occurs somewhere else, and it's tough or impossible to distinguish
those.

In real code, it's likely that a large proportion of methods will be
closures of some sort, referring to methods or classes from the
standard library or elsewhere in the code:
  method create {
    object {
      def data = [1, 2, 3, 4]
      method ascending { Lists.isSorted(data) }
    }
  }
  def x = create
  def y = create
  x == y // ??

The obvious answer is that any object with a closure is unegal, and
methods are otherwise compared by pointer. It's then impossible to
define an object egal to one defined elsewhere in the code or in a
library if that object contains methods. Objects defined at the same
call-site would be unegal if they referred to anything not explicitly
made part of their representation with def. That may be the right
answer, but it also leads to some behaviours that might be unexpected.
It does seem to limit the usefulness of value objects, or to restrict
them in practice to only defs even if they are theoretically more
capable.

Another option would be to say that objects containing real methods
always use identity comparisons, and def-only objects are the special
case, which would at least be consistent (but which the Baker paper
dismisses as "throwing the baby out with the bathwater"). Comparing
methods and closures in some more rigorous way is the third option,
but exactly what that way should be for Grace I don't know. Baker's
paper brings the problem down to comparing code (S-expressions) and
environments, where the environment is a set of named pointers to
immutable values and mutable cells. Comparing both methods themselves
and associated data using pointer equality is another possibility,
which will usually turn out the same as object identity except in the
presence of inheritance.

On the closure issue, there is an interesting workaround by making the
external value part of the object's representation:
  method create {
    object {
      def data = [1, 2, 3, 4]
      def Lists' = Lists
      method ascending { Lists'.isSorted(data) }
    }
  }
  def x = create
  def y = create
  x == y // Probably true

I don't know that that's something to encourage, but it does sidestep
the problem. It doesn't help with redefining methods, unless there's
some way to obtain a method pointer and assign it into an object.

In any case, I'm interested in what the right approach is here. I lean
towards "only defs are egal" on the basis of consistency and
explicability, but it does force you to have state and behaviour
separated for value objects, which I'd prefer not to have.
-Michael


More information about the Grace-core mailing list