Skip to content

Commit

Permalink
support $is & $match for Data.TaggedEnum with generics (#2702)
Browse files Browse the repository at this point in the history
  • Loading branch information
tim-smart committed May 8, 2024
1 parent 94eaf4b commit 2e08da3
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 50 deletions.
5 changes: 5 additions & 0 deletions .changeset/quiet-ladybugs-fly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"effect": minor
---

support $is & $match for Data.TaggedEnum with generics
169 changes: 121 additions & 48 deletions packages/effect/src/Data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as internal from "./internal/data.js"
import { StructuralPrototype } from "./internal/effectable.js"
import * as Predicate from "./Predicate.js"
import type * as Types from "./Types.js"
import type { Unify } from "./Unify.js"

/**
* @since 2.0.0
Expand Down Expand Up @@ -336,13 +337,63 @@ export declare namespace TaggedEnum {
}
& {
readonly $is: <Tag extends A["_tag"]>(tag: Tag) => (u: unknown) => u is Extract<A, { readonly _tag: Tag }>
readonly $match: <
readonly $match: {
<
Cases extends {
readonly [Tag in A["_tag"]]: (args: Extract<A, { readonly _tag: Tag }>) => any
}
>(cases: Cases): (value: A) => Unify<ReturnType<Cases[A["_tag"]]>>
<
Cases extends {
readonly [Tag in A["_tag"]]: (args: Extract<A, { readonly _tag: Tag }>) => any
}
>(value: A, cases: Cases): Unify<ReturnType<Cases[A["_tag"]]>>
}
}
>

/**
* @since 3.2.0
*/
export interface GenericMatchers<Z extends WithGenerics<number>> {
readonly $is: <Tag extends Z["taggedEnum"]["_tag"]>(
tag: Tag
) => {
<T extends TaggedEnum.Kind<Z, any, any, any, any>>(
u: T
): u is T & { readonly _tag: Tag }
(u: unknown): u is Extract<TaggedEnum.Kind<Z>, { readonly _tag: Tag }>
}
readonly $match: {
<
A,
B,
C,
D,
Cases extends {
readonly [Tag in Z["taggedEnum"]["_tag"]]: (
args: Extract<TaggedEnum.Kind<Z, A, B, C, D>, { readonly _tag: Tag }>
) => any
}
>(
cases: Cases
): (self: TaggedEnum.Kind<Z, A, B, C, D>) => Unify<ReturnType<Cases[Z["taggedEnum"]["_tag"]]>>
<
A,
B,
C,
D,
Cases extends {
readonly [Tag in A["_tag"]]: (args: Extract<A, { readonly _tag: Tag }>) => any
readonly [Tag in Z["taggedEnum"]["_tag"]]: (
args: Extract<TaggedEnum.Kind<Z, A, B, C, D>, { readonly _tag: Tag }>
) => any
}
>(cases: Cases) => (value: A) => ReturnType<Cases[A["_tag"]]>
>(
self: TaggedEnum.Kind<Z, A, B, C, D>,
cases: Cases
): Unify<ReturnType<Cases[Z["taggedEnum"]["_tag"]]>>
}
>
}
}

/**
Expand Down Expand Up @@ -379,72 +430,94 @@ export declare namespace TaggedEnum {
* @since 2.0.0
*/
export const taggedEnum: {
<Z extends TaggedEnum.WithGenerics<1>>(): {
readonly [Tag in Z["taggedEnum"]["_tag"]]: <A>(
args: TaggedEnum.Args<
TaggedEnum.Kind<Z, A>,
Tag,
Extract<TaggedEnum.Kind<Z, A>, { readonly _tag: Tag }>
>
) => TaggedEnum.Value<TaggedEnum.Kind<Z, A>, Tag>
}
<Z extends TaggedEnum.WithGenerics<1>>(): Types.Simplify<
{
readonly [Tag in Z["taggedEnum"]["_tag"]]: <A>(
args: TaggedEnum.Args<
TaggedEnum.Kind<Z, A>,
Tag,
Extract<TaggedEnum.Kind<Z, A>, { readonly _tag: Tag }>
>
) => TaggedEnum.Value<TaggedEnum.Kind<Z, A>, Tag>
} & TaggedEnum.GenericMatchers<Z>
>

<Z extends TaggedEnum.WithGenerics<2>>(): {
readonly [Tag in Z["taggedEnum"]["_tag"]]: <A, B>(
args: TaggedEnum.Args<
TaggedEnum.Kind<Z, A, B>,
Tag,
Extract<TaggedEnum.Kind<Z, A, B>, { readonly _tag: Tag }>
>
) => TaggedEnum.Value<TaggedEnum.Kind<Z, A, B>, Tag>
}
<Z extends TaggedEnum.WithGenerics<2>>(): Types.Simplify<
{
readonly [Tag in Z["taggedEnum"]["_tag"]]: <A, B>(
args: TaggedEnum.Args<
TaggedEnum.Kind<Z, A, B>,
Tag,
Extract<TaggedEnum.Kind<Z, A, B>, { readonly _tag: Tag }>
>
) => TaggedEnum.Value<TaggedEnum.Kind<Z, A, B>, Tag>
} & TaggedEnum.GenericMatchers<Z>
>

<Z extends TaggedEnum.WithGenerics<3>>(): {
readonly [Tag in Z["taggedEnum"]["_tag"]]: <A, B, C>(
args: TaggedEnum.Args<
TaggedEnum.Kind<Z, A, B, C>,
Tag,
Extract<TaggedEnum.Kind<Z, A, B, C>, { readonly _tag: Tag }>
>
) => TaggedEnum.Value<TaggedEnum.Kind<Z, A, B, C>, Tag>
}
<Z extends TaggedEnum.WithGenerics<3>>(): Types.Simplify<
{
readonly [Tag in Z["taggedEnum"]["_tag"]]: <A, B, C>(
args: TaggedEnum.Args<
TaggedEnum.Kind<Z, A, B, C>,
Tag,
Extract<TaggedEnum.Kind<Z, A, B, C>, { readonly _tag: Tag }>
>
) => TaggedEnum.Value<TaggedEnum.Kind<Z, A, B, C>, Tag>
} & TaggedEnum.GenericMatchers<Z>
>

<Z extends TaggedEnum.WithGenerics<4>>(): {
readonly [Tag in Z["taggedEnum"]["_tag"]]: <A, B, C, D>(
args: TaggedEnum.Args<
TaggedEnum.Kind<Z, A, B, C, D>,
Tag,
Extract<TaggedEnum.Kind<Z, A, B, C, D>, { readonly _tag: Tag }>
>
) => TaggedEnum.Value<TaggedEnum.Kind<Z, A, B, C, D>, Tag>
}
<Z extends TaggedEnum.WithGenerics<4>>(): Types.Simplify<
{
readonly [Tag in Z["taggedEnum"]["_tag"]]: <A, B, C, D>(
args: TaggedEnum.Args<
TaggedEnum.Kind<Z, A, B, C, D>,
Tag,
Extract<TaggedEnum.Kind<Z, A, B, C, D>, { readonly _tag: Tag }>
>
) => TaggedEnum.Value<TaggedEnum.Kind<Z, A, B, C, D>, Tag>
} & TaggedEnum.GenericMatchers<Z>
>

<A extends { readonly _tag: string }>(): TaggedEnum.Constructor<A>
} = () =>
new Proxy({}, {
get(_target, tag, _receiver) {
if (tag === "$is") {
return taggedIs
return Predicate.isTagged
} else if (tag === "$match") {
return taggedMatch
}
return tagged(tag as string)
}
}) as any

