Equality vs Identity in SuperCollider Documentation Enhancements
Overview of Equality (==
) and Identity (===
)
Equality (==
) in SuperCollider checks whether two objects have equivalent value or content, as defined by their class. By default, in class Object
, ==
falls back to identity comparison (pointer equality), but many classes override it to compare internal values. For example:
- Numeric classes consider different numeric types equal if their values match (
5.0 == 5
yields true) while still being distinct objects (5.0 === 5
is false). - Two separate arrays or strings with the same contents will compare equal with
==
even though they are not the same instance in memory.
In short, ==
is an equivalence relation defined per class â reflexive, symmetric, transitive â intended to answer if two objects âmeanâ the same thing.
Identity (===
) checks if two references point to the exact same object in memory. This is a low-level comparison (implemented via a primitive) that cannot be overridden by user classes. Identity returns true only if both operands are literally the same instance (same pointer). For example:
- Two variables referencing the same array will be identical (
a === b
is true ifb = a
), but two separately constructed arrays with identical contents will not (a === c
is false ifc = a.copy
or similar).
Identity is a stricter condition than equality â it implies equality in most cases (identical objects generally have equal content by definition), but the converse is not true. SuperCollider uses ===
for cases where you need to distinguish actual object identity, independent of value.
Key Behavior Differences
- Value vs. Reference:
==
asks if objects have equal value, whereas===
asks if they are the same object (reference). - Class-dependent Equality: What âequalâ means for
==
depends on the class. By default, itâs identity, but many classes provide a custom definition. For===
, the definition is uniform: a direct memory reference comparison. - Examples:
"hello" == "hello"
is true (two separate strings with the same characters), but"hello" === "hello"
is false (different string objects).5.0 == 5
is true (Float vs Integer with the same numeric value), but5.0 === 5
is false (distinct objects/types).- Two identical arrays
a = [1,2]
andb = [1,2]
:a == b
is true (element-wise equality), whilea === b
is false. - Symbol literals are singletons, so
\symbol === \symbol
is true (both references point to the unique interned symbol).
Technical Details and Edge Cases
Default Implementation
In class Object
, ==
is implemented simply as ^this === obj
. This means if a class does not override ==
, it effectively uses identity comparison by default. Many core classes override ==
to implement meaningful equality:
-
Numbers:
Integer
andFloat
override equality so that integers and floats can be compared across types by value. For instance,1 == 1.0
returns true because both represent the numeric value 1. However, be careful mixing numeric types: extremely large integers not exactly representable as 64-bit floats, or special values like infinities/NaNs, may behave subtly. Notably, floating-point NaN is never equal to itself â sonan == nan
is false. -
Symbols and Strings: SuperCollider treats Symbols as unique interned strings. A given symbol literal (e.g.
\name
) is a singleton â any occurrence refers to the same object in memory. Symbols with the same characters are identical (\name === \name
is true) and equal. Symbols and Strings are considered equal (==
) if their text is the same, even though they are different types. For example,\foo == "foo"
yields true â the symbol is equal in content to the string, but they are not identical objects (\foo === "foo"
is false). -
Collections (Arrays, Lists, etc.): Collection types generally override
==
to perform element-wise comparisons. An Arrayâs equality will recurse into its elements: e.g.,[[1],[2]] == [[1],[2]]
returns true â it checks that both outer arrays have two elements and each pair of corresponding inner arrays are equal in turn. This is deep equality. Deep comparisons can be expensive for large or nested structures, and they do not detect cyclic references. SuperColliderâs==
implementation does not explicitly guard against cycles, so users must avoid comparing cyclic structures manually. -
Dictionaries and Sets: Dictionary (and ordinary Set) use
==
for key lookup and membership. Two keys are considered the same if==
returns true. Using an immutable object (e.g., a Symbol or number) as a key means any other object equal in value will retrieve the same entry. In contrast, IdentityDictionary (and IdentitySet) use===
for keys, requiring the exact same object instance to consider it a match. Identity-based containers wonât merge entries even if they are âequalâ in value.
Singletons and Special Objects
Certain objects in SC are unique by design:
-
nil
and Booleans:nil
is a singleton (likenull
in other languages). Thereâs only onenil
object, sonil == nil
andnil === nil
are both true. Similarly,true
andfalse
are singletons; any reference totrue
points to the one true object, etc. Thustrue === true
is true, andtrue == true
is also true. -
Symbols: As mentioned, symbols are interned. If you create a symbol from a string at runtime (
Symbol("hello")
), it will return the existing interned symbol if one already exists, or create and intern a new one. This unique-interning property means symbols can be compared either by==
or===
. -
The Server object:
Server.default
returns the singleton default server instance. Repeated calls give the same object, soServer.default === Server.default
is true. Many system resources in SC (servers, audio buses, etc.) are managed as unique objects, using identity to check for âthe same resource.â
Immutability and Identity
Most of SCâs core types are mutable (except Symbols, numbers, and a few others). If two objects are equal (==
) but not identical, they represent the same content at that moment â changes to one will not affect the other.
Example:
a = [1,2,3];
b = [1,2,3];
a == b; // true initially
a.add(4);
a == b; // becomes false, because b remains [1,2,3] while a becomes [1,2,3,4].
Floating-Point Precision Pitfalls
Comparing floating-point numbers for equality is error-prone due to binary rounding:
0.1 + 0.1 + 0.1 == 0.3 // might be false!
Use a tolerance or .fuzzyEqual
method instead of ==
for float comparisons.
Special Case â Functions, Routines, and Streams
Functions (Function
), routines (Routine
/Thread
), Patterns, and UGens typically inherit Object ==
(identity) as they lack universal equality semantics. Two functions are equal only if theyâre the same object in memory.
Overriding ==
in Custom Classes
If you create your own classes, you can override ==
for custom equality logic. Important: Whenever you override ==
, you must also override the .hash
method to maintain consistency.
Overriding ===
(Identity)
Technically, you can override ===
in a class, but this is strongly discouraged. The base implementation is primitive and expected to check object identity directly.
Practical Implications in Real-Time Audio Programming
Understanding ==
vs ===
is crucial for both correctness and performance in SuperColliderâs real-time audio programming:
- Pattern and UGen Comparisons (Lazy Evaluation): Directly using
==
to compare signal values in the server or future values in a pattern is ineffective.
- The issue stems from inheritance: == doesnât evaluate arguments, while >= and <= do
-
Object Lifetimes and Node Identity: Nodes in real-time audio contexts require tracking of their exact object rather than relying on equality for identification. Use
===
or IdentityDictionary to store these objects. -
Memory Efficiency â Reusing Objects vs. Copying: Identity checks help optimize programs. Checking identity prevents unnecessary duplications and allows effective resource management.
-
Performance Considerations: Identity comparison (
===
) is a constant-time operation. In contrast, equality (==
) might be costly depending on the objectâs complexity.
Theoretical and Paradigm Perspectives
- Mathematical vs Programming Identity: Programming identity concerns actual entities, unlike mathematical identities, which focus on algebraic properties.
- Functional Purity: Pure functional languages de-emphasize identity. SuperColliderâs stateful nature makes identity
The distinction between equality and identity in SuperCollider mirrors fundamental concepts in programming language theory and mathematics:
-
Mathematical vs Programming Identity: In mathematics, an âidentityâ often refers to an equation that is true for all values (or an identity element in algebra, etc.), which is a different notion than object identity. Mathematically, one might say two expressions are identically equal if they can be shown to be the same under all conditions (e.g. two formulas that simplify to each other). In programming, identity is not about an algebraic property but about an actual entity. A helpful mental model: think of objects as real-world entities with an identity (like two cars of the same model: they may be equal in many attributes but are distinct individuals). Equality is then a property or relation comparing some aspects (like two cars might be equal in color and make).
-
Functional Purity: In purely functional languages (like Haskell), the concept of identity is deemphasized or absent for most values. If a function returns a new value equal to an old value, you typically donât care if itâs âthe same instanceâ because there are no side effects or mutability â you only care about the value. In Haskell, one usually cannot observe object identity at all (there is no
===
equivalent by default). In fact, two separate lists[1,2,3]
and another[1,2,3]
are not distinguished by any observable behavior (they will compare equal and you canât mutate them to tell them apart). However, Haskell does have ways to check identity indirectly (likeStableName
orunsafePtrEquality#
), mainly for optimization or foreign interface purposes. This is considered advanced usage. The takeaway is: in a purely functional paradigm, equality is typically structural/value-based, and identity is only relevant for things like I/O handles or mutable references (IORef
) where identity matters. SuperCollider, by contrast, is an impure, stateful language â objects can have mutable state and side effects. Therefore, identity is a first-class concept. When you set a Nodeâs parameter or update a GUI object, you refer to that specific objectâs state. If SC were pure, you could replace an object with an equal one freely; but in SC, replacing an object with an equal one might break things if other parts of the code were expecting the exact same instance (for example, replacing a live Synth object with an equal one doesnât make sense â you need the actual object to free it). -
Category Theory Analogy: In category theory, the term identity refers to identity morphisms (identity arrows) â essentially the do-nothing transformations on objects. The concept of two objects being âidenticalâ in category theory is actually not straightforward; typically one speaks of objects being isomorphic rather than equal, unless working in a specific foundational framework where objects can be equal. If we stretch an analogy: equality in programming is like an equivalence relation (comparable to an isomorphism class â two different objects can be considered âthe sameâ under some equivalence), whereas identity is a stronger notion (comparable to the idea of the object itself, akin to a unique identity arrow from the object to itself). This analogy is loose, but can help: SCâs
==
defines an equivalence relation on objects (partitioning objects into classes of equivalence â e.g., all strings equal to âhelloâ), whereas===
partitions objects in a trivial way (each object is only equivalent to itself). In formal terms,===
is congruence on reference identity. -
Type Systems and Equality: Different programming languages handle equality and identity differently:
- In Java, for instance,
==
on objects checks identity (reference equality), while.equals()
is a method that classes override for value equality. Python uses==
for value equality andis
for identity. SuperColliderâs approach is more akin to Smalltalkâs: it provides separate operators/messages for equality vs identity (Smalltalk uses=
for value equality and==
for identity; SC uses==
and===
respectively). This separation is generally considered good practice, as it makes code intentions clearer. SCâs design ensures you can always fall back to identity (===
) even if a class overrides==
. It also means equality can be customized per class without losing a way to distinguish instances. In a strong type system (like in some ML languages or Rust), equality might even be restricted â e.g., you cannot compare functions or opaque types for equality unless they implement a specific trait. SC being dynamically typed doesnât enforce such rules, but conceptually, trying to use==
on something like a Function is undefined behavior in terms of value (so it defaults to identity).
- In Java, for instance,
In summary, understanding these broader perspectives reinforces why SC has both operators. It lets the language be flexible (you can define what âequalityâ means for new data types) while also pragmatic (you can always check if two references are the same underlying thing). It reflects the balance between abstract value-oriented thinking and concrete resource management in a real-time system.
Suggestions for Documentation Improvements
To enrich the official documentation on ==
and ===
, we can incorporate the following enhancements:
-
Clarify Class-Specific Behavior: Expand the documentation to list common classes and how they implement
==
. For example, explicitly mention:- Numbers: numeric equivalence across
Integer
andFloat
(with a note about precision limits). - Collections: deep content equality for arrays, lists, etc., vs. the existence of
IdentityArray
(if any) or identity sets. - Strings and Symbols: mention that string equality is based on characters, and that symbol equality works similarly but symbols are interned (perhaps include the example that
'a' == "a"
is true, and why). - Booleans and nil: trivially equal to themselves.
This gives users a quicker reference without guessing which classes override equality. The current doc shows examples, but a structured list or table could improve clarity.
- Numbers: numeric equivalence across
-
Mention Hash Contract and IdentityHash: The note about overriding
hash
when==
is overridden is crucial â keep that, and perhaps also mention the existence of.identityHash
explicitly. Users might not realize SC has bothhash
andidentityHash
. Including the example from the class library (e.g.,"hello".hash == "hello".hash
true, but"hello".identityHash == "hello".identityHash
false) is very illuminating. It shows how two equal strings share a hash but differ in identityHash. This drives home how identityHash is unique per object and that identity implies identical identityHash. Also, documenting thatidentityHash
is not user-overridable (and shouldnât be) can prevent confusion like the case on the forum where someone tried to override it. -
Emphasize Identity Containers: In the
Dictionary
andIdentityDictionary
help (and Set/IdentitySet), ensure cross-references. The IdentityDictionary help does note the difference, but the main Object help for==
/===
could include a brief note: âFor container classes, note that Dictionary/Set use==
for key matching, while their Identity counterparts use===
. Use IdentityDictionary when you need keys distinguished by instance, which trades away merging of equal-valued keys for speed.â This guides users to the right choice of data structure and reveals a practical nuance of how equality is used in SCâs collections. -
Add Real-World Examples and Pitfalls: The documentation would benefit from examples that illustrate subtle behavior:
-
Float precision pitfall: Show an example where two floats that print the same are not equal (e.g.
0.1 + 0.2 == 0.3 // false
), followed by using a tolerance orfuzzyEqual
. This warns new users of a common mistake in audio timing or DSP calculations. -
UGen comparison issue: As noted, many users eventually wonder âhow do I compare a synthâs parameter or two signals?â. The docs could proactively mention that
==
is not available for signal comparison and point toBinaryOpUGen("==", a, b)
as the solution. Perhaps in the Operators help or in a âCommon Mistakesâ section, say: âTrying to use==
on UGens will not create a comparator UGen; it will only tell you if two UGen objects are the same instance (which is rarely the case). To compare audio or control signals, use a BinaryOpUGen or language-level logic with.poll
for debugging.â This addition could save users confusion. -
Mutability and equality change: An example:
a = [1,2,3]; b = a.copy; // b == a initially true a.add(4); a == b; // now false, since a changed
This demonstrates how two objects that were equal can cease to be equal after a mutation (while of course
a === b
was never true). It reinforces the conceptual difference between sharing an object vs copying. -
Equality of events or dictionaries with mixed key types: e.g.,
e = (foo: 123); e["foo"] == e[\foo]; // true, string key and symbol key refer to same entry
This shows how
==
is used in lookup (string and symbol considered equal in that context). It also subtly highlights that Event (a subclass of IdentityDictionary) is doing some magic to allow that, likely by converting or by Symbol == String logic.
-
-
Structuring by Use-Cases: The help could be structured into sub-sections or bullet points like in the forum post:
-
When to use
==
: comparing numeric values, structural comparisons, checking if two different objects have equivalent content, most typical use in conditional logic, etc⌠-
When to use
===
: checking if something is exactly the same object (e.g., to see if a function returned a reference to a cached object or a new one), working with identity-based collections, ensuring a singleton (likeServer.default
) is being referenced and not a copy, or for performance when you know equality would do a deep comparison unnecessarily. For instance, if you have a loop that needs to frequently check if a reference has changed, using===
is optimal. -
Common mistakes: Floats, as mentioned; forgetting to override hash; using
==
where===
was intended (and vice versa). Some beginners accidentally use=
(assignment) when they meant==
â the docs could note: âRemember,=
sets a variable,==
compares values.â It sounds obvious but in some languages (e.g., Pascal) itâs different, so it can trip newcomers. -
Best practices: e.g., âIf two objects have a conceptual âidentityâ in your program (like a synth node, a GUI widget), use identity comparisons to track them. If you only care about an objectâs data (like a list of numbers), use equality.â Also, âUse symbols for keys to leverage identity lookups, but be mindful that symbols persist for the runtime duration (they are not garbage-collected), so donât generate tons of unique symbols dynamically.â (Thatâs another subtle point: every new Symbol sticks around; strings can be freed. So if a program generates unique symbols in a loop, thatâs a memory leak of sorts. The documentation on Symbols covers this, but a tie-in here could be helpful where performance is discussed.)
-
-
Link to Community Resources: The official docs could link to the relevant sections of the SuperCollider Book or tutorials where equality is explained, or even to the community wiki/FAQ if one exists. For instance, a link to a guide or FAQ on â== vs ===â would be useful. Since we have a well-structured forum summary, perhaps the documentation could incorporate some of that knowledge (as we are doing here). Direct links to GitHub issues or forum posts might not be stable for official docs, but referencing them in release notes or developer commentary could help advanced users find more information. For example, a brief mention: âFor an in-depth discussion of equality/identity and their theoretical background, see [this thread on scsynth.org]âŚâ could be given as a footnote.
-
Unresolved Questions: A section could outline any known limitations or debates:
- Floating point display vs equality: (This was debated and resolved to keep things as is â users should just be educated that equality on floats is exact).
- No structural equality for functions: It might be noted that SC does not attempt to define
==
for Functions beyond identity â i.e., two function objects are only equal if theyâre the same object. This is by design, since determining if two arbitrary functions do the same thing is generally impossible (undecidable problem). - Cyclic data structures: Caution that
==
may not terminate on self-referential structures (though this is an edge case). - There arenât major open issues in the SC GitHub about changing
==
/===
semantics as of latest versions â itâs stable. However, if there were any proposals (for SC 4 or a future version) such as introducing value types or changes in equality, those could be hinted at. Currently, it appears the consensus is that the model is fine, just needs better explanation to users.
By integrating these points, the documentation will not only cover the basic usage of ==
and ===
but also guide the reader through subtleties (like numeric precision, identity vs equality in collections, etc.) and practical advice on when to use which operator. This richer explanation will benefit both newcomers (preventing common misunderstandings) and advanced users (shedding light on performance and deeper implications). The goal is a clear, scan-friendly reference with examples and bullet points â much like the structure above â so that users can quickly find answers to âAre these two things equal or the same?â in any given scenario.
References and Further Reading:
- SuperCollider Help: Object - Equality and Identity (
==
,===
definitions and examples) - SuperCollider Help: Object - Hash and IdentityHash (guidelines for overriding and identity semantics)
- SuperCollider Help: BinaryOpUGen (note on
==
not producing a UGen, with example of usingBinaryOpUGen('==', a, b)
) - SuperCollider Help: Control Structures (floating-point comparison note:
(2/3) == (1 - 1/3) is false
) - SuperCollider Help: IdentityDictionary (keys match by identity vs Dictionary by equality)
- Community Forum: âUnderstanding Equality and Identityâ â in-depth discussion with examples of singletons, collections, pattern keys, etc.
- Community Forum: Float Precision/Display â confirmation that users should avoid strict float equality and the rationale behind not showing full precision by default
- GitHub Issue #1875: Float rounding and equality â example where
(0.1+0.1+0.1).round(0.1)
vs0.3
led to false, illustrating precision issues - Community Q&A: IdentityDictionary with custom keys â explanation that identity truly means same instance, overriding
===
doesnât change IdentityDictionary behavior (reinforcing that identity is fundamental).