diff --git a/.changeset/clean-beds-cheer.md b/.changeset/clean-beds-cheer.md new file mode 100644 index 00000000000..a7378a7036c --- /dev/null +++ b/.changeset/clean-beds-cheer.md @@ -0,0 +1,5 @@ +--- +"@effect/core": patch +--- + +add Differ datatype diff --git a/packages/core/_src/global.ts b/packages/core/_src/global.ts index dbc2785d5c6..b0d7c3a3e0c 100644 --- a/packages/core/_src/global.ts +++ b/packages/core/_src/global.ts @@ -71,6 +71,10 @@ import { DefaultServices } from "@effect/core/io/DefaultServices/definition" * @tsplus global */ import { Deferred } from "@effect/core/io/Deferred/definition" +/** + * @tsplus global + */ +import { Differ } from "@effect/core/io/Differ/definition" /** * @tsplus global */ diff --git a/packages/core/_src/index.ts b/packages/core/_src/index.ts index 107bf387545..1d4bc94aac0 100644 --- a/packages/core/_src/index.ts +++ b/packages/core/_src/index.ts @@ -6,6 +6,7 @@ export * as cause from "@effect/core/io/Cause" export * as clock from "@effect/core/io/Clock" export * as defaultServices from "@effect/core/io/DefaultServices" export * as deferred from "@effect/core/io/Deferred" +export * as differ from "@effect/core/io/Differ" export * as effect from "@effect/core/io/Effect" export * as executionStrategy from "@effect/core/io/ExecutionStrategy" export * as exit from "@effect/core/io/Exit" diff --git a/packages/core/_src/io/Differ.ts b/packages/core/_src/io/Differ.ts new file mode 100644 index 00000000000..cd9ee49784b --- /dev/null +++ b/packages/core/_src/io/Differ.ts @@ -0,0 +1,5 @@ +// codegen:start {preset: barrel, include: ./Differ/*.ts, prefix: "@effect/core/io"} +export * from "@effect/core/io/Differ/definition" +export * from "@effect/core/io/Differ/operations" +export * from "@effect/core/io/Differ/OrPatch" +// codegen:end diff --git a/packages/core/_src/io/Differ/ChunkPatch.ts b/packages/core/_src/io/Differ/ChunkPatch.ts new file mode 100644 index 00000000000..80031fe5ce1 --- /dev/null +++ b/packages/core/_src/io/Differ/ChunkPatch.ts @@ -0,0 +1,4 @@ +// codegen:start {preset: barrel, include: ./ChunkPatch/*.ts, prefix: "@effect/core/io/Differ"} +export * from "@effect/core/io/Differ/ChunkPatch/definition" +export * from "@effect/core/io/Differ/ChunkPatch/operations" +// codegen:end diff --git a/packages/core/_src/io/Differ/ChunkPatch/definition.ts b/packages/core/_src/io/Differ/ChunkPatch/definition.ts new file mode 100644 index 00000000000..59f6889fb2e --- /dev/null +++ b/packages/core/_src/io/Differ/ChunkPatch/definition.ts @@ -0,0 +1,102 @@ +export const ChunkPatchSym = Symbol.for("@effect/core/io/Differ.ChunkPatch") +export type ChunkPatchSym = typeof ChunkPatchSym + +export const ChunkPatchValueSym = Symbol.for("@effect/core/io/Differ.ChunkPatch.Value") +export type ChunkPatchValueSym = typeof ChunkPatchValueSym + +export const ChunkPatchPatchSym = Symbol.for("@effect/core/io/Differ.ChunkPatch.Patch") +export type ChunkPatchPatchSym = typeof ChunkPatchPatchSym + +/** + * A patch which describes updates to a chunk of values. + * + * @tsplus type effect/core/io/Differ.ChunkPatch + */ +export interface ChunkPatch { + readonly [ChunkPatchSym]: ChunkPatchSym + readonly [ChunkPatchValueSym]: () => Value + readonly [ChunkPatchPatchSym]: () => Patch +} + +/** + * @tsplus type effect/core/io/Differ.ChunkPatch.Ops + */ +export interface ChunkPatchOps { + readonly $: ChunkPatchAspects +} +/** + * @tsplus static effect/core/io/Differ.Ops ChunkPatch + */ +export const ChunkPatch: ChunkPatchOps = { + $: {} +} + +/** + * @tsplus type effect/core/io/Differ.ChunkPatch.Aspects + */ +export interface ChunkPatchAspects {} + +/** + * @tsplus unify effect/core/io/Differ.ChunkPatch + */ +export function unifyChunkPatch>(self: X): ChunkPatch< + [X] extends [{ [ChunkPatchValueSym]: () => infer Value }] ? Value : never, + [X] extends [{ [ChunkPatchPatchSym]: () => infer Patch }] ? Patch : never +> { + return self +} + +export abstract class BaseChunkPatch implements ChunkPatch { + readonly [ChunkPatchSym]: ChunkPatchSym = ChunkPatchSym + readonly [ChunkPatchValueSym]!: () => Value + readonly [ChunkPatchPatchSym]!: () => Patch +} + +export class AppendChunkPatch extends BaseChunkPatch { + readonly _tag = "Append" + constructor(readonly values: Chunk) { + super() + } +} + +export class SliceChunkPatch extends BaseChunkPatch { + readonly _tag = "Slice" + constructor(readonly from: number, readonly until: number) { + super() + } +} + +export class UpdateChunkPatch extends BaseChunkPatch { + readonly _tag = "Update" + constructor(readonly index: number, readonly patch: Patch) { + super() + } +} + +export class AndThenChunkPatch extends BaseChunkPatch { + readonly _tag = "AndThen" + constructor(readonly first: ChunkPatch, readonly second: ChunkPatch) { + super() + } +} + +export class EmptyChunkPatch extends BaseChunkPatch { + readonly _tag = "Empty" +} + +export type ChunkPatchInstruction = + | AppendChunkPatch + | SliceChunkPatch + | UpdateChunkPatch + | AndThenChunkPatch + | EmptyChunkPatch + +/** + * @tsplus macro identity + */ +export function chunkPatchInstruction( + self: ChunkPatch +): ChunkPatchInstruction { + // @ts-expect-error + return self +} diff --git a/packages/core/_src/io/Differ/ChunkPatch/operations.ts b/packages/core/_src/io/Differ/ChunkPatch/operations.ts new file mode 100644 index 00000000000..9794b270b9a --- /dev/null +++ b/packages/core/_src/io/Differ/ChunkPatch/operations.ts @@ -0,0 +1,6 @@ +// codegen:start {preset: barrel, include: ./operations/*.ts, prefix: "@effect/core/io/Differ/ChunkPatch"} +export * from "@effect/core/io/Differ/ChunkPatch/operations/apply" +export * from "@effect/core/io/Differ/ChunkPatch/operations/combine" +export * from "@effect/core/io/Differ/ChunkPatch/operations/diff" +export * from "@effect/core/io/Differ/ChunkPatch/operations/empty" +// codegen:end diff --git a/packages/core/_src/io/Differ/ChunkPatch/operations/apply.ts b/packages/core/_src/io/Differ/ChunkPatch/operations/apply.ts new file mode 100644 index 00000000000..f845e71fa21 --- /dev/null +++ b/packages/core/_src/io/Differ/ChunkPatch/operations/apply.ts @@ -0,0 +1,52 @@ +import type { ChunkPatch } from "@effect/core/io/Differ/ChunkPatch/definition" +import { chunkPatchInstruction } from "@effect/core/io/Differ/ChunkPatch/definition" + +/** + * Applies a chunk patch to a chunk of values to produce a new chunk of + * values which represents the original chunk of values updated with the + * changes described by this patch. + * + * @tsplus static effect/core/io/Differ.ChunkPatch.Aspects apply + * @tsplus pipeable effect/core/io/Differ.ChunkPatch apply + */ +export function apply(oldValue: Chunk, differ: Differ) { + return (self: ChunkPatch): Chunk => applyLoop(differ, oldValue, List(self)) +} + +/** + * @tsplus tailRec + */ +function applyLoop( + differ: Differ, + chunk: Chunk, + patches: List> +): Chunk { + if (patches.isNil()) { + return chunk + } + const patch = chunkPatchInstruction(patches.head) + const nextPatches = patches.tail + switch (patch._tag) { + case "Append": { + return applyLoop(differ, chunk.concat(patch.values), nextPatches) + } + case "AndThen": { + return applyLoop(differ, chunk, nextPatches.prepend(patch.second).prepend(patch.first)) + } + case "Empty": { + return applyLoop(differ, chunk, nextPatches) + } + case "Slice": { + return applyLoop( + differ, + Chunk.from(chunk.toArray.slice(patch.from, patch.until)), + nextPatches + ) + } + case "Update": { + const array = chunk.toArray + array[patch.index] = differ.patch(patch.patch, array[patch.index]!) + return applyLoop(differ, Chunk.from(array), nextPatches) + } + } +} diff --git a/packages/core/_src/io/Differ/ChunkPatch/operations/combine.ts b/packages/core/_src/io/Differ/ChunkPatch/operations/combine.ts new file mode 100644 index 00000000000..ef5480d76c3 --- /dev/null +++ b/packages/core/_src/io/Differ/ChunkPatch/operations/combine.ts @@ -0,0 +1,14 @@ +import type { ChunkPatch } from "@effect/core/io/Differ/ChunkPatch/definition" +import { AndThenChunkPatch } from "@effect/core/io/Differ/ChunkPatch/definition" + +/** + * Combines two chunk patches to produce a new chunk patch that describes + * applying their changes sequentially. + * + * @tsplus static effect/core/io/Differ.ChunkPatch.Aspects combine + * @tsplus pipeable effect/core/io/Differ.ChunkPatch combine + */ +export function combine(that: ChunkPatch) { + return (self: ChunkPatch): ChunkPatch => + new AndThenChunkPatch(self, that) +} diff --git a/packages/core/_src/io/Differ/ChunkPatch/operations/diff.ts b/packages/core/_src/io/Differ/ChunkPatch/operations/diff.ts new file mode 100644 index 00000000000..b3c9cb2ad7f --- /dev/null +++ b/packages/core/_src/io/Differ/ChunkPatch/operations/diff.ts @@ -0,0 +1,37 @@ +import type { ChunkPatch } from "@effect/core/io/Differ/ChunkPatch/definition" +import { + AppendChunkPatch, + SliceChunkPatch, + UpdateChunkPatch +} from "@effect/core/io/Differ/ChunkPatch/definition" + +/** + * Constructs a chunk patch from a new and old chunk of values and a differ + * for the values. + * + * @tsplus static effect/core/io/Differ.ChunkPatch.Ops diff + */ +export function diff( + oldValue: Chunk, + newValue: Chunk, + differ: Differ +): ChunkPatch { + let i = 0 + let patch = Differ.ChunkPatch.empty() + while (i < oldValue.length && i < newValue.length) { + const oldElement = oldValue.unsafeGet(i) + const newElement = newValue.unsafeGet(i) + const valuePatch = differ.diff(oldElement, newElement) + if (!Equals.equals(valuePatch, differ.empty)) { + patch = patch.combine(new UpdateChunkPatch(i, valuePatch)) + } + i = i + 1 + } + if (i < oldValue.length) { + patch = patch.combine(new SliceChunkPatch(0, i)) + } + if (i < newValue.length) { + patch = patch.combine(new AppendChunkPatch(newValue.drop(i))) + } + return patch +} diff --git a/packages/core/_src/io/Differ/ChunkPatch/operations/empty.ts b/packages/core/_src/io/Differ/ChunkPatch/operations/empty.ts new file mode 100644 index 00000000000..31d0785bd87 --- /dev/null +++ b/packages/core/_src/io/Differ/ChunkPatch/operations/empty.ts @@ -0,0 +1,11 @@ +import type { ChunkPatch } from "@effect/core/io/Differ/ChunkPatch/definition" +import { EmptyChunkPatch } from "@effect/core/io/Differ/ChunkPatch/definition" + +/** + * Constructs an empty chunk patch. + * + * @tsplus static effect/core/io/Differ.ChunkPatch.Ops empty + */ +export function empty(): ChunkPatch { + return new EmptyChunkPatch() +} diff --git a/packages/core/_src/io/Differ/HashMapPatch.ts b/packages/core/_src/io/Differ/HashMapPatch.ts new file mode 100644 index 00000000000..b10406f0eb3 --- /dev/null +++ b/packages/core/_src/io/Differ/HashMapPatch.ts @@ -0,0 +1,4 @@ +// codegen:start {preset: barrel, include: ./HashMapPatch/*.ts, prefix: "@effect/core/io/Differ"} +export * from "@effect/core/io/Differ/HashMapPatch/definition" +export * from "@effect/core/io/Differ/HashMapPatch/operations" +// codegen:end diff --git a/packages/core/_src/io/Differ/HashMapPatch/definition.ts b/packages/core/_src/io/Differ/HashMapPatch/definition.ts new file mode 100644 index 00000000000..7bfb95637e3 --- /dev/null +++ b/packages/core/_src/io/Differ/HashMapPatch/definition.ts @@ -0,0 +1,113 @@ +export const HashMapPatchSym = Symbol.for("@effect/core/io/Differ.HashMapPatch") +export type HashMapPatchSym = typeof HashMapPatchSym + +export const HashMapPatchKeySym = Symbol.for("@effect/core/io/Differ.HashMapPatch.Key") +export type HashMapPatchKeySym = typeof HashMapPatchKeySym + +export const HashMapPatchValueSym = Symbol.for("@effect/core/io/Differ.HashMapPatch.Value") +export type HashMapPatchValueSym = typeof HashMapPatchValueSym + +export const HashMapPatchPatchSym = Symbol.for("@effect/core/io/Differ.HashMapPatch.Patch") +export type HashMapPatchPatchSym = typeof HashMapPatchPatchSym + +/** + * A patch which describes updates to a map of keys and values. + * + * @tsplus type effect/core/io/Differ.HashMapPatch + */ +export interface HashMapPatch { + readonly [HashMapPatchSym]: HashMapPatchSym + readonly [HashMapPatchKeySym]: () => Key + readonly [HashMapPatchValueSym]: () => Value + readonly [HashMapPatchPatchSym]: () => Patch +} + +/** + * @tsplus type effect/core/io/Differ.HashMapPatch.Ops + */ +export interface HashMapPatchOps { + readonly $: HashMapPatchAspects +} +/** + * @tsplus static effect/core/io/Differ.Ops HashMapPatch + */ +export const HashMapPatch: HashMapPatchOps = { + $: {} +} + +/** + * @tsplus type effect/core/io/Differ.HashMapPatch.Aspects + */ +export interface HashMapPatchAspects {} + +/** + * @tsplus unify effect/core/io/Differ.HashMapPatch + */ +export function unifyHashMapPatch>(self: X): HashMapPatch< + [X] extends [{ [HashMapPatchKeySym]: () => infer Key }] ? Key : never, + [X] extends [{ [HashMapPatchValueSym]: () => infer Value }] ? Value : never, + [X] extends [{ [HashMapPatchPatchSym]: () => infer Patch }] ? Patch : never +> { + return self +} + +export abstract class BaseHashMapPatch + implements HashMapPatch +{ + readonly [HashMapPatchSym]: HashMapPatchSym = HashMapPatchSym + readonly [HashMapPatchKeySym]!: () => Key + readonly [HashMapPatchValueSym]!: () => Value + readonly [HashMapPatchPatchSym]!: () => Patch +} + +export class AddHashMapPatch extends BaseHashMapPatch { + readonly _tag = "Add" + constructor(readonly key: Key, readonly value: Value) { + super() + } +} + +export class RemoveHashMapPatch extends BaseHashMapPatch { + readonly _tag = "Remove" + constructor(readonly key: Key) { + super() + } +} + +export class UpdateHashMapPatch extends BaseHashMapPatch { + readonly _tag = "Update" + constructor(readonly key: Key, readonly patch: Patch) { + super() + } +} + +export class EmptyHashMapPatch extends BaseHashMapPatch { + readonly _tag = "Empty" +} + +export class AndThenHashMapPatch extends BaseHashMapPatch { + readonly _tag = "AndThen" + constructor( + readonly first: HashMapPatch, + readonly second: HashMapPatch + ) { + super() + } +} + +export type HashMapPatchInstruction = + | AddHashMapPatch + | RemoveHashMapPatch + | UpdateHashMapPatch + | EmptyHashMapPatch + | AndThenHashMapPatch + +/** + * @tsplus macro identity + */ +export function hashMapPatchInstruction( + self: HashMapPatch +): HashMapPatchInstruction { + // @ts-expect-error + return self +} diff --git a/packages/core/_src/io/Differ/HashMapPatch/operations.ts b/packages/core/_src/io/Differ/HashMapPatch/operations.ts new file mode 100644 index 00000000000..a47df6d6a04 --- /dev/null +++ b/packages/core/_src/io/Differ/HashMapPatch/operations.ts @@ -0,0 +1,6 @@ +// codegen:start {preset: barrel, include: ./operations/*.ts, prefix: "@effect/core/io/Differ/HashMapPatch"} +export * from "@effect/core/io/Differ/HashMapPatch/operations/apply" +export * from "@effect/core/io/Differ/HashMapPatch/operations/combine" +export * from "@effect/core/io/Differ/HashMapPatch/operations/diff" +export * from "@effect/core/io/Differ/HashMapPatch/operations/empty" +// codegen:end diff --git a/packages/core/_src/io/Differ/HashMapPatch/operations/apply.ts b/packages/core/_src/io/Differ/HashMapPatch/operations/apply.ts new file mode 100644 index 00000000000..6c482b3342e --- /dev/null +++ b/packages/core/_src/io/Differ/HashMapPatch/operations/apply.ts @@ -0,0 +1,57 @@ +import { hashMapPatchInstruction } from "@effect/core/io/Differ/HashMapPatch/definition" +import type { HashMapPatch } from "@effect/core/io/Differ/HashMapPatch/definition" + +/** + * Applies a map patch to a map of keys and values to produce a new map of + * keys and values values which represents the original map of keys and + * values updated with the changes described by this patch. + * + * @tsplus static effect/core/io/Differ.HashMapPatch.Aspects apply + * @tsplus pipeable effect/core/io/Differ.HashMapPatch apply + */ +export function apply( + oldValue: HashMap, + differ: Differ +) { + return (self: HashMapPatch): HashMap => + applyLoop(differ, oldValue, List(self)) +} + +/** + * @tsplus tailRec + */ +function applyLoop( + differ: Differ, + map: HashMap, + patches: List> +): HashMap { + if (patches.isNil()) { + return map + } + const patch = hashMapPatchInstruction(patches.head) + const nextPatches = patches.tail + switch (patch._tag) { + case "Add": { + return applyLoop(differ, map.set(patch.key, patch.value), nextPatches) + } + case "AndThen": { + return applyLoop(differ, map, nextPatches.prepend(patch.second).prepend(patch.first)) + } + case "Empty": { + return applyLoop(differ, map, nextPatches) + } + case "Remove": { + return applyLoop(differ, map.remove(patch.key), nextPatches) + } + case "Update": { + return applyLoop( + differ, + map.get(patch.key).fold( + map, + (oldValue) => map.set(patch.key, differ.patch(patch.patch, oldValue)) + ), + nextPatches + ) + } + } +} diff --git a/packages/core/_src/io/Differ/HashMapPatch/operations/combine.ts b/packages/core/_src/io/Differ/HashMapPatch/operations/combine.ts new file mode 100644 index 00000000000..805445d2cb5 --- /dev/null +++ b/packages/core/_src/io/Differ/HashMapPatch/operations/combine.ts @@ -0,0 +1,14 @@ +import type { HashMapPatch } from "@effect/core/io/Differ/HashMapPatch/definition" +import { AndThenHashMapPatch } from "@effect/core/io/Differ/HashMapPatch/definition" + +/** + * Combines two map patches to produce a new map patch that describes + * applying their changes sequentially. + * + * @tsplus static effect/core/io/Differ.HashMapPatch.Aspects combine + * @tsplus pipeable effect/core/io/Differ.HashMapPatch combine + */ +export function combine(that: HashMapPatch) { + return (self: HashMapPatch): HashMapPatch => + new AndThenHashMapPatch(self, that) +} diff --git a/packages/core/_src/io/Differ/HashMapPatch/operations/diff.ts b/packages/core/_src/io/Differ/HashMapPatch/operations/diff.ts new file mode 100644 index 00000000000..ac77716e6ff --- /dev/null +++ b/packages/core/_src/io/Differ/HashMapPatch/operations/diff.ts @@ -0,0 +1,41 @@ +import { + AddHashMapPatch, + HashMapPatch, + RemoveHashMapPatch, + UpdateHashMapPatch +} from "@effect/core/io/Differ/HashMapPatch/definition" + +/** + * Constructs a map patch from a new and old map of keys and values and a + * differ for the values. + * + * @tsplus static effect/core/io/Differ.HashMapPatch.Ops diff + */ +export function diff( + oldValue: HashMap, + newValue: HashMap, + differ: Differ +): HashMapPatch { + const { tuple: [removed, patch] } = newValue.reduceWithIndex( + Tuple(oldValue, HashMapPatch.empty()), + ({ tuple: [map, patch] }, key, newValue) => { + const maybe = map.get(key) + switch (maybe._tag) { + case "Some": { + const valuePatch = differ.diff(maybe.value, newValue) + if (Equals.equals(valuePatch, differ.empty)) { + return Tuple(map.remove(key), patch) + } + return Tuple(map.remove(key), patch.combine(new UpdateHashMapPatch(key, valuePatch))) + } + case "None": { + return Tuple(map.remove(key), patch.combine(new AddHashMapPatch(key, newValue))) + } + } + } + ) + return removed.reduceWithIndex( + patch, + (patch, key, _) => patch.combine(new RemoveHashMapPatch(key)) + ) +} diff --git a/packages/core/_src/io/Differ/HashMapPatch/operations/empty.ts b/packages/core/_src/io/Differ/HashMapPatch/operations/empty.ts new file mode 100644 index 00000000000..aa23c9c1347 --- /dev/null +++ b/packages/core/_src/io/Differ/HashMapPatch/operations/empty.ts @@ -0,0 +1,11 @@ +import { EmptyHashMapPatch } from "@effect/core/io/Differ/HashMapPatch/definition" +import type { HashMapPatch } from "@effect/core/io/Differ/HashMapPatch/definition" + +/** + * Constructs an empty map patch. + * + * @tsplus static effect/core/io/Differ.HashMapPatch.Ops empty + */ +export function empty(): HashMapPatch { + return new EmptyHashMapPatch() +} diff --git a/packages/core/_src/io/Differ/HashSetPatch.ts b/packages/core/_src/io/Differ/HashSetPatch.ts new file mode 100644 index 00000000000..70c92a6a472 --- /dev/null +++ b/packages/core/_src/io/Differ/HashSetPatch.ts @@ -0,0 +1,4 @@ +// codegen:start {preset: barrel, include: ./HashSetPatch/*.ts, prefix: "@effect/core/io/Differ"} +export * from "@effect/core/io/Differ/HashSetPatch/definition" +export * from "@effect/core/io/Differ/HashSetPatch/operations" +// codegen:end diff --git a/packages/core/_src/io/Differ/HashSetPatch/definition.ts b/packages/core/_src/io/Differ/HashSetPatch/definition.ts new file mode 100644 index 00000000000..dee28cc9a9a --- /dev/null +++ b/packages/core/_src/io/Differ/HashSetPatch/definition.ts @@ -0,0 +1,86 @@ +export const HashSetPatchSym = Symbol.for("@effect/core/io/Differ.HashSetPatch") +export type HashSetPatchSym = typeof HashSetPatchSym + +export const HashSetPatchValueSym = Symbol.for("@effect/core/io/Differ.HashSetPatch.Value") +export type HashSetPatchValueSym = typeof HashSetPatchValueSym + +/** + * A patch which describes updates to a set of values. + * + * @tsplus type effect/core/io/Differ.HashSetPatch + */ +export interface HashSetPatch { + readonly [HashSetPatchSym]: HashSetPatchSym + readonly [HashSetPatchValueSym]: () => Value +} + +/** + * @tsplus type effect/core/io/Differ.HashSetPatch.Ops + */ +export interface HashSetPatchOps { + readonly $: HashSetPatchAspects +} +/** + * @tsplus static effect/core/io/Differ.Ops HashSetPatch + */ +export const HashSetPatch: HashSetPatchOps = { + $: {} +} + +/** + * @tsplus type effect/core/io/Differ.HashSetPatch.Aspects + */ +export interface HashSetPatchAspects {} + +/** + * @tsplus unify effect/core/io/Differ.HashSetPatch + */ +export function unifyHashSetPatch>(self: X): HashSetPatch< + [X] extends [{ [HashSetPatchValueSym]: () => infer Value }] ? Value : never +> { + return self +} + +export abstract class BaseHashSetPatch implements HashSetPatch { + readonly [HashSetPatchSym]: HashSetPatchSym = HashSetPatchSym + readonly [HashSetPatchValueSym]!: () => Value +} + +export class AddHashSetPatch extends BaseHashSetPatch { + readonly _tag = "Add" + constructor(readonly value: Value) { + super() + } +} + +export class AndThenHashSetPatch extends BaseHashSetPatch { + readonly _tag = "AndThen" + constructor(readonly first: HashSetPatch, readonly second: HashSetPatch) { + super() + } +} + +export class EmptyHashSetPatch extends BaseHashSetPatch { + readonly _tag = "Empty" +} + +export class RemoveHashSetPatch extends BaseHashSetPatch { + readonly _tag = "Remove" + constructor(readonly value: Value) { + super() + } +} + +export type HashSetPatchInstruction = + | AddHashSetPatch + | AndThenHashSetPatch + | EmptyHashSetPatch + | RemoveHashSetPatch + +/** + * @tsplus macro identity + */ +export function hashSetPatchInstruction(self: HashSetPatch): HashSetPatchInstruction { + // @ts-expect-error + return self +} diff --git a/packages/core/_src/io/Differ/HashSetPatch/operations.ts b/packages/core/_src/io/Differ/HashSetPatch/operations.ts new file mode 100644 index 00000000000..c9080c965cf --- /dev/null +++ b/packages/core/_src/io/Differ/HashSetPatch/operations.ts @@ -0,0 +1,6 @@ +// codegen:start {preset: barrel, include: ./operations/*.ts, prefix: "@effect/core/io/Differ/HashSetPatch"} +export * from "@effect/core/io/Differ/HashSetPatch/operations/apply" +export * from "@effect/core/io/Differ/HashSetPatch/operations/combine" +export * from "@effect/core/io/Differ/HashSetPatch/operations/diff" +export * from "@effect/core/io/Differ/HashSetPatch/operations/empty" +// codegen:end diff --git a/packages/core/_src/io/Differ/HashSetPatch/operations/apply.ts b/packages/core/_src/io/Differ/HashSetPatch/operations/apply.ts new file mode 100644 index 00000000000..8ef75b20c00 --- /dev/null +++ b/packages/core/_src/io/Differ/HashSetPatch/operations/apply.ts @@ -0,0 +1,39 @@ +import type { HashSetPatch } from "@effect/core/io/Differ/HashSetPatch/definition" +import { hashSetPatchInstruction } from "@effect/core/io/Differ/HashSetPatch/definition" + +/** + * Applies a set patch to a set of values to produce a new set of values + * which represents the original set of values updated with the changes + * described by this patch. + * + * @tsplus static effect/core/io/Differ.HashSetPatch.Aspects apply + * @tsplus pipeable effect/core/io/Differ.HashSetPatch apply + */ +export function apply(oldValue: HashSet) { + return (self: HashSetPatch): HashSet => applyLoop(oldValue, List(self)) +} + +/** + * @tsplus tailRec + */ +function applyLoop(set: HashSet, patches: List>): HashSet { + if (patches.isNil()) { + return set + } + const patch = hashSetPatchInstruction(patches.head) + const nextPatches = patches.tail + switch (patch._tag) { + case "Add": { + return applyLoop(set.add(patch.value), nextPatches) + } + case "AndThen": { + return applyLoop(set, nextPatches.prepend(patch.second).prepend(patch.first)) + } + case "Empty": { + return applyLoop(set, nextPatches) + } + case "Remove": { + return applyLoop(set.remove(patch.value), nextPatches) + } + } +} diff --git a/packages/core/_src/io/Differ/HashSetPatch/operations/combine.ts b/packages/core/_src/io/Differ/HashSetPatch/operations/combine.ts new file mode 100644 index 00000000000..b0ba1a9573b --- /dev/null +++ b/packages/core/_src/io/Differ/HashSetPatch/operations/combine.ts @@ -0,0 +1,13 @@ +import type { HashSetPatch } from "@effect/core/io/Differ/HashSetPatch/definition" +import { AndThenHashSetPatch } from "@effect/core/io/Differ/HashSetPatch/definition" + +/** + * Combines two set patches to produce a new set patch that describes + * applying their changes sequentially. + * + * @tsplus static effect/core/io/Differ.HashSetPatch.Aspects combine + * @tsplus pipeable effect/core/io/Differ.HashSetPatch combine + */ +export function combine(that: HashSetPatch) { + return (self: HashSetPatch): HashSetPatch => new AndThenHashSetPatch(self, that) +} diff --git a/packages/core/_src/io/Differ/HashSetPatch/operations/diff.ts b/packages/core/_src/io/Differ/HashSetPatch/operations/diff.ts new file mode 100644 index 00000000000..37b66e75037 --- /dev/null +++ b/packages/core/_src/io/Differ/HashSetPatch/operations/diff.ts @@ -0,0 +1,26 @@ +import { + AddHashSetPatch, + HashSetPatch, + RemoveHashSetPatch +} from "@effect/core/io/Differ/HashSetPatch/definition" + +/** + * Constructs a set patch from a new set of values. + * + * @tsplus static effect/core/io/Differ.HashSetPatch.Ops diff + */ +export function diff( + oldValue: HashSet, + newValue: HashSet +): HashSetPatch { + const { tuple: [removed, patch] } = newValue.reduce( + Tuple(oldValue, HashSetPatch.empty()), + ({ tuple: [set, patch] }, value) => { + if (set.has(value)) { + return Tuple(set.remove(value), patch) + } + return Tuple(set, patch.combine(new AddHashSetPatch(value))) + } + ) + return removed.reduce(patch, (patch, value) => patch.combine(new RemoveHashSetPatch(value))) +} diff --git a/packages/core/_src/io/Differ/HashSetPatch/operations/empty.ts b/packages/core/_src/io/Differ/HashSetPatch/operations/empty.ts new file mode 100644 index 00000000000..9437e8a87c1 --- /dev/null +++ b/packages/core/_src/io/Differ/HashSetPatch/operations/empty.ts @@ -0,0 +1,11 @@ +import type { HashSetPatch } from "@effect/core/io/Differ/HashSetPatch/definition" +import { EmptyHashSetPatch } from "@effect/core/io/Differ/HashSetPatch/definition" + +/** + * Constructs an empty set patch. + * + * @tsplus static effect/core/io/Differ.HashSetPatch.Ops empty + */ +export function empty(): HashSetPatch { + return new EmptyHashSetPatch() +} diff --git a/packages/core/_src/io/Differ/OrPatch.ts b/packages/core/_src/io/Differ/OrPatch.ts new file mode 100644 index 00000000000..257aa201fb5 --- /dev/null +++ b/packages/core/_src/io/Differ/OrPatch.ts @@ -0,0 +1,4 @@ +// codegen:start {preset: barrel, include: ./OrPatch/*.ts, prefix: "@effect/core/io/Differ"} +export * from "@effect/core/io/Differ/OrPatch/definition" +export * from "@effect/core/io/Differ/OrPatch/operations" +// codegen:end diff --git a/packages/core/_src/io/Differ/OrPatch/definition.ts b/packages/core/_src/io/Differ/OrPatch/definition.ts new file mode 100644 index 00000000000..1e013539ad4 --- /dev/null +++ b/packages/core/_src/io/Differ/OrPatch/definition.ts @@ -0,0 +1,139 @@ +export const OrPatchSym = Symbol.for("@effect/core/io/Differ.OrPatch") +export type OrPatchSym = typeof OrPatchSym + +export const OrPatchValueSym = Symbol.for("@effect/core/io/Differ.OrPatch.Value") +export type OrPatchValueSym = typeof OrPatchValueSym + +export const OrPatchValue2Sym = Symbol.for("@effect/core/io/Differ.OrPatch.Value2") +export type OrPatchValue2Sym = typeof OrPatchValueSym + +export const OrPatchPatchSym = Symbol.for("@effect/core/io/Differ.OrPatch.Patch") +export type OrPatchPatchSym = typeof OrPatchPatchSym + +export const OrPatchPatch2Sym = Symbol.for("@effect/core/io/Differ.OrPatch.Patch2") +export type OrPatchPatch2Sym = typeof OrPatchPatch2Sym + +/** + * A patch which describes updates to either one value or another. + * + * @tsplus type effect/core/io/Differ.OrPatch + */ +export interface OrPatch { + readonly [OrPatchSym]: OrPatchSym + readonly [OrPatchValueSym]: () => Value + readonly [OrPatchValue2Sym]: () => Value2 + readonly [OrPatchPatchSym]: () => Patch + readonly [OrPatchPatch2Sym]: () => Patch2 +} + +/** + * @tsplus type effect/core/io/Differ.OrPatch.Ops + */ +export interface OrPatchOps { + readonly $: OrPatchAspects +} +/** + * @tsplus static effect/core/io/Differ.Ops OrPatch + */ +export const OrPatch: OrPatchOps = { + $: {} +} + +/** + * @tsplus type effect/core/io/Differ.OrPatch.Aspects + */ +export interface OrPatchAspects {} + +/** + * @tsplus unify effect/core/io/Differ.OrPatch + */ +export function unifyOrPatch>(self: X): OrPatch< + [X] extends [{ [OrPatchValueSym]: () => infer Value }] ? Value : never, + [X] extends [{ [OrPatchValue2Sym]: () => infer Value2 }] ? Value2 : never, + [X] extends [{ [OrPatchPatchSym]: () => infer Patch }] ? Patch : never, + [X] extends [{ [OrPatchPatch2Sym]: () => infer Patch2 }] ? Patch2 : never +> { + return self +} + +export abstract class BaseOrPatch + implements OrPatch +{ + readonly [OrPatchSym]: OrPatchSym = OrPatchSym + readonly [OrPatchValueSym]!: () => Value + readonly [OrPatchValue2Sym]!: () => Value2 + readonly [OrPatchPatchSym]!: () => Patch + readonly [OrPatchPatch2Sym]!: () => Patch2 +} + +export class AndThenOrPatch + extends BaseOrPatch +{ + readonly _tag = "AndThen" + constructor( + readonly first: OrPatch, + readonly second: OrPatch + ) { + super() + } +} + +export class EmptyOrPatch + extends BaseOrPatch +{ + readonly _tag = "Empty" +} + +export class SetLeftOrPatch + extends BaseOrPatch +{ + readonly _tag = "SetLeft" + constructor(readonly value: Value) { + super() + } +} + +export class SetRightOrPatch + extends BaseOrPatch +{ + readonly _tag = "SetRight" + constructor(readonly value: Value2) { + super() + } +} + +export class UpdateLeftOrPatch + extends BaseOrPatch +{ + readonly _tag = "UpdateLeft" + constructor(readonly patch: Patch) { + super() + } +} + +export class UpdateRightOrPatch + extends BaseOrPatch +{ + readonly _tag = "UpdateRight" + constructor(readonly patch: Patch2) { + super() + } +} + +export type OrPatchInstruction = + | AndThenOrPatch + | EmptyOrPatch + | SetLeftOrPatch + | SetRightOrPatch + | UpdateLeftOrPatch + | UpdateRightOrPatch + +/** + * @tsplus macro identity + */ +export function orPatchInstruction( + self: OrPatch +): OrPatchInstruction { + // @ts-expect-error + return self +} diff --git a/packages/core/_src/io/Differ/OrPatch/operations.ts b/packages/core/_src/io/Differ/OrPatch/operations.ts new file mode 100644 index 00000000000..24bf24eb4f2 --- /dev/null +++ b/packages/core/_src/io/Differ/OrPatch/operations.ts @@ -0,0 +1,6 @@ +// codegen:start {preset: barrel, include: ./operations/*.ts, prefix: "@effect/core/io/Differ/OrPatch"} +export * from "@effect/core/io/Differ/OrPatch/operations/apply" +export * from "@effect/core/io/Differ/OrPatch/operations/combine" +export * from "@effect/core/io/Differ/OrPatch/operations/diff" +export * from "@effect/core/io/Differ/OrPatch/operations/empty" +// codegen:end diff --git a/packages/core/_src/io/Differ/OrPatch/operations/apply.ts b/packages/core/_src/io/Differ/OrPatch/operations/apply.ts new file mode 100644 index 00000000000..be9886152f6 --- /dev/null +++ b/packages/core/_src/io/Differ/OrPatch/operations/apply.ts @@ -0,0 +1,78 @@ +import type { OrPatch } from "@effect/core/io/Differ/OrPatch/definition" +import { orPatchInstruction } from "@effect/core/io/Differ/OrPatch/definition" + +/** + * Applies an or patch to a value to produce a new value which represents + * the original value updated with the changes described by this patch. + * + * @tsplus static effect/core/io/Differ.OrPatch.Aspects apply + * @tsplus pipeable effect/core/io/Differ.OrPatch apply + */ +export function apply( + oldValue: Either, + left: Differ, + right: Differ +) { + return (self: OrPatch): Either => + applyLoop(left, right, oldValue, List(self)) +} + +/** + * @tsplus tailRec + */ +function applyLoop( + left: Differ, + right: Differ, + either: Either, + patches: List> +): Either { + if (patches.isNil()) { + return either + } + const patch = orPatchInstruction(patches.head) + const nextPatches = patches.tail + switch (patch._tag) { + case "AndThen": { + return applyLoop(left, right, either, nextPatches.prepend(patch.second).prepend(patch.first)) + } + case "Empty": { + return applyLoop(left, right, either, nextPatches) + } + case "UpdateLeft": { + switch (either._tag) { + case "Left": { + return applyLoop( + left, + right, + Either.left(left.patch(patch.patch, either.left)), + nextPatches + ) + } + case "Right": { + return applyLoop(left, right, either, nextPatches) + } + } + } + case "UpdateRight": { + switch (either._tag) { + case "Left": { + return applyLoop(left, right, either, nextPatches) + } + case "Right": { + return applyLoop( + left, + right, + Either.right(right.patch(patch.patch, either.right)), + nextPatches + ) + } + } + } + case "SetLeft": { + return applyLoop(left, right, Either.left(patch.value), nextPatches) + } + case "SetRight": { + return applyLoop(left, right, Either.right(patch.value), nextPatches) + } + } +} diff --git a/packages/core/_src/io/Differ/OrPatch/operations/combine.ts b/packages/core/_src/io/Differ/OrPatch/operations/combine.ts new file mode 100644 index 00000000000..454bed5bfbb --- /dev/null +++ b/packages/core/_src/io/Differ/OrPatch/operations/combine.ts @@ -0,0 +1,14 @@ +import type { OrPatch } from "@effect/core/io/Differ/OrPatch/definition" +import { AndThenOrPatch } from "@effect/core/io/Differ/OrPatch/definition" + +/** + * Combines two or patches to produce a new or patch that describes applying + * their changes sequentially. + * + * @tsplus static effect/core/io/Differ.OrPatch.Aspects combine + * @tsplus pipeable effect/core/io/Differ.OrPatch combine + */ +export function combine(that: OrPatch) { + return (self: OrPatch): OrPatch => + new AndThenOrPatch(self, that) +} diff --git a/packages/core/_src/io/Differ/OrPatch/operations/diff.ts b/packages/core/_src/io/Differ/OrPatch/operations/diff.ts new file mode 100644 index 00000000000..0be825aaaf8 --- /dev/null +++ b/packages/core/_src/io/Differ/OrPatch/operations/diff.ts @@ -0,0 +1,52 @@ +import type { OrPatch } from "@effect/core/io/Differ/OrPatch/definition" +import { + EmptyOrPatch, + SetLeftOrPatch, + SetRightOrPatch, + UpdateLeftOrPatch, + UpdateRightOrPatch +} from "@effect/core/io/Differ/OrPatch/definition" + +/** + * Constructs an `OrPatch` from a new and old value and a differ for the + * values. + * + * @tsplus static effect/core/io/Differ.OrPatch.Ops diff + */ +export function diff( + oldValue: Either, + newValue: Either, + left: Differ, + right: Differ +): OrPatch { + switch (oldValue._tag) { + case "Left": { + switch (newValue._tag) { + case "Left": { + const valuePatch = left.diff(oldValue.left, newValue.left) + if (Equals.equals(valuePatch, left.empty)) { + return new EmptyOrPatch() + } + return new UpdateLeftOrPatch(valuePatch) + } + case "Right": { + return new SetRightOrPatch(newValue.right) + } + } + } + case "Right": { + switch (newValue._tag) { + case "Left": { + return new SetLeftOrPatch(newValue.left) + } + case "Right": { + const valuePatch = right.diff(oldValue.right, newValue.right) + if (Equals.equals(valuePatch, right.empty)) { + return new EmptyOrPatch() + } + return new UpdateRightOrPatch(valuePatch) + } + } + } + } +} diff --git a/packages/core/_src/io/Differ/OrPatch/operations/empty.ts b/packages/core/_src/io/Differ/OrPatch/operations/empty.ts new file mode 100644 index 00000000000..e24712bd3fc --- /dev/null +++ b/packages/core/_src/io/Differ/OrPatch/operations/empty.ts @@ -0,0 +1,11 @@ +import type { OrPatch } from "@effect/core/io/Differ/OrPatch/definition" +import { EmptyOrPatch } from "@effect/core/io/Differ/OrPatch/definition" + +/** + * Constructs an empty or patch. + * + * @tsplus static effect/core/io/Differ.OrPatch.Ops empty + */ +export function empty(): OrPatch { + return new EmptyOrPatch() +} diff --git a/packages/core/_src/io/Differ/definition.ts b/packages/core/_src/io/Differ/definition.ts new file mode 100644 index 00000000000..6bc3c2feed1 --- /dev/null +++ b/packages/core/_src/io/Differ/definition.ts @@ -0,0 +1,67 @@ +import type * as OP from "@effect/core/io/Differ/OrPatch/definition" + +export const DifferSym = Symbol.for("@effect/core/io/Differ") +export type DifferSym = typeof DifferSym + +/** + * A `Differ` knows how to compare an old value and new value of + * type `Value` to produce a patch of type `Patch` that describes the + * differences between those values. A `Differ` also knows how to apply a patch + * to an old value to produce a new value that represents the old value updated + * with the changes described by the patch. + * + * A `Differ` can be used to construct a `FiberRef` supporting compositional + * updates using the `FiberRef.makePatch` constructor. + * + * The `Differ` companion object contains constructors for `Differ` values for + * common data types such as `Chunk`, `HashMap`, and `HashSet``. In addition, + * `Differ`values can be transformed using the `transform` operator and combined + * using the `orElseEither` and `zip` operators. This allows creating `Differ` + * values for arbitrarily complex data types compositionally. + * + * @tsplus type effect/core/io/Differ + */ +export interface Differ { + readonly [DifferSym]: DifferSym + /** + * An empty patch that describes no changes. + */ + readonly empty: Patch + /** + * Constructs a patch describing the updates to a value from an old value and + * a new value. + */ + readonly diff: (oldValue: Value, newValue: Value) => Patch + /** + * Combines two patches to produce a new patch that describes the updates of + * the first patch and then the updates of the second patch. The combine + * operation should be associative. In addition, if the combine operation is + * commutative then joining multiple fibers concurrently will result in + * deterministic `FiberRef` values. + */ + readonly combine: (first: Patch, second: Patch) => Patch + /** + * Applies a patch to an old value to produce a new value that is equal to the + * old value with the updates described by the patch. + */ + readonly patch: (patch: Patch, oldValue: Value) => Value +} + +export declare namespace Differ { + export type OrPatch = OP.OrPatch +} + +/** + * @tsplus type effect/core/io/Differ.Ops + */ +export interface DifferOps { + readonly $: DifferAspects +} +export const Differ: DifferOps = { + $: {} +} + +/** + * @tsplus type effect/core/io/Differ.Aspects + */ +export interface DifferAspects {} diff --git a/packages/core/_src/io/Differ/operations.ts b/packages/core/_src/io/Differ/operations.ts new file mode 100644 index 00000000000..b49205501af --- /dev/null +++ b/packages/core/_src/io/Differ/operations.ts @@ -0,0 +1,12 @@ +// codegen:start {preset: barrel, include: ./operations/*.ts, prefix: "@effect/core/io/Differ"} +export * from "@effect/core/io/Differ/operations/chunk" +export * from "@effect/core/io/Differ/operations/environment" +export * from "@effect/core/io/Differ/operations/hashMap" +export * from "@effect/core/io/Differ/operations/hashSet" +export * from "@effect/core/io/Differ/operations/make" +export * from "@effect/core/io/Differ/operations/orElseEither" +export * from "@effect/core/io/Differ/operations/transform" +export * from "@effect/core/io/Differ/operations/update" +export * from "@effect/core/io/Differ/operations/updateWith" +export * from "@effect/core/io/Differ/operations/zip" +// codegen:end diff --git a/packages/core/_src/io/Differ/operations/chunk.ts b/packages/core/_src/io/Differ/operations/chunk.ts new file mode 100644 index 00000000000..d7afeff239c --- /dev/null +++ b/packages/core/_src/io/Differ/operations/chunk.ts @@ -0,0 +1,18 @@ +import { ChunkPatch } from "@effect/core/io/Differ/ChunkPatch/definition" + +/** + * Constructs a differ that knows how to diff a `Chunk` of values given a + * differ that knows how to diff the values. + * + * @tsplus static effect/core/io/Differ.Ops chunk + */ +export function chunk( + differ: Differ +): Differ, ChunkPatch> { + return Differ.make({ + empty: ChunkPatch.empty(), + combine: (first, second) => first.combine(second), + diff: (oldValue, newValue) => ChunkPatch.diff(oldValue, newValue, differ), + patch: (patch, oldValue) => patch.apply(oldValue, differ) + }) +} diff --git a/packages/core/_src/io/Differ/operations/environment.ts b/packages/core/_src/io/Differ/operations/environment.ts new file mode 100644 index 00000000000..d3d5a25ee55 --- /dev/null +++ b/packages/core/_src/io/Differ/operations/environment.ts @@ -0,0 +1,13 @@ +/** + * Constructs a differ that knows how to diff `Env` values. + * + * @tsplus static effect/core/io/Differ.Ops environment + */ +export function environment(): Differ, Service.Patch> { + return Differ.make({ + empty: Service.Patch.empty(), + combine: (first, second) => first.combine(second), + diff: (oldValue, newValue) => Service.Patch.diff(oldValue, newValue), + patch: (patch, oldValue) => patch.patch(oldValue) + }) +} diff --git a/packages/core/_src/io/Differ/operations/hashMap.ts b/packages/core/_src/io/Differ/operations/hashMap.ts new file mode 100644 index 00000000000..fc3090bacf5 --- /dev/null +++ b/packages/core/_src/io/Differ/operations/hashMap.ts @@ -0,0 +1,18 @@ +import { HashMapPatch } from "@effect/core/io/Differ/HashMapPatch/definition" + +/** + * Constructs a differ that knows how to diff a `HashMap` of keys and values given + * a differ that knows how to diff the values. + * + * @tsplus static effect/core/io/Differ.Ops hashMap + */ +export function hashMap( + differ: Differ +): Differ, HashMapPatch> { + return Differ.make({ + empty: HashMapPatch.empty(), + combine: (first, second) => first.combine(second), + diff: (oldValue, newValue) => HashMapPatch.diff(oldValue, newValue, differ), + patch: (patch, oldValue) => patch.apply(oldValue, differ) + }) +} diff --git a/packages/core/_src/io/Differ/operations/hashSet.ts b/packages/core/_src/io/Differ/operations/hashSet.ts new file mode 100644 index 00000000000..4d5ee125485 --- /dev/null +++ b/packages/core/_src/io/Differ/operations/hashSet.ts @@ -0,0 +1,15 @@ +import { HashSetPatch } from "@effect/core/io/Differ/HashSetPatch/definition" + +/** + * Constructs a differ that knows how to diff a `HashSet` of values. + * + * @tsplus static effect/core/io/Differ.Ops hashSet + */ +export function hashSet(): Differ, HashSetPatch> { + return Differ.make({ + empty: HashSetPatch.empty(), + combine: (first, second) => first.combine(second), + diff: (oldValue, newValue) => HashSetPatch.diff(oldValue, newValue), + patch: (patch, oldValue) => patch.apply(oldValue) + }) +} diff --git a/packages/core/_src/io/Differ/operations/make.ts b/packages/core/_src/io/Differ/operations/make.ts new file mode 100644 index 00000000000..2de4f1a24e3 --- /dev/null +++ b/packages/core/_src/io/Differ/operations/make.ts @@ -0,0 +1,19 @@ +import { DifferSym } from "@effect/core/io/Differ/definition" + +/** + * Constructs a new `Differ`. + * + * @tsplus static effect/core/io/Differ.Ops __call + * @tsplus static effect/core/io/Differ.Ops make + */ +export function make(params: { + readonly empty: Patch + readonly diff: (oldValue: Value, newValue: Value) => Patch + readonly combine: (first: Patch, second: Patch) => Patch + readonly patch: (patch: Patch, oldValue: Value) => Value +}): Differ { + return { + [DifferSym]: DifferSym, + ...params + } +} diff --git a/packages/core/_src/io/Differ/operations/orElseEither.ts b/packages/core/_src/io/Differ/operations/orElseEither.ts new file mode 100644 index 00000000000..690632fa2d8 --- /dev/null +++ b/packages/core/_src/io/Differ/operations/orElseEither.ts @@ -0,0 +1,18 @@ +/** + * Combines this differ and the specified differ to produce a differ that + * knows how to diff the sum of their values. + * + * @tsplus static effect/core/io/Differ.Aspects orElseEither + * @tsplus pipeable effect/core/io/Differ orElseEither + */ +export function orElseEither(that: Differ) { + return ( + self: Differ + ): Differ, Differ.OrPatch> => + Differ.make({ + empty: Differ.OrPatch.empty(), + combine: (first, second) => first.combine(second), + diff: (oldValue, newValue) => Differ.OrPatch.diff(oldValue, newValue, self, that), + patch: (patch, oldValue) => patch.apply(oldValue, self, that) + }) +} diff --git a/packages/core/_src/io/Differ/operations/transform.ts b/packages/core/_src/io/Differ/operations/transform.ts new file mode 100644 index 00000000000..ad0d6885514 --- /dev/null +++ b/packages/core/_src/io/Differ/operations/transform.ts @@ -0,0 +1,16 @@ +/** + * Transforms the type of values that this differ knows how to differ using + * the specified functions that map the new and old value types to each other. + * + * @tsplus static effect/core/io/Differ.Aspects transform + * @tsplus pipeable effect/core/io/Differ transform + */ +export function transform(f: (value: Value) => Value2, g: (value: Value2) => Value) { + return (self: Differ): Differ => + Differ.make({ + empty: self.empty, + combine: (first, second) => self.combine(first, second), + diff: (oldValue, newValue) => self.diff(g(oldValue), g(newValue)), + patch: (patch, oldValue) => f(self.patch(patch, g(oldValue))) + }) +} diff --git a/packages/core/_src/io/Differ/operations/update.ts b/packages/core/_src/io/Differ/operations/update.ts new file mode 100644 index 00000000000..47d3c8cde01 --- /dev/null +++ b/packages/core/_src/io/Differ/operations/update.ts @@ -0,0 +1,11 @@ +/** + * Constructs a differ that just diffs two values by returning a function that + * sets the value to the new value. This differ does not support combining + * multiple updates to the value compositionally and should only be used when + * there is no compositional way to update them. + * + * @tsplus static effect/core/io/Differ.Ops update + */ +export function update(): Differ A> { + return Differ.updateWith((_, a) => a) +} diff --git a/packages/core/_src/io/Differ/operations/updateWith.ts b/packages/core/_src/io/Differ/operations/updateWith.ts new file mode 100644 index 00000000000..9c2dd6c6593 --- /dev/null +++ b/packages/core/_src/io/Differ/operations/updateWith.ts @@ -0,0 +1,29 @@ +import { constant } from "@tsplus/stdlib/data/Function" + +/** + * A variant of `update` that allows specifying the function that will be used + * to combine old values with new values. + * + * @tsplus static effect/core/io/Differ.Ops updateWith + */ +export function updateWith(f: (x: A, y: A) => A): Differ A> { + return Differ.make({ + empty: identity, + combine: (first, second) => { + if (Equals.equals(first, identity)) { + return second + } + if (Equals.equals(second, identity)) { + return first + } + return (a) => second(first(a)) + }, + diff: (oldValue, newValue) => { + if (Equals.equals(oldValue, newValue)) { + return identity + } + return constant(newValue) + }, + patch: (patch, oldValue) => f(oldValue, patch(oldValue)) + }) +} diff --git a/packages/core/_src/io/Differ/operations/zip.ts b/packages/core/_src/io/Differ/operations/zip.ts new file mode 100644 index 00000000000..5d44b1f706a --- /dev/null +++ b/packages/core/_src/io/Differ/operations/zip.ts @@ -0,0 +1,33 @@ +/** + * Combines this differ and the specified differ to produce a new differ that + * knows how to diff the product of their values. + * + * @tsplus static effect/core/io/Differ.Aspects zip + * @tsplus pipeable effect/core/io/Differ zip + */ +export function zip(that: Differ) { + return ( + self: Differ + ): Differ, Tuple<[Patch, Patch2]>> => + Differ.make({ + empty: Tuple( + self.empty, + that.empty + ), + combine: (first, second) => + Tuple( + self.combine(first.get(0), second.get(0)), + that.combine(first.get(1), second.get(1)) + ), + diff: (oldValue, newValue) => + Tuple( + self.diff(oldValue.get(0), newValue.get(0)), + that.diff(oldValue.get(1), newValue.get(1)) + ), + patch: (patch, oldValue) => + Tuple( + self.patch(patch.get(0), oldValue.get(0)), + that.patch(patch.get(1), oldValue.get(1)) + ) + }) +} diff --git a/packages/core/_src/io/FiberRef/operations/_internal.ts b/packages/core/_src/io/FiberRef/operations/_internal.ts index f6276d164f4..328c63adc90 100644 --- a/packages/core/_src/io/FiberRef/operations/_internal.ts +++ b/packages/core/_src/io/FiberRef/operations/_internal.ts @@ -16,21 +16,20 @@ export class FiberRefInternal implements FiberRef.WithPatch Patch, - readonly combine: (first: Patch, second: Patch) => Patch, - readonly patch: (patch: Patch) => (oldValue: Value) => Value, + readonly differ: Differ, readonly fork: Patch ) {} - /** - * Atomically modifies the `FiberRef` with the specified function, which - * computes a return value for the modification. This is a more powerful - * version of `update`. - */ - modify( - f: (a: Value) => Tuple<[B, Value]> - ): Effect { - return new IFiberRefModify(this, f) + diff(oldValue: Value, newValue: Value): Patch { + return this.differ.diff(oldValue, newValue) + } + + combine(first: Patch, second: Patch): Patch { + return this.differ.combine(first, second) + } + + patch(patch: Patch) { + return (oldValue: Value): Value => this.differ.patch(patch, oldValue) } get get(): Effect { @@ -45,6 +44,17 @@ export class FiberRefInternal implements FiberRef.WithPatch( + f: (a: Value) => Tuple<[B, Value]> + ): Effect { + return new IFiberRefModify(this, f) + } + getAndSet( this: FiberRef.WithPatch, value: Value @@ -205,14 +215,10 @@ export function makeEnvironment( */ export function makePatch( initial: Value, - diff: (oldValue: Value, newValue: Value) => Patch, - combine: (first: Patch, second: Patch) => Patch, - patch: (patch: Patch) => (oldValue: Value) => Value, + differ: Differ, fork: Patch ): Effect> { - return FiberRef.makeWith( - FiberRef.unsafeMakePatch(initial, diff, combine, patch, fork) - ) + return FiberRef.makeWith(FiberRef.unsafeMakePatch(initial, differ, fork)) } /** @@ -233,13 +239,11 @@ export function makeWith( export function unsafeMake( initial: A, fork: (a: A) => A = identity, - join: (left: A, right: A) => A = (_, a) => a + join: (left: A, right: A) => A = (_, right) => right ): FiberRef { return FiberRef.unsafeMakePatch A>( initial, - (_, newValue) => () => newValue, - (first, second) => (value) => second(first(value)), - (patch) => (value) => join(value, patch(value)), + Differ.updateWith(join), fork ) } @@ -250,13 +254,7 @@ export function unsafeMake( export function unsafeMakeEnvironment( initial: Service.Env ): FiberRef.WithPatch, Service.Patch> { - return new FiberRefInternal( - initial, - Service.Patch.diff, - (first, second) => first.combine(second), - (patch) => (value) => patch.patch(value), - Service.Patch.empty() - ) + return FiberRef.unsafeMakePatch(initial, Differ.environment(), Service.Patch.empty()) } /** @@ -264,18 +262,10 @@ export function unsafeMakeEnvironment( */ export function unsafeMakePatch( initial: Value, - diff: (oldValue: Value, newValue: Value) => Patch, - combine: (first: Patch, second: Patch) => Patch, - patch: (patch: Patch) => (oldValue: Value) => Value, + differ: Differ, fork: Patch ): FiberRef.WithPatch { - return new FiberRefInternal( - initial, - diff, - combine, - patch, - fork - ) + return new FiberRefInternal(initial, differ, fork) } // diff --git a/packages/core/_src/io/Ref/definition.ts b/packages/core/_src/io/Ref/definition.ts index cb6eecf461a..415f465c212 100644 --- a/packages/core/_src/io/Ref/definition.ts +++ b/packages/core/_src/io/Ref/definition.ts @@ -42,7 +42,7 @@ export interface Ref { /** * Reads the value from the `Ref`. */ - get get(): Effect.UIO + get get(): Effect /** * Atomically modifies the `Ref` with the specified function, which computes a diff --git a/packages/core/_test/io/Differ/differ.test.ts b/packages/core/_test/io/Differ/differ.test.ts new file mode 100644 index 00000000000..7bdf0c1d0e6 --- /dev/null +++ b/packages/core/_test/io/Differ/differ.test.ts @@ -0,0 +1,45 @@ +import { diffLaws } from "@effect/core/test/io/Differ/test-utils" + +const smallInt = Gen.int({ min: 0, max: 100 }) + +describe.concurrent("Differ", () => { + describe.concurrent("chunk", () => { + diffLaws( + Differ.chunk number>(Differ.update()), + Gen.chunkOf(smallInt), + Equals.equals + ) + }) + + describe.concurrent("either", () => { + diffLaws( + Differ.update().orElseEither(Differ.update()), + Gen.either(smallInt, smallInt), + Equals.equals + ) + }) + + // describe.concurrent("hashMap", () => { + // diffLaws( + // Differ.hashMap number>(Differ.update()), + // Gen.mapOf(smallInt, smallInt), + // Equals.equals + // ) + // }) + + describe.concurrent("hashSet", () => { + diffLaws( + Differ.hashSet(), + Gen.setOf(smallInt), + Equals.equals + ) + }) + + describe.concurrent("tuple", () => { + diffLaws( + Differ.update().zip(Differ.update()), + smallInt.zip(smallInt), + Equals.equals + ) + }) +}) diff --git a/packages/core/_test/io/Differ/test-utils.ts b/packages/core/_test/io/Differ/test-utils.ts new file mode 100644 index 00000000000..ece0dfcdc3c --- /dev/null +++ b/packages/core/_test/io/Differ/test-utils.ts @@ -0,0 +1,57 @@ +export function diffLaws( + differ: Differ, + gen: Gen, + equal: (a: Value, b: Value) => boolean +): void { + describe.concurrent("differ laws", () => { + it.effect("combining patches is associative", () => + Do(($) => { + const values = $(gen.runCollectN(4)) + const value1 = values.unsafeGet(0) + const value2 = values.unsafeGet(1) + const value3 = values.unsafeGet(2) + const value4 = values.unsafeGet(3) + const patch1 = differ.diff(value1, value2) + const patch2 = differ.diff(value2, value3) + const patch3 = differ.diff(value3, value4) + const left = differ.combine(differ.combine(patch1, patch2), patch3) + const right = differ.combine(patch1, differ.combine(patch2, patch3)) + assert.isTrue(equal(differ.patch(left, value1), differ.patch(right, value1))) + })) + + it.effect("combining a patch with an empty patch is an identity", () => + Do(($) => { + const values = $(gen.runCollectN(2)) + const oldValue = values.unsafeGet(0) + const newValue = values.unsafeGet(1) + const patch = differ.diff(oldValue, newValue) + const left = differ.combine(patch, differ.empty) + const right = differ.combine(differ.empty, patch) + assert.isTrue(equal(differ.patch(left, oldValue), newValue)) + assert.isTrue(equal(differ.patch(right, oldValue), newValue)) + })) + + it.effect("diffing a value with itself returns an empty patch", () => + Do(($) => { + const values = $(gen.runCollectN(1)) + const value = values.unsafeGet(0) + assert.deepStrictEqual(differ.diff(value, value), differ.empty) + })) + + it.effect("diffing and then patching is an identity", () => + Do(($) => { + const values = $(gen.runCollectN(2)) + const oldValue = values.unsafeGet(0) + const newValue = values.unsafeGet(1) + const patch = differ.diff(oldValue, newValue) + assert.isTrue(equal(differ.patch(patch, oldValue), newValue)) + })) + + it.effect("patching with an empty patch is an identity", () => + Do(($) => { + const values = $(gen.runCollectN(1)) + const value = values.unsafeGet(0) + assert.isTrue(equal(differ.patch(differ.empty, value), value)) + })) + }) +} diff --git a/vitest.config.ts b/vitest.config.ts index ff03d24baa4..763045ff26c 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -3,8 +3,6 @@ import { defineConfig } from "vite" export default defineConfig({ test: { - include: [ - "packages/*/build/test/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}" - ] + include: ["packages/*/build/test/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"] } })