Understanding Equality and Identity

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 if b = a), but two separately constructed arrays with identical contents will not (a === c is false if c = 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), but 5.0 === 5 is false (distinct objects/types).
    • Two identical arrays a = [1,2] and b = [1,2]: a == b is true (element-wise equality), while a === 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 and Float 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 – so nan == 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 (like null in other languages). There’s only one nil object, so nil == nil and nil === nil are both true. Similarly, true and false are singletons; any reference to true points to the one true object, etc. Thus true === true is true, and true == 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, so Server.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 (like StableName or unsafePtrEquality#), 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 and is 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 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 and Float (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.

  • 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 both hash and identityHash. 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 that identityHash 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 and IdentityDictionary 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 or fuzzyEqual. 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 to BinaryOpUGen("==", 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 (like Server.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 using BinaryOpUGen('==', 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) vs 0.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).
4 Likes

This is great! Just a comment that occurred to me regarding the “mathematical” concept of identity.

Firstly, I think the problems people might run into when first encountering identity in programming is more due to their not thinking of identity in formal terms at all… that is, they do not come to programming with a category theory, or even math, background but rather with a more naive, “everyday” understanding of it. I would conjecture that if somebody already knows about category theory, they won’t really struggle with identity in programming, not because the respective conceptions of id in these fields are similar, but because they are use to thinking in more abstract, formal terms and are aware that id is in effect a technical term.

So in a way this could be less theoretically specific here, even to the point of saying something along the lines “identity in programming and mathematics is a technical term, and as such more clearly defined than the notion of “sameness” in everyday language, …”, and then put the category theory terminology in a footnote, and go on with the id function example.

(EDIT: cutting unnecessary thread-derailing bits)

I think the discussion re the id function is good (just the placement of the comment // properties of identity may be revisited, as the g function isn’t an identity, nor is x.)

1 Like

The idea was to have a supplement, another help document with proper definitions.

Do you see that as confusion or material for those wanting to understand the fundamentals?

Because I see a lot of confusion among all users, one usually picks a definition from one language or another, which has no real fundamentals. That’s why mathematics is the only reference.

We can discuss how each programming language design uses those ideas from there. Memory location will force those concepts to change a lot, for example.

It does not say g is an identity function, it means this:

identity function properties:

* Left identity: f(g(x)) = g(x)
* Right identity: g(f(x)) = g(x)

That’s where those concepts were developed:

Category Theory / Type Theory 101:

I am aware, but it may be confusing still. I imagine something like this could help:

// Properties of identity
g = { |x| x + 1 }; // given some arbitrary function g 
x = 42; // and some arbitrary x...

// Left and right identity is defined as follows:
f.(g.(x)) == g.(x);  // true - left identity
g.(f.(x)) == g.(x);  // true - right identity

Also, counterexamples help, but obviously they result in more text…

1 Like

It would also be worth mentioning that hash comparison doesn’t ensure either equality or identity, because of hash collisions.

Two unequal entities may have the same hash, simply because there are more than 2^32 possible entities (hence some of them must share a 32-bit int hash).

If hashes are unequal, then the entities cannot be equal. But if the hashes match, the entities may yet be different.

hjh

1 Like

Can we put it like this?

  • If two objects have different hashes, they are different
  • If two objects have the same hash, they might be identical (but aren’t necessary!)
  • Identity hashes are always unique for different object instances
  • Regular hashes are content-based and can have collisions

Pratical implication:

Hashing is very adequate for:

  • Dictionary/lookup table
  • Quick equality comparisons
  • Caching systems
  • Data integrity checks

Along with your explanation on why collisions can happen.

Can’t watch that video right now, and also I’m not sure if I understand your reply correctly here (what does “That” and “those concepts” refer to?). I assume you mean “Set theory was where those [category theory] concepts were developed.”, correct me if I’m wrong.

Can’t argue with that, but the notion of identity is a bit older than category theory and modern set theory. I’m not saying that it’s “wrong” to include category theory here, but more that in the context of an sc documentation, what matters is the contrast/distinction from understandings of identity that aren’t those operative in sc; and I’m not sure if this needs to be that specific. And this really only concerns the bit about objects and morphisms! For instance, the rest of the draft never reuses the term “morphism”, so that’s a good indication that it isn’t very helpful here.

On the other hand, I’d be very much in favor of including footnotes with links to encyclopedia entries, e.g. Identity (Stanford Encyclopedia of Philosophy) or Category Theory (Stanford Encyclopedia of Philosophy).
It’s just that these are long-standing philosophical issues that we can’t possibly adress usefully within the docs, so we should focus on the more general aspect, which in my opinion is just (for newcomers to programming and/or math) that formal notions of identity aren’t necessarily coextensive with everyday notions of identity.

1 Like

Set Theory was a serious problem on its basis. In computer science, I think we find profound research borrowing ideas from category theory (and I agree that it is overkill). But Set theory is not relevant, I think.

1 Like

(EDIT: cutting unecessary thread-derailing bits)

1 Like

1901 (way before the 1960s, as you mentioned).

1 Like

(EDIT: cutting unecessary thread-derailing bits)

1 Like

Well, we can enter a confusing territory here. Let’s go slow.

Today, Sets kinds of are category theory. And it happens in different ways.

First, small categories are actually “sets.”

Another example is just defining sets using category theory.

http://www.tac.mta.ca/tac/reprints/articles/11/tr11.pdf

So, yes, it can be used in many simplified contexts. But there is no benefit here.


I do not understand why you are discussing this.

Primarily because it’s interesting (as you too seem to think) but also because I have the reflex to correct people if they seem to imply that I am misinformed. Let’s leave it there.

1 Like

Thank you for this!
I would close my PR if you open a PR with it.
However, I have a question: Why is it necessary to mention the Haskell? To understand the Haskell part, the reader should already know the corresponding syntax and semantics in Haskell…

1 Like

It is not necessary. However, it is a good example of a programming language that uses another concept of identity and allows the simulation of how SC works (memory location).

It can be improved to make it more clear. It’s just a draft.

It could be an excellent example for those who are already familiar with Haskell. However, without an explanation of why the Haskell example is presented at this point, it might feel somewhat out of context. Additionally, newcomers might perceive that understanding Haskell is a prerequisite for using SuperCollider. Thus, it might be better to omit the Haskell example in this context.

Nevertheless, I would like to retain this example if it is included outside the SC documentation, or if the section title is “More to Read for Interest: Implementation Comparison of Pure Functional Approach and Pure Object-Oriented Approach” within a note:: or cf:: (compare) context. Unfortunately, sclang does not have cf:: tag.

2 Likes

You are right.

But I think the idea was to have a separate reference with some intermediary and more complete information.

The Haskell code can be removed or not; in any case, I agree there must be a better text there.

1 Like

My suggestion would be to move it to the end and clearly mark it as an addendum, so that it’s clear that it isn’t part of the main line of the argument. When it’s in the middle, it seems like “must-know” but I agree with prko that for most users, it won’t be.

SC documentation could use more side topics like this (while being mindful of the maintenance cost, which should be minimal in this case)! Just as long as they’re clearly marked.

hjh

3 Likes