Skip to content

Commit 5ad2eec

Browse files
authoredFeb 20, 2024··
add Hash.cached for caching hashes (#2167)
1 parent 6612682 commit 5ad2eec

38 files changed

+177
-96
lines changed
 

‎.changeset/fuzzy-grapes-bow.md

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
"effect": patch
3+
---
4+
5+
add Hash.cached
6+
7+
This api assists with adding a layer of caching, when hashing immutable data structures.
8+
9+
```ts
10+
import { Data, Hash } from "effect";
11+
12+
class User extends Data.Class<{
13+
id: number;
14+
name: string;
15+
}> {
16+
[Hash.symbol]() {
17+
return Hash.cached(this, Hash.string(`${this.id}-${this.name}`));
18+
}
19+
}
20+
```

‎packages/effect/src/BigDecimal.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ const BigDecimalProto: Omit<BigDecimal, "value" | "scale" | "normalized"> = {
5757
const normalized = normalize(this)
5858
return pipe(
5959
Hash.hash(normalized.value),
60-
Hash.combine(Hash.number(normalized.scale))
60+
Hash.combine(Hash.number(normalized.scale)),
61+
Hash.cached(this)
6162
)
6263
},
6364
[Equal.symbol](this: BigDecimal, that: unknown): boolean {

‎packages/effect/src/Chunk.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ const ChunkProto: Omit<Chunk<unknown>, "backing" | "depth" | "left" | "length" |
142142
return isChunk(that) && _equivalence(this, that)
143143
},
144144
[Hash.symbol]<A>(this: Chunk<A>): number {
145-
return Hash.array(toReadonlyArray(this))
145+
return Hash.cached(this, Hash.array(toReadonlyArray(this)))
146146
},
147147
[Symbol.iterator]<A>(this: Chunk<A>): Iterator<A> {
148148
switch (this.backing._tag) {

‎packages/effect/src/Cron.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ const CronProto: Omit<Cron, "minutes" | "hours" | "days" | "months" | "weekdays"
5050
Hash.combine(Hash.array(ReadonlyArray.fromIterable(this.hours))),
5151
Hash.combine(Hash.array(ReadonlyArray.fromIterable(this.days))),
5252
Hash.combine(Hash.array(ReadonlyArray.fromIterable(this.months))),
53-
Hash.combine(Hash.array(ReadonlyArray.fromIterable(this.weekdays)))
53+
Hash.combine(Hash.array(ReadonlyArray.fromIterable(this.weekdays))),
54+
Hash.cached(this)
5455
)
5556
},
5657
toString(this: Cron) {

‎packages/effect/src/Duration.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,8 @@ const infinityValue: DurationValue = { _tag: "Infinity" }
116116

117117
const DurationProto: Omit<Duration, "value"> = {
118118
[TypeId]: TypeId,
119-
[Hash.symbol](this: Duration): number {
120-
return Hash.structure(this.value)
119+
[Hash.symbol](this: Duration) {
120+
return Hash.cached(this, Hash.structure(this.value))
121121
},
122122
[Equal.symbol](this: Duration, that: unknown): boolean {
123123
return isDuration(that) && equals(this, that)

‎packages/effect/src/Hash.ts

+31
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,34 @@ export const array = <A>(arr: ReadonlyArray<A>) => {
156156
}
157157
return optimize(h)
158158
}
159+
160+
/**
161+
* @since 2.0.0
162+
* @category hashing
163+
*/
164+
export const cached: {
165+
(self: object): (hash: number) => number
166+
(self: object, hash: number): number
167+
} = function() {
168+
if (arguments.length === 1) {
169+
const self = arguments[0] as object
170+
return function(hash: number) {
171+
Object.defineProperty(self, symbol, {
172+
value() {
173+
return hash
174+
},
175+
enumerable: false
176+
})
177+
return hash
178+
} as any
179+
}
180+
const self = arguments[0] as object
181+
const hash = arguments[1] as number
182+
Object.defineProperty(self, symbol, {
183+
value() {
184+
return hash
185+
},
186+
enumerable: false
187+
})
188+
return hash
189+
}

‎packages/effect/src/List.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ const ConsProto: Omit<Cons<unknown>, "head" | "tail"> = {
118118
_equivalence(this, that)
119119
},
120120
[Hash.symbol](this: Cons<unknown>): number {
121-
return Hash.array(toArray(this))
121+
return Hash.cached(this, Hash.array(toArray(this)))
122122
},
123123
[Symbol.iterator](this: Cons<unknown>): Iterator<unknown> {
124124
let done = false
@@ -162,6 +162,7 @@ const makeCons = <A>(head: A, tail: List<A>): MutableCons<A> => {
162162
return cons
163163
}
164164

165+
const NilHash = Hash.string("Nil")
165166
const NilProto: Nil<unknown> = {
166167
[TypeId]: TypeId,
167168
_tag: "Nil",
@@ -178,7 +179,7 @@ const NilProto: Nil<unknown> = {
178179
return this.toJSON()
179180
},
180181
[Hash.symbol](): number {
181-
return Hash.array(toArray(this))
182+
return NilHash
182183
},
183184
[Equal.symbol](that: unknown): boolean {
184185
return isList(that) && this._tag === that._tag

‎packages/effect/src/SortedMap.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,11 @@ const SortedMapProto: Omit<SortedMap<unknown, unknown>, "tree"> = {
4141
_V: (_: never) => _
4242
},
4343
[Hash.symbol]<K, V>(this: SortedMap<K, V>): number {
44-
return pipe(Hash.hash(this.tree), Hash.combine(Hash.hash("effect/SortedMap")))
44+
return pipe(
45+
Hash.hash(this.tree),
46+
Hash.combine(Hash.hash("effect/SortedMap")),
47+
Hash.cached(this)
48+
)
4549
},
4650
[Equal.symbol]<K, V>(this: SortedMap<K, V>, that: unknown): boolean {
4751
return isSortedMap(that) && Equal.equals(this.tree, that.tree)

‎packages/effect/src/SortedSet.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@ const SortedSetProto: Omit<SortedSet<unknown>, "keyTree"> = {
4040
_A: (_: any) => _
4141
},
4242
[Hash.symbol]<A>(this: SortedSet<A>): number {
43-
return pipe(Hash.hash(this.keyTree), Hash.combine(Hash.hash(TypeId)))
43+
return pipe(
44+
Hash.hash(this.keyTree),
45+
Hash.combine(Hash.hash(TypeId)),
46+
Hash.cached(this)
47+
)
4448
},
4549
[Equal.symbol]<A>(this: SortedSet<A>, that: unknown): boolean {
4650
return isSortedSet(that) && Equal.equals(this.keyTree, that.keyTree)

‎packages/effect/src/TestAnnotation.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ class TestAnnotationImpl<A> implements Equal.Equal {
5151
[Hash.symbol](): number {
5252
return pipe(
5353
Hash.hash(TestAnnotationSymbolKey),
54-
Hash.combine(Hash.hash(this.identifier))
54+
Hash.combine(Hash.hash(this.identifier)),
55+
Hash.cached(this)
5556
)
5657
}
5758
[Equal.symbol](that: unknown): boolean {

‎packages/effect/src/internal/cache.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,8 @@ class MapKeyImpl<out K> implements MapKey<K> {
123123
return pipe(
124124
Hash.hash(this.current),
125125
Hash.combine(Hash.hash(this.previous)),
126-
Hash.combine(Hash.hash(this.next))
126+
Hash.combine(Hash.hash(this.next)),
127+
Hash.cached(this)
127128
)
128129
}
129130
[Equal.symbol](that: unknown): boolean {

‎packages/effect/src/internal/cause.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ const proto = {
3939
[Hash.symbol](this: Cause.Cause<any>): number {
4040
return pipe(
4141
Hash.hash(CauseSymbolKey),
42-
Hash.combine(Hash.hash(flattenCause(this)))
42+
Hash.combine(Hash.hash(flattenCause(this))),
43+
Hash.cached(this)
4344
)
4445
},
4546
[Equal.symbol](this: Cause.Cause<any>, that: unknown): boolean {

‎packages/effect/src/internal/context.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export const ContextProto: Omit<C.Context<unknown>, "unsafeMap"> = {
111111
return false
112112
},
113113
[Hash.symbol]<A>(this: C.Context<A>): number {
114-
return Hash.number(this.unsafeMap.size)
114+
return Hash.cached(this, Hash.number(this.unsafeMap.size))
115115
},
116116
pipe<A>(this: C.Context<A>) {
117117
return pipeArguments(this, arguments)

‎packages/effect/src/internal/core.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ class EffectPrimitive {
158158
return this === that
159159
}
160160
[Hash.symbol](this: {}) {
161-
return Hash.random(this)
161+
return Hash.cached(this, Hash.random(this))
162162
}
163163
pipe() {
164164
return pipeArguments(this, arguments)
@@ -195,7 +195,7 @@ class EffectPrimitiveFailure {
195195
return this === that
196196
}
197197
[Hash.symbol](this: {}) {
198-
return Hash.random(this)
198+
return Hash.cached(this, Hash.random(this))
199199
}
200200
get cause() {
201201
return this.i0
@@ -233,7 +233,7 @@ class EffectPrimitiveSuccess {
233233
return this === that
234234
}
235235
[Hash.symbol](this: {}) {
236-
return Hash.random(this)
236+
return Hash.cached(this, Hash.random(this))
237237
}
238238
get value() {
239239
return this.i0
@@ -1734,7 +1734,7 @@ export class RequestResolverImpl<out R, in A> implements RequestResolver.Request
17341734
this.runAll = runAll as any
17351735
}
17361736
[Hash.symbol](): number {
1737-
return this.target ? Hash.hash(this.target) : Hash.random(this)
1737+
return Hash.cached(this, this.target ? Hash.hash(this.target) : Hash.random(this))
17381738
}
17391739
[Equal.symbol](that: unknown): boolean {
17401740
return this.target ?

‎packages/effect/src/internal/data.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { StructuralPrototype } from "./effectable.js"
66
/** @internal */
77
export const ArrayProto: Equal.Equal = Object.assign(Object.create(Array.prototype), {
88
[Hash.symbol](this: Array<any>) {
9-
return Hash.array(this)
9+
return Hash.cached(this, Hash.array(this))
1010
},
1111
[Equal.symbol](this: Array<any>, that: Equal.Equal) {
1212
if (Array.isArray(that) && this.length === that.length) {

‎packages/effect/src/internal/effect/circular.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ class Key<in out A> implements Equal.Equal {
315315
return false
316316
}
317317
[Hash.symbol]() {
318-
return this.eq ? 0 : Hash.hash(this.a)
318+
return this.eq ? 0 : Hash.cached(this, Hash.hash(this.a))
319319
}
320320
}
321321

‎packages/effect/src/internal/effectable.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export const EffectPrototype: Effect.Effect<never> & Equal.Equal = {
7373
return this === that
7474
},
7575
[Hash.symbol]() {
76-
return Hash.random(this)
76+
return Hash.cached(this, Hash.random(this))
7777
},
7878
pipe() {
7979
return pipeArguments(this, arguments)
@@ -82,8 +82,8 @@ export const EffectPrototype: Effect.Effect<never> & Equal.Equal = {
8282

8383
/** @internal */
8484
export const StructuralPrototype: Equal.Equal = {
85-
[Hash.symbol](this: Equal.Equal) {
86-
return Hash.structure(this)
85+
[Hash.symbol]() {
86+
return Hash.cached(this, Hash.structure(this))
8787
},
8888
[Equal.symbol](this: Equal.Equal, that: Equal.Equal) {
8989
const selfKeys = Object.keys(this)

‎packages/effect/src/internal/either.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const RightProto = Object.assign(Object.create(CommonProto), {
3737
return isEither(that) && isRight(that) && Equal.equals(that.right, this.right)
3838
},
3939
[Hash.symbol]<E, A>(this: Either.Right<E, A>) {
40-
return Hash.combine(Hash.hash(this._tag))(Hash.hash(this.right))
40+
return Hash.cached(this, Hash.combine(Hash.hash(this._tag))(Hash.hash(this.right)))
4141
},
4242
toJSON<E, A>(this: Either.Right<E, A>) {
4343
return {
@@ -55,7 +55,7 @@ const LeftProto = Object.assign(Object.create(CommonProto), {
5555
return isEither(that) && isLeft(that) && Equal.equals(that.left, this.left)
5656
},
5757
[Hash.symbol]<E, A>(this: Either.Left<E, A>) {
58-
return Hash.combine(Hash.hash(this._tag))(Hash.hash(this.left))
58+
return Hash.cached(this, Hash.combine(Hash.hash(this._tag))(Hash.hash(this.left)))
5959
},
6060
toJSON<E, A>(this: Either.Left<E, A>) {
6161
return {

‎packages/effect/src/internal/fiberId.ts

+7-13
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,8 @@ class Runtime implements FiberId.Runtime {
7171
readonly id: number,
7272
readonly startTimeMillis: number
7373
) {}
74-
_hash: number | undefined;
7574
[Hash.symbol](): number {
76-
if (this._hash == undefined) {
77-
this._hash = Hash.string(`${FiberIdSymbolKey}-${this._tag}-${this.id}-${this.startTimeMillis}`)
78-
}
79-
return this._hash
75+
return Hash.cached(this, Hash.string(`${FiberIdSymbolKey}-${this._tag}-${this.id}-${this.startTimeMillis}`))
8076
}
8177
[Equal.symbol](that: unknown): boolean {
8278
return isFiberId(that) &&
@@ -111,14 +107,12 @@ class Composite implements FiberId.Composite {
111107
}
112108
_hash: number | undefined;
113109
[Hash.symbol](): number {
114-
if (this._hash == undefined) {
115-
this._hash = pipe(
116-
Hash.string(`${FiberIdSymbolKey}-${this._tag}`),
117-
Hash.combine(Hash.hash(this.left)),
118-
Hash.combine(Hash.hash(this.right))
119-
)
120-
}
121-
return this._hash
110+
return pipe(
111+
Hash.string(`${FiberIdSymbolKey}-${this._tag}`),
112+
Hash.combine(Hash.hash(this.left)),
113+
Hash.combine(Hash.hash(this.right)),
114+
Hash.cached(this)
115+
)
122116
}
123117
[Equal.symbol](that: unknown): boolean {
124118
return isFiberId(that) &&

‎packages/effect/src/internal/fiberStatus.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,14 @@ export const OP_SUSPENDED = "Suspended" as const
3131
/** @internal */
3232
export type OP_SUSPENDED = typeof OP_SUSPENDED
3333

34+
const DoneHash = Hash.string(`${FiberStatusSymbolKey}-${OP_DONE}`)
35+
3436
/** @internal */
3537
class Done implements FiberStatus.Done {
3638
readonly [FiberStatusTypeId]: FiberStatus.FiberStatusTypeId = FiberStatusTypeId
3739
readonly _tag = OP_DONE;
3840
[Hash.symbol](): number {
39-
return pipe(
40-
Hash.hash(FiberStatusSymbolKey),
41-
Hash.combine(Hash.hash(this._tag))
42-
)
41+
return DoneHash
4342
}
4443
[Equal.symbol](that: unknown): boolean {
4544
return isFiberStatus(that) && that._tag === OP_DONE
@@ -55,7 +54,8 @@ class Running implements FiberStatus.Running {
5554
return pipe(
5655
Hash.hash(FiberStatusSymbolKey),
5756
Hash.combine(Hash.hash(this._tag)),
58-
Hash.combine(Hash.hash(this.runtimeFlags))
57+
Hash.combine(Hash.hash(this.runtimeFlags)),
58+
Hash.cached(this)
5959
)
6060
}
6161
[Equal.symbol](that: unknown): boolean {
@@ -80,7 +80,8 @@ class Suspended implements FiberStatus.Suspended {
8080
Hash.hash(FiberStatusSymbolKey),
8181
Hash.combine(Hash.hash(this._tag)),
8282
Hash.combine(Hash.hash(this.runtimeFlags)),
83-
Hash.combine(Hash.hash(this.blockingOn))
83+
Hash.combine(Hash.hash(this.blockingOn)),
84+
Hash.cached(this)
8485
)
8586
}
8687
[Equal.symbol](that: unknown): boolean {

‎packages/effect/src/internal/hashMap.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,12 @@ const HashMapProto: HM.HashMap<unknown, unknown> = {
4747
[Symbol.iterator]<K, V>(this: HashMapImpl<K, V>): Iterator<[K, V]> {
4848
return new HashMapIterator(this, (k, v) => [k, v])
4949
},
50-
[Hash.symbol](): number {
50+
[Hash.symbol](this: HM.HashMap<unknown, unknown>): number {
5151
let hash = Hash.hash(HashMapSymbolKey)
5252
for (const item of this) {
5353
hash ^= pipe(Hash.hash(item[0]), Hash.combine(Hash.hash(item[1])))
5454
}
55-
return hash
55+
return Hash.cached(this, hash)
5656
},
5757
[Equal.symbol]<K, V>(this: HashMapImpl<K, V>, that: unknown): boolean {
5858
if (isHashMap(that)) {

‎packages/effect/src/internal/hashSet.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ const HashSetProto: Omit<HashSetImpl<unknown>, "_keyMap"> = {
2626
return HM.keys(this._keyMap)
2727
},
2828
[Hash.symbol]<A>(this: HashSetImpl<A>): number {
29-
return Hash.combine(Hash.hash(this._keyMap))(Hash.hash(HashSetSymbolKey))
29+
return Hash.cached(
30+
this,
31+
Hash.combine(Hash.hash(this._keyMap))(Hash.hash(HashSetSymbolKey))
32+
)
3033
},
3134
[Equal.symbol]<A>(this: HashSetImpl<A>, that: unknown): boolean {
3235
if (isHashSet(that)) {

‎packages/effect/src/internal/keyedPool.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ class Complete<in out A, out E> implements Equal.Equal {
6262
[Hash.symbol](): number {
6363
return pipe(
6464
Hash.string("effect/KeyedPool/Complete"),
65-
Hash.combine(Hash.hash(this.pool))
65+
Hash.combine(Hash.hash(this.pool)),
66+
Hash.cached(this)
6667
)
6768
}
6869
[Equal.symbol](u: unknown): boolean {
@@ -80,7 +81,8 @@ class Pending<in out A, in out E> implements Equal.Equal {
8081
[Hash.symbol](): number {
8182
return pipe(
8283
Hash.string("effect/KeyedPool/Pending"),
83-
Hash.combine(Hash.hash(this.deferred))
84+
Hash.combine(Hash.hash(this.deferred)),
85+
Hash.cached(this)
8486
)
8587
}
8688
[Equal.symbol](u: unknown): boolean {

0 commit comments

Comments
 (0)
Please sign in to comment.