The distinction between identity and equality connects to wider issues in philosophy, logic, and computer science.1
The present discussion is more narrowly concerned with an overview of how the distinction is understood in SuperCollider.
A factory may produce thousands of screwdrivers each day that are all equal to each other with respect to some specification; but each single screwdriver is identical only to itself (because it occupies this particular location at this particular time, or has a particular serial number stamped on it). Thus we can say that identity, the relation of an object to itself, is a stronger relation than equality, the relation of an object so some other object with respect to a common property.
Likewise, in SuperCollider, identity of objects created in sclang is accounted for through their location in memory.2
In the above example, a and b were different instances of the Array class, each with their own memory address. You can query said address through the .dump method:
For newcomers to programming, it may be sufficient to be aware of this difference:
The === (identity) and == (equality) methods are not interchangeable, and mean different things.
In most cases, == behaves as "naively" expected. For instance, from high school math, we expect 5 == 5.0 to return true; and it does.3
The behavior of ===, on the other hand, depends more on lower-level implementation, and in some cases may at first seem counter-intuitive.
The following two examples illustrate the difference further with respect to two relevant use cases: Copying the contents of a variable rather than merely assigning it to another variable (a distinction fundamental to programming, not just in SuperCollider); and indexing into an instance of IdentityDictionary or Event (specific to SuperCollider).
The above example assigned each variable to an instance of an array [1,2,3], and demonstrated that the variables now point do distinct memory locations, hence their contents are not identical. However, when a variable is assigned to another variable rather than to an object instance, no new instance is created; instead, both variables will refer to the same object, at the same location in memory. This is relevant in practice when you want to modify a given list in multiple distinct ways, but that list only exists in one variable. The problem then becomes one of creating multiple equal but non-identical instances from just this one variable. The following example does not work as desired:
Stacked fourths are nice, but we wanted to be boring here and just use triads. What we need to do is use the .copy method to create versions of the Array object referenced by x that are equal but not identical to it, so we can then modify them separately:
Thus, the distinction between equality and identity is implicit in such simple instructions such as "paint this screwdriver green, and that one red." (Computers just need to be told these things in more formal terms.)
In Dictionary and its subclasses, the choice of lookup algorithm can impact performance. Using identity for this algorithm, as IdentityDictionary and Event do, is generally faster than usign equality, as Dictionary does. Therefore when using IdentityDictionary and Event keys should not be Strings, but Symbols. The following example demonstrates why:
Why does Symbol work to index into an IdentityDictionary, while String does not? Because each Symbol has a unique representation such that 'symbol' and 'symbol'will always point to the same memory location, regardless of where they sit in the code. Thus, the instance that is passed to the argument of the .at method of the dictionary is identical, rather than just equal, to the instance that serves as the key argument to the dictionary.
Other examples of classes with unique representations are SimpleNumber and its subclasses (Integer, Float): Each reference to a Float of a given value (say 1.234) points to one and the same instance. (Of course, this instance is not identical to floats of a different value, e.g. 1.23400001.)
As we have seen, this was not the case for String, where each occurrence of a String in the code will create a new instance rather than pointing to an exiting instance of the same value. It is also generally not the case for Collections. We have already seen this for Arrays; here is a similar example for Sets.
Not all classes explicitly implement a == method. In that case, == simply defaults to ===. An instance of this is Function,4 where the following behavior may be surprising:
To understand this behavior, we must know two things: First, instances of Function do not have unique representations, that is, like String (mentioned above), each {1} expression in the code is assigned a separate instance. Thus {1} === {1} will return false. Second, unlike String, Function does not implement a == method, and therefore its == defaults to ===. {1} == {1} is thus really a question about whether the two functions designate the same object instance in memory, i.e. whether {1} === {1}---which, as we just learned, returns false.
0.1 * 3 == 0.3 may return false. However, this is not strictly speaking an issue with equality, but with floating-point representation (see Float).