ERights Home elang 
Back to: Kernel Reference x On to: Scalar Data Types

When Are Two
Things the Same?


E has two flavors of built-in equality -- synchronous and asynchronous. The next chapter covers the asynchronous flavor, join. This chapter covers the synchronous flavor, "==", which asks if two references are known to be the same at this moment. Indeed, in E, the expression "a == b" is pronounced "Is a the same as b?"

Note: This chapter is written ahead of the implementation. The behavior it specifies is not yet implemented. If you see problems with the specification, please comment on it soon, so we can avoid implementing down blind alleys. Thanks.

The examples below that don't yet run as shown are colored red.

The semantics of sameness corresponds to the equality we learned in high school algebra: If a == b (if a is the same as b) then if you take a system and magically replace any number of one with the other, the resulting system must be equivalent (mean the same thing) as the original system. Here system means computation state. This strong requirement is incompatible with the normal object-oriented practice of asking one object if it's equal to another, since the object might lie.

Sameness is Reflexive

If "a" is in scope and settled, "a == a" is always true. Mathematicians call this property reflexiveness. (***reflexivity?) (If "a" isn't settled, then "a == a" will throw a NotSettledException. In no case will "a == a" ever be false in E.) Believe it or not, reflexiveness has one odd effect:

? pragma.syntax("0.8")

? def a := 0.0/0.0
# value: NaN

? a == a
# value: true

In all ways besides sameness, the behavior of E's floating point values corresponds exactly to the subset of the IEEE spec implemented by Java. However, IEEE specifies that "NaN == anything" is always false. What is were E to adopt this rule and so lose reflexivity? Consider:

? def m := [].asMap().diverge()
# value: [].asMap().diverge()

? m[a] := "foo"
# value: "foo"

? m[a]
# value: "foo"

During lookup, the map m needs to test if the key provided is the same as any of the key-value pairs it contains. Only if "==" is guaranteed to be reflexive will the above code return "foo" independent of the value of a. E provides this guarantee.

For an IEEE conformant comparison, use instead E's "<=>" operator, pronounced "is the same magnitude as". It is one of E's magnitude comparison operators along with "<", "<=", ">=", and ">". Like other operators in E, but unlike "==", the magnitude comparison operators expand to message sends to the left hand operand, so the result is according to the left hand operand.

? a <=> a
# value: false

When are Scalars the Same?

As documented here, E has four scalar data types, integer, float64, boolean, and char. In all cases, two of these are the same if they are the same type and have the same value.

? 2 == 2
# value: true

? 2 == 3
# value: false

? 0 == 0.0
# value: false

? 'a' == 'a'.asInteger()
# value: false

Surprisingly, when applied to float64s, this rule has some counter-intuitive effects.

Selfish and Selfless Objects

For a surprise, let's revisit our familiar makePoint example:

? def makePoint(x, y) :any {
>     return def point {
>         to getX() :any { return x }
>         to getY() :any { return y }
>         to __printOn(out) :void {
>             out.print(`<$x, $y>`)
>         }
>     }
> }
# value: <makePoint>

? def a := makePoint(3, 5)
# value: <3, 5>

? def b := makePoint(3, 5)
# value: <3, 5>

? a == a
# value: true

? a == b
# value: false

Why false? In the corresponding situation, Java, Smalltalk, Python, and most other object languages would also say false, and for a similar reason. Each newly created object has, besides its state and behavior, a unique identity, or self, endowed by the act of creation. "==" is asking not "Do a and b point at objects with the same contents?" but "Do a and b point at the same object, that is to say, an object with the same identity?". In these languages, we say that "==" compares object identity, rather than object contents.

In E likewise, between two objects with identity -- or selfish objects -- "a == b" justs tests whether both pointers designate the same object identity. All objects, such as the above points, are selfish unless declared selfless. The scalars explained above are a kind of selfless object. Scalars are the same based only on their value, not their identity, since they have no identity. Just as 3 is only 3, without a separate identity, Euclid and Decarte would find it strange for one <3, 5> point wasn't the same as another. Here's how you'd create mathematically respectable selfless points:

? pragma.enable("meta-scope")

? def makePoint(x, y) :any {
>     return def point implements PassByCopy {
>         to getX() :any { return x }
>         to getY() :any { return y }
>         to openState() :any {
>             return meta.getState()
>         }
>         to openSource() :any {
>             return meta.context().getOptSource()
>         }
>         to __printOn(out) :void {
>             out.print(`<$x, $y>`)
>         }
>     }
> }
# value: <makePoint>

? def c := makePoint(3, 5)
# value: <3, 5>

? def d := makePoint(3, 5)
# value: <3, 5>

? def e := makePoint(7, 5)
# value: <7, 5>

? c == c
# value: true

? c == d
# value: true

? c == e
# value: false

The previous a and b were both <3, 5> points, but were distinct because they were different objects, created separately. c and d are simply <3, 5> points, with no other hidden information.

An object is selfless if it is the value of an object expession that is declared selfless, as shown above. This declaration is only accepted as valid after three conditions are checked. Before listing the conditions, we first define an instance variable of an object expression as a variable used within the object expression whose corresponding definition is outside the object expression, though not in the universalScope.

  • Immutable. All instance variables of a selfless object must be defined as final, although their values may be mutable objects.
  • Open State. There must be a openState method exactly as shown above. The body of this method provides to the caller the current scope. Since there are no intervening variable definitions, this scope hold exactly the point's instance variables. Objects are generally encapsulating, meaning they can have and use instance variables that they deny their callers access to. Open State ensures that a selfless object is non-encapsulating.
  • Open Source. There must be a openSource method exactly as shown above. The body of this method provides to the caller the source of the enclosing named object expression. Objects are generally polymorphic, meaning their external behavior does not uniquely determine an implementation of that behavior. Open Source ensures that a selfless object is non-polymorphic.

We say that an object is transparent if it is both open state and open source.

By declaring an object selfless, you ensure that it is immutable and transparent, you cause it to pass-by-copy between Vats, you cause its sameness to be tested by contents comparison, and you cause its selflessness to be apparent.

By being immutable, we know that if c's contents and d's contents are the same now, they'll be the same later. By being transparent, we know that there are no encapsulated secrets for a contents comparison to reveal. Therefore, it's safe for this object to give up its identity, and use its contents for its sameness check.

When an object is immutable, and lacks an identity, then it cannot be distinguished from an equivalent copy of itself. A local implementation should feel free to copy such objects at will, or collapse equivalent copies into one representation, as in neither case can it be caught. However, if an encapsulating object were implicitly copied between machines, this would violate the encapsulation expressed by the E program, since an untrustworthy recipient machine would be improperly gaining access to the contents. Since E's selfless objects are also transparent, a distributed implementation is also free to copy them. Indeed, in Reference Mechanics we'll see that it's often obligated to.

Collections

? [2, 3] == [2, 3]
# value: true

? def cl := [2, 3]
# value: [2, 3]

? def fla := cl.diverge()
# value: [2, 3].diverge()

? def flb := cl.diverge()
# value: [2, 3].diverge()

? fla == fla
# value: true

? fla == flb
# value: false

? fla.snapshot() == flb.snapshot()
# value: true

Infinite Rational Selflessness

Broken References

Deferred References vs Stable Answers

Join: Asynchronous Sameness

Cooperatively-Transparent Forwardability

 

 
Unless stated otherwise, all text on this page which is either unattributed or by Mark S. Miller is hereby placed in the public domain.
ERights Home elang 
Back to: Kernel Reference x On to: Scalar Data Types
Download    FAQ    API    Mail Archive    Donate

report bug (including invalid html)

Golden Key Campaign Blue Ribbon Campaign