Try   HackMD

R&T / Deep Equality discussion

Three contrasting implementations

  • objects without identity
  • R&T as primitives
  • A new integrity level, a new equality operation (not used by existing maps and sets)
  • A new deep eq. operator

Question 1: Does a deep equality operator make Records and Tuples better in terms of implementation?

Problem 1: if R&T are objects, what happesn to builtin methods?

x = [#{a}];
x.includes(#{a}) // if this is an object, it will be false
x.includes(#{a}) // if this is a primitive, it will be true

a ~ b, where a = {}, b = {}, will be true

proposal: Change the semantics, to check for immutability

Proposal 2: Special casing builtin methods to treat records and tuples specially, feels wrong. We should instead introduce new methods that check deep equality, as they are functionally different.

x = [#{a}];
x.includes_deep(#{a}) // if this is an object, it will be true

proposal 3:

x = [#{a}];
x.includes(#{a}, (a, b) => a ~ b) // if this is an object, it will be true
y = [{a}];
y.includes({a}, (a, b) => a ~ b) // if this is an object, it will be true

Counter idea: If records and tuples are primitives, then one way we can implement deep equality is by casting to a record or a tuple for various complex types. This is equivalent to

x = {
    [Symbol[@@ToPrimitive]] () {
      return Record.toRecord(this);
    }
}

x === #{} // false, an argument for deep equality operator.
x =~= #{} // true

Object.deepEqual(x, y) // an alternative syntax
Object.deepEqual = function deepEqual(x, y) {
    return x[@@ToPrimitive](x) == y[@@ToPrimitive](y);
}

Idea: Introduce interned objects

Atomized objects are extractions of the observable surface area of an object. They are not equal to their source object, they are an immutable representation of their detectable surface area.

In this case - The R&T syntax becomes a sugar for atomized objects.

A shim:

// hidden object
var hashed = {}

// Note: this is not complete, obviously. We would have
// a different algorithm to do this. This is a short
// hand.
let PLACEHOLDER_SERIALIZER = JSON.parse;
let PLACEHOLDER_IDENTITY_EXTRACTOR = JSON.stringify;

// the api
function intern(object) {
  if (object !== Object(object)) {
      return object;
  }
  let identity = PLACEHOLDER_IDENTITY_EXTRACTOR(object);
  if (hashed[identity]) {
    return hashed[identity];
  }
  let newObj = Object.create(null)

  let jsonObj = PLACEHOLDER_SERIALIZER(identity);
  let props = Object.getOwnPropertyNames(jsonObj);
  for (let property of props) {
    newObj[property] = jsonObj[property];
  }
  // Note: in this case, objects are frozen for their
  // identity to work. However, an alternative is possible.
  // If the object is modified, it's identity is 
  // different, and it will be a copy. 
  // Immutability is not required for this 
  // to work, but it makes this easier.
  Object.freeze(newObj); 
  hashed[identity] = newObj;
  return hashed[identity];
  // Note, also not implemented here: we don't do 
  // memoization, but this would be a core part of what is 
  // built in here. 
}

Example with proxies:


const target = {
  message1: "hello",
  message2: "everyone"
};

const handler1 = {};

const proxy1 = new Proxy(target, handler1); 

atomize(target) === atomize(proxy1); // true

What we introduce is not immutability, but object identities tied to structure.

https://github.com/PapenfussLab/bionix

Question 2: Does having a deep equality operator make other things in the language better?

  • Deep equality is interesting when you are comparing many things, many times. if this is the case, then you can do the atomization technique.
  • With the toPrimative, it is difficult to tell apart the difference that you would have if the records and tuples proposal went ahead as a primitive.

rough syntax

a ~ b
a ~= b
a ==== b
//etc. 

Notes:

  • Functions in general do not satisfy deep equality
    • therefore getters do not either. Or any function on an objects as an ownProperty.
    • This will, as a consequence, disallow proxies.
  • Deep equality should not have the side effect of creating properties as it is checking.

The shim so far

function externalEq(a, b) {
    return someEquality(a, b);
}

function someEquality(a, b, past = []) {
  if (past.includes(a)) {
    if (a === b) {      
       return true;
    }
    return false
  }
  console.log("comparing ", a, b);
  if (a !== Object(a)) {
    if (a === b) {
      return true
    }
    return false
  }
  if (a[Symbol["StructEq"]] === b[Symbol["StructEq"]]) {
    if (a[Symbol["StructEq"]]) { // Callable?
      return a[Symbol["StructEq"]](b);
    }
  } else {
    return false;
  }
  if (Object.getPrototypeOf(a) !==  Object.getPrototypeOf(b)) {
    return false;
  }
  if (typeof a ===  "function") {
    return a === b;
  }
  let aProps = Object.getOwnPropertyNames(a);
  if (aProps.length != Object.getOwnPropertyNames(b).length) {
    return false;
  }
  for (const property of aProps) {
    if (property === "prototype") {
      continue;
    }
    if (!Object.hasOwn(b, property)) {
      console.log('different hasown ', b, property)
      return false
    }
    let testedPropA = a[property];
    let testedPropB = b[property];
    if (testedPropA !== Object(testedPropA)) {
      if (testedPropA !== testedPropB) {
        console.log('different prop value: ', testedPropA, testedPropB)
        return false;
      }
    }
    if (testedPropA === a && testedPropB === b && a === b) {
      continue;
    }
    console.log("object test, ", property);
    if (property === "constructor") {
      if (testedPropA !== testedPropB) {
        return false;
      }
      continue;
    }
    past.push(a);
    if (!someEquality(testedPropA, testedPropB, past)) {
      console.log('structural equality failure: ', testedPropA, testedPropB)
      return false;
    }
  } 
  return true;
}