diff --git a/.changeset/witty-horses-hammer.md b/.changeset/witty-horses-hammer.md
new file mode 100644
index 00000000..04141f19
--- /dev/null
+++ b/.changeset/witty-horses-hammer.md
@@ -0,0 +1,6 @@
+---
+"@tsplus/runtime": patch
+"@tsplus/stdlib": patch
+---
+
+Change Equals to work Structurally, change Either/Maybe to be plain objects
diff --git a/packages/runtime/_src/Guard.ts b/packages/runtime/_src/Guard.ts
index da911d92..20d3feeb 100644
--- a/packages/runtime/_src/Guard.ts
+++ b/packages/runtime/_src/Guard.ts
@@ -1,6 +1,4 @@
import { Cons, Nil } from "@tsplus/stdlib/collections/List/definition"
-import { Left, Right } from "@tsplus/stdlib/data/Either/definition"
-import { None, Some } from "@tsplus/stdlib/data/Maybe/definition"
/**
* A Guard is a type representing the ability to identify when a value is of type A at runtime
@@ -156,12 +154,24 @@ export function deriveOption>(
: never
): Guard {
return Guard((u): u is A => {
- if (u instanceof None) {
+ if (typeof u !== "object" || u == null) {
+ return false
+ }
+
+ if (Equals.equals(Maybe.none, u)) {
return true
}
- if (u instanceof Some) {
- return element.is(u.value)
+
+ const keys = Object.keys(u)
+
+ if (keys.length !== 2) {
+ return false
+ }
+
+ if ("_tag" in u && "value" in u && u["_tag"] === "Some") {
+ return element.is(u["value"])
}
+
return false
})
}
@@ -270,12 +280,27 @@ export function deriveEither>(
: never
): Guard {
return Guard((u): u is A => {
- if (u instanceof Left) {
- return left.is(u.left)
+ if (typeof u !== "object" || u == null) {
+ return false
}
- if (u instanceof Right) {
- return right.is(u.right)
+
+ const keys = Object.keys(u)
+
+ if (keys.length !== 2) {
+ return false
}
+
+ if ("_tag" in u) {
+ switch (u["_tag"]) {
+ case "Left": {
+ return "left" in u && left.is(u["left"])
+ }
+ case "Right": {
+ return "right" in u && right.is(u["right"])
+ }
+ }
+ }
+
return false
})
}
diff --git a/packages/stdlib/_src/data/Either/definition.ts b/packages/stdlib/_src/data/Either/definition.ts
index e94c8aff..7f259c7c 100644
--- a/packages/stdlib/_src/data/Either/definition.ts
+++ b/packages/stdlib/_src/data/Either/definition.ts
@@ -1,8 +1,7 @@
/**
* adapted from https://github.com/gcanti/fp-ts
*/
-const _leftHash = Hash.string("Either.Left")
-const _rightHash = Hash.string("Either.Right")
+
/**
* @tsplus type Either
*/
@@ -39,33 +38,17 @@ export declare namespace Either {
/**
* @tsplus type Either.Left
*/
-export class Left implements Equals {
- readonly _tag = "Left"
-
- constructor(readonly left: E) {}
-
- [Equals.sym](that: unknown): boolean {
- return that instanceof Left && Equals.equals(this.left, that.left)
- }
- [Hash.sym](): number {
- return Hash.combine(_leftHash, Hash.unknown(this.left))
- }
+export interface Left {
+ readonly _tag: "Left"
+ readonly left: E
}
/**
* @tsplus type Either.Right
*/
-export class Right implements Equals {
- readonly _tag = "Right"
-
- constructor(readonly right: A) {}
-
- [Equals.sym](that: unknown): boolean {
- return that instanceof Right && Equals.equals(this.right, that.right)
- }
- [Hash.sym](): number {
- return Hash.combine(_rightHash, Hash.unknown(this.right))
- }
+export interface Right {
+ readonly _tag: "Right"
+ readonly right: A
}
/**
@@ -126,23 +109,15 @@ export function getRight(self: Either): Maybe {
return self._tag === "Right" ? Maybe.some(self.right) : Maybe.none
}
-/**
- * Constructs a new `Either` holding a `Right` value. This usually represents a
- * successful value due to the right bias of this structure.
- *
- * @tsplus static Either.Ops __call
- */
-export function apply(a: A): Either {
- return new Right(a)
-}
/**
* Constructs a new `Either` holding a `Right` value. This usually represents a
* successful value due to the right bias of this structure.
*
* @tsplus static Either.Ops right
+ * @tsplus static Either.Ops __call
*/
export function right(a: A): Either {
- return new Right(a)
+ return { _tag: "Right", right: a }
}
/**
@@ -152,7 +127,7 @@ export function right(a: A): Either {
* @tsplus static Either.Ops rightW
*/
export function rightW(a: A): Either {
- return new Right(a)
+ return { _tag: "Right", right: a }
}
/**
@@ -162,7 +137,7 @@ export function rightW(a: A): Either {
* @tsplus static Either.Ops left
*/
export function left(e: E): Either {
- return new Left(e)
+ return { _tag: "Left", left: e }
}
/**
@@ -172,7 +147,7 @@ export function left(e: E): Either {
* @tsplus static Either.Ops leftW
*/
export function leftW(e: E): Either {
- return new Left(e)
+ return { _tag: "Left", left: e }
}
/**
@@ -222,3 +197,10 @@ export function widenA() {
(self: Either): Either => self
)
}
+
+/**
+ * @tsplus operator Either ==
+ */
+export function equals(a: Either, b: Either) {
+ return Equals.equals(a, b)
+}
diff --git a/packages/stdlib/_src/data/Maybe/definition.ts b/packages/stdlib/_src/data/Maybe/definition.ts
index d71a76b4..3e1d20e9 100644
--- a/packages/stdlib/_src/data/Maybe/definition.ts
+++ b/packages/stdlib/_src/data/Maybe/definition.ts
@@ -27,39 +27,21 @@ export const Maybe: MaybeOps = {
*/
export interface MaybeAspects {}
-const _noneHash = Hash.string("Maybe.None")
-const _someHash = Hash.string("Maybe.Some")
-
/**
* Definitions
*
* @tsplus type Maybe.None
*/
-export class None implements Equals {
- readonly _tag = "None";
-
- [Equals.sym](that: unknown): boolean {
- return that instanceof None
- }
- [Hash.sym](): number {
- return _noneHash
- }
+export interface None {
+ readonly _tag: "None"
}
/**
* @tsplus type Maybe.Some
*/
-export class Some implements Equals {
- readonly _tag = "Some"
-
- constructor(readonly value: A) {}
-
- [Equals.sym](that: unknown): boolean {
- return that instanceof Some && Equals.equals(this.value, that.value)
- }
- [Hash.sym](): number {
- return Hash.combine(_someHash, Hash.unknown(this.value))
- }
+export interface Some {
+ readonly _tag: "Some"
+ readonly value: A
}
/**
@@ -82,7 +64,7 @@ export type ArrayOfMaybies[]> = {
*
* @tsplus static Maybe.Ops none
*/
-export const none: Maybe = new None()
+export const none: Maybe = { _tag: "None" }
/**
* Constructs `None`.
@@ -99,7 +81,7 @@ export function empty(): Maybe {
* @tsplus static Maybe.Ops some
*/
export function some(a: A): Maybe {
- return new Some(a)
+ return { _tag: "Some", value: a }
}
/**
@@ -131,3 +113,10 @@ export function isMaybe(u: unknown): u is Maybe {
(u["_tag"] === "Some" || u["_tag"] === "None")
)
}
+
+/**
+ * @tsplus operator Maybe ==
+ */
+export function equals(a: Maybe, b: Maybe) {
+ return Equals.equals(a, b)
+}
diff --git a/packages/stdlib/_src/structure/Equals.ts b/packages/stdlib/_src/structure/Equals.ts
index 1d4b4a86..9217313d 100644
--- a/packages/stdlib/_src/structure/Equals.ts
+++ b/packages/stdlib/_src/structure/Equals.ts
@@ -30,6 +30,76 @@ export function sameValueZeroEqual(a: any, b: any) {
return a === b || (a !== a && b !== b)
}
+const protoMap = new Map boolean>([
+ [
+ Array.prototype,
+ (a: Array, b: Array) => a.length === b.length && a.every((v, i) => equals(v, b[i]))
+ ],
+ [
+ Set.prototype,
+ (a: Set, b: Set) => {
+ if (a.size !== b.size) {
+ return false
+ }
+ for (const va of a.values()) {
+ let found = false
+ for (const vb of b.values()) {
+ if (equals(va, vb)) {
+ found = true
+ break
+ }
+ }
+ if (!found) {
+ return false
+ }
+ }
+ return true
+ }
+ ],
+ [
+ Object.prototype,
+ (a: object, b: object) => {
+ const keysA = Object.keys(a).sort()
+ const keysB = Object.keys(b).sort()
+ if (keysA.length !== keysB.length) {
+ return false
+ }
+ if (!equals(keysA, keysB)) {
+ return false
+ }
+ for (const ka of keysA) {
+ const va = a[ka]
+ const vb = b[ka]
+ if (!equals(va, vb)) {
+ return false
+ }
+ }
+ return true
+ }
+ ],
+ [
+ Map.prototype,
+ (a: Map, b: Map) => {
+ if (a.size !== b.size) {
+ return false
+ }
+ for (const [ka, va] of a.entries()) {
+ let found = false
+ for (const [kb, vb] of b.entries()) {
+ if (equals(ka, kb) && equals(va, vb)) {
+ found = true
+ break
+ }
+ }
+ if (!found) {
+ return false
+ }
+ }
+ return true
+ }
+ ]
+])
+
/**
* @tsplus static Equals.Ops equals
* @tsplus fluent Equals equals
@@ -41,12 +111,24 @@ export function equals(a: unknown, b: unknown): boolean {
if (a === b) {
return true
}
- if (!sameValueZeroEqual(Hash.unknown(a), Hash.unknown(b))) {
- return false
- } else if (isEquals(a)) {
+ if (isEquals(a)) {
+ if (!isEquals(b)) {
+ return false
+ }
+ if (!sameValueZeroEqual(Hash.unknown(a), Hash.unknown(b))) {
+ return false
+ }
return a[Equals.sym](b)
- } else if (isEquals(b)) {
- return b[Equals.sym](a)
+ }
+ if (typeof a === "object" && typeof b === "object") {
+ const protoA = Object.getPrototypeOf(a)
+ const protoB = Object.getPrototypeOf(b)
+ if (protoA === protoB) {
+ const compare = protoMap.get(protoA)
+ if (compare) {
+ return compare(a, b)
+ }
+ }
}
return sameValueZeroEqual(a, b)
}
diff --git a/packages/stdlib/_src/structure/Hash.ts b/packages/stdlib/_src/structure/Hash.ts
index eba5975e..e697617f 100644
--- a/packages/stdlib/_src/structure/Hash.ts
+++ b/packages/stdlib/_src/structure/Hash.ts
@@ -47,6 +47,20 @@ export function hashArray(arr: readonly unknown[]): number {
return optimize(_hashArray(arr))
}
+/**
+ * @tsplus static Hash.Ops map
+ */
+export function hashMap(arr: Map): number {
+ return optimize(_hashMap(arr))
+}
+
+/**
+ * @tsplus static Hash.Ops set
+ */
+export function hashSet(arr: Set): number {
+ return optimize(_hashSet(arr))
+}
+
/**
* @tsplus static Hash.Ops args
*/
@@ -162,17 +176,45 @@ function _hashArray(arr: readonly any[]): number {
return h
}
+function _hashMap(arr: Map): number {
+ let h = 9744
+ arr.forEach((v, k) => {
+ h ^= _combineHash(_hash(k), _hash(v))
+ })
+ return h
+}
+
+function _hashSet(arr: Set): number {
+ let h = 2362
+ arr.forEach((v) => {
+ h ^= _hash(v)
+ })
+ return h
+}
+
function _combineHash(a: number, b: number): number {
return (a * 53) ^ b
}
+const protoMap = new Map number>([
+ [Array.prototype, hashArray],
+ [Map.prototype, hashMap],
+ [Set.prototype, hashSet],
+ [Object.prototype, hashPlainObject]
+])
+
function _hashObject(value: object): number {
let h = CACHE.get(value)
if (isDefined(h)) return h
if (isHash(value)) {
h = value[Hash.sym]()
} else {
- h = hashRandom()
+ const primitiveHash = protoMap.get(Object.getPrototypeOf(value))
+ if (primitiveHash) {
+ h = primitiveHash(value as any)
+ } else {
+ h = hashRandom()
+ }
}
CACHE.set(value, h)
return h
@@ -197,11 +239,10 @@ function _hashIterator(it: Iterator): number {
function _hashPlainObject(o: object): number {
CACHE.set(o, randomInt())
- const keys = Object.keys(o).sort()
+ const keys = Object.keys(o)
let h = 12289
for (let i = 0; i < keys.length; i++) {
- h = _combineHash(h, _hashString(keys[i]!))
- h = _combineHash(h, hashUnknown((o as any)[keys[i]!]))
+ h ^= _combineHash(_hashString(keys[i]!), hashUnknown((o as any)[keys[i]!]))
}
return h
}
diff --git a/packages/stdlib/_test/Equals.test.ts b/packages/stdlib/_test/Equals.test.ts
new file mode 100644
index 00000000..e7bb3841
--- /dev/null
+++ b/packages/stdlib/_test/Equals.test.ts
@@ -0,0 +1,46 @@
+describe("Equals", () => {
+ it("object", () => {
+ const a = { foo: "bar", bar: "foo" }
+ const b = { bar: "foo", foo: "bar" }
+ const c = { bar: "foo", foo: "bar", baz: "not" }
+ assert.isTrue(Equals.equals(a, b))
+ assert.isFalse(Equals.equals(a, c))
+ })
+ it("custom", () => {
+ const a = {
+ foo: "bar",
+ bar: "foo",
+ [Hash.sym]: () => 1337,
+ [Equals.sym]: (a: any, b: any) => a === b
+ }
+ const b = {
+ foo: "bar",
+ bar: "foo",
+ [Hash.sym]: () => 1337,
+ [Equals.sym]: (a: any, b: any) => a === b
+ }
+ assert.isTrue(Equals.equals(a, a))
+ assert.isFalse(Equals.equals(a, b))
+ })
+ it("array", () => {
+ const a = [0, 1, 2]
+ const b = [0, 1, 2]
+ const c = [0, 1, 2, 3]
+ assert.isTrue(Equals.equals(a, b))
+ assert.isFalse(Equals.equals(a, c))
+ })
+ it("map", () => {
+ const a = new Map([[0, 0], [1, 1]])
+ const b = new Map([[1, 1], [0, 0]])
+ const c = new Map([[0, 0], [1, 1], [2, 2]])
+ assert.isTrue(Equals.equals(a, b))
+ assert.isFalse(Equals.equals(a, c))
+ })
+ it("set", () => {
+ const a = new Set([0, 1])
+ const b = new Set([1, 0])
+ const c = new Set([0, 1, 2])
+ assert.isTrue(Equals.equals(a, b))
+ assert.isFalse(Equals.equals(a, c))
+ })
+})
diff --git a/packages/stdlib/_test/Hash.test.ts b/packages/stdlib/_test/Hash.test.ts
new file mode 100644
index 00000000..d94974b1
--- /dev/null
+++ b/packages/stdlib/_test/Hash.test.ts
@@ -0,0 +1,34 @@
+describe("Hash", () => {
+ it("object", () => {
+ const a = { foo: "bar", bar: "foo" }
+ const b = { bar: "foo", foo: "bar" }
+ const c = { bar: "foo", foo: "bar", baz: "not" }
+ assert.strictEqual(Hash.unknown(a), Hash.unknown(b))
+ assert.notStrictEqual(Hash.unknown(a), Hash.unknown(c))
+ })
+ it("custom", () => {
+ const a = { foo: "bar", bar: "foo", [Hash.sym]: () => 1337 }
+ assert.strictEqual(Hash.unknown(a), 1337)
+ })
+ it("array", () => {
+ const a = [0, 1, 2]
+ const b = [0, 1, 2]
+ const c = [0, 1, 2, 3]
+ assert.strictEqual(Hash.unknown(a), Hash.unknown(b))
+ assert.notStrictEqual(Hash.unknown(a), Hash.unknown(c))
+ })
+ it("map", () => {
+ const a = new Map([[0, 0], [1, 1]])
+ const b = new Map([[1, 1], [0, 0]])
+ const c = new Map([[0, 0], [1, 1], [2, 2]])
+ assert.strictEqual(Hash.unknown(a), Hash.unknown(b))
+ assert.notStrictEqual(Hash.unknown(a), Hash.unknown(c))
+ })
+ it("set", () => {
+ const a = new Set([0, 1])
+ const b = new Set([1, 0])
+ const c = new Set([0, 1, 2])
+ assert.strictEqual(Hash.unknown(a), Hash.unknown(b))
+ assert.notStrictEqual(Hash.unknown(a), Hash.unknown(c))
+ })
+})