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)) + }) +})