| title | Comparing Data by Value with Structural Equality | ||||||
|---|---|---|---|---|---|---|---|
| id | comparing-data-by-value-with-structural-equality | ||||||
| skillLevel | beginner | ||||||
| applicationPatternId | core-concepts | ||||||
| summary | Use Data.struct and Equal.equals to safely compare objects by their value instead of their reference, avoiding common JavaScript pitfalls. | ||||||
| tags |
|
||||||
| rule |
|
||||||
| related |
|
||||||
| author | effect_website | ||||||
| lessonOrder | 5 |
To compare objects or classes by their contents rather than by their memory reference, use one of two methods:
- For plain data objects: Define them with
Data.struct. - For classes: Extend
Data.Classor implement theEqual.Equalinterface.
Then, compare instances using the Equal.equals(a, b) function.
In JavaScript, comparing two non-primitive values with === checks for referential equality. It only returns true if they are the exact same instance in memory. This means two objects with identical contents are not considered equal, which is a common source of bugs.
{ a: 1 } === { a: 1 } // false!
Effect solves this with structural equality. All of Effect's built-in data structures (Option, Either, Chunk, etc.) can be compared by their structure and values. By using helpers like Data.struct, you can easily give your own data structures this same powerful and predictable behavior.
We define two points using Data.struct. Even though p1 and p2 are different instances in memory, Equal.equals correctly reports them as equal because their contents match.
import { Data, Equal, Effect } from "effect";
// Define a Point type with structural equality
interface Point {
readonly _tag: "Point";
readonly x: number;
readonly y: number;
}
const Point = Data.tagged<Point>("Point");
// Create a program to demonstrate structural equality
const program = Effect.gen(function* () {
const p1 = Point({ x: 1, y: 2 });
const p2 = Point({ x: 1, y: 2 });
const p3 = Point({ x: 3, y: 4 });
// Standard reference equality fails
yield* Effect.log("Comparing points with reference equality (===):");
yield* Effect.log(`p1 === p2: ${p1 === p2}`);
// Structural equality works as expected
yield* Effect.log("\nComparing points with structural equality:");
yield* Effect.log(`p1 equals p2: ${Equal.equals(p1, p2)}`);
yield* Effect.log(`p1 equals p3: ${Equal.equals(p1, p3)}`);
// Show the actual points
yield* Effect.log("\nPoint values:");
yield* Effect.log(`p1: ${JSON.stringify(p1)}`);
yield* Effect.log(`p2: ${JSON.stringify(p2)}`);
yield* Effect.log(`p3: ${JSON.stringify(p3)}`);
});
// Run the program
Effect.runPromise(program);Relying on === for object or array comparison. This will lead to bugs when you expect two objects with the same values to be treated as equal, especially when working with data in collections, Refs, or Effect's success values.
// ❌ WRONG: This will not behave as expected.
const user1 = { id: 1, name: "Paul" };
const user2 = { id: 1, name: "Paul" };
if (user1 === user2) {
// This code block will never be reached.
console.log("Users are the same.");
}
// Another common pitfall
const selectedUsers = [user1];
// This check will fail, even though a user with id 1 is in the array.
if (selectedUsers.includes({ id: 1, name: "Paul" })) {
// ...
}