function taggedIs<A extends { readonly _tag: string }, Tag extends A["_tag"]>(tag: Tag) {
return Predicate.isTagged(tag)
}

function taggedMatch<
A extends { readonly _tag: string },
Cases extends {
readonly [K in A["_tag"]]: (args: Extract<A, { readonly _tag: K }>) => any
}
>(cases: Cases) {
return function(value: A): ReturnType<Cases[A["_tag"]]> {
return cases[value._tag as A["_tag"]](value as any)
>(self: A, cases: Cases): ReturnType<Cases[A["_tag"]]>
function taggedMatch<
A extends { readonly _tag: string },
Cases extends {
readonly [K in A["_tag"]]: (args: Extract<A, { readonly _tag: K }>) => any
}
>(cases: Cases): (value: A) => ReturnType<Cases[A["_tag"]]>
function taggedMatch<
A extends { readonly _tag: string },
Cases extends {
readonly [K in A["_tag"]]: (args: Extract<A, { readonly _tag: K }>) => any
}
>(): any {
if (arguments.length === 1) {
const cases = arguments[0] as Cases
return function(value: A): ReturnType<Cases[A["_tag"]]> {
return cases[value._tag as A["_tag"]](value as any)
}
}
const value = arguments[0] as A
const cases = arguments[1] as Cases
return cases[value._tag as A["_tag"]](value as any)
}

/**
Expand Down
33 changes: 31 additions & 2 deletions packages/effect/test/Data.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as Data from "effect/Data"
import * as Equal from "effect/Equal"
import { describe, expect, it } from "vitest"
import { pipe } from "effect/Function"
import { assert, describe, expect, it } from "vitest"

describe("Data", () => {
it("struct", () => {
Expand Down Expand Up @@ -218,7 +219,7 @@ describe("Data", () => {
interface ResultDefinition extends Data.TaggedEnum.WithGenerics<2> {
readonly taggedEnum: Result<this["A"], this["B"]>
}
const { Failure, Success } = Data.taggedEnum<ResultDefinition>()
const { $is, $match, Failure, Success } = Data.taggedEnum<ResultDefinition>()

const a = Success({ value: 1 }) satisfies Result<unknown, number>
const b = Failure({ error: "test" }) satisfies Result<string, unknown>
Expand All @@ -233,6 +234,34 @@ describe("Data", () => {

expect(Equal.equals(a, b)).toBe(false)
expect(Equal.equals(a, c)).toBe(true)

const aResult = Success({ value: 1 }) as Result<unknown, number>
const bResult = Failure({ error: "boom" }) as Result<string, number>

assert.strictEqual(
$match(aResult, {
Success: (_) => 1,
Failure: (_) => 2
}),
1
)
const result = pipe(
bResult,
$match({
Success: (_) => _.value,
Failure: (_) => _.error
})
)
result satisfies string | number
assert.strictEqual(result, "boom")

assert($is("Success")(aResult))
aResult satisfies { readonly _tag: "Success"; readonly value: number }
assert.strictEqual(aResult.value, 1)

assert($is("Failure")(bResult))
bResult satisfies { readonly _tag: "Failure"; readonly error: string }
assert.strictEqual(bResult.error, "boom")
})

describe("Error", () => {
Expand Down

0 comments on commit 2e08da3

Please sign in to comment.