Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(breaking): Add ClassTag, improve Effect.serviceConstants, require string identifier for all tags #2028

Merged
merged 4 commits into from
Feb 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions .changeset/fair-guests-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
"@effect/platform-node-shared": minor
"@effect/platform-browser": minor
"@effect/opentelemetry": minor
"@effect/platform-node": minor
"@effect/experimental": minor
"@effect/platform-bun": minor
"@effect/rpc-workers": minor
"@effect/platform": minor
"@effect/rpc-http": minor
"effect": minor
"@effect/schema": minor
"@effect/cli": minor
"@effect/rpc": minor
---

With this change we now require a string key to be provided for all tags and renames the dear old `Tag` to `GenericTag`, so when previously you could do:

```ts
import { Effect, Context } from "effect";
interface Service {
readonly _: unique symbol;
}
const Service = Context.Tag<
Service,
{
number: Effect.Effect<never, never, number>;
}
>();
```

you are now mandated to do:

```ts
import { Effect, Context } from "effect";
interface Service {
readonly _: unique symbol;
}
const Service = Context.GenericTag<
Service,
{
number: Effect.Effect<never, never, number>;
}
>("Service");
```

This makes by default all tags globals and ensures better debuggaility when unexpected errors arise.

Furthermore we introduce a new way of constructing tags that should be considered the new default:

```ts
import { Effect, Context } from "effect";
class Service extends Context.Tag("Service")<
Service,
{
number: Effect.Effect<never, never, number>;
}
>() {}

const program = Effect.flatMap(Service, ({ number }) => number).pipe(
Effect.flatMap((_) => Effect.log(`number: ${_}`))
);
```

this will use "Service" as the key and will create automatically an opaque identifier (the class) to be used at the type level, it does something similar to the above in a single shot.
30 changes: 30 additions & 0 deletions .changeset/warm-keys-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
"@effect/platform-node-shared": minor
"@effect/platform-browser": minor
"@effect/opentelemetry": minor
"@effect/platform-node": minor
"@effect/experimental": minor
"@effect/platform-bun": minor
"@effect/rpc-workers": minor
"@effect/platform": minor
"@effect/rpc-http": minor
"effect": minor
"@effect/schema": minor
"@effect/cli": minor
"@effect/rpc": minor
---

This change enables `Effect.serviceConstants` and `Effect.serviceMembers` to access any constant in the service, not only the effects, namely it is now possible to do:

```ts
import { Effect, Context } from "effect";

class NumberRepo extends Context.TagClass("NumberRepo")<
NumberRepo,
{
readonly numbers: Array<number>;
}
>() {
static numbers = Effect.serviceConstants(NumberRepo).numbers;
}
```
2 changes: 1 addition & 1 deletion packages/cli/examples/naval-fate/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export interface NavalFateStore {
removeMine(x: number, y: number): Effect.Effect<never, never, void>
}

export const NavalFateStore = Context.Tag<NavalFateStore>()
export const NavalFateStore = Context.GenericTag<NavalFateStore>("NavalFateStore")

export const make = Effect.gen(function*($) {
const shipsStore = yield* $(Effect.map(
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/internal/cliConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const make = (params?: Partial<CliConfig.CliConfig>): CliConfig.CliConfig
})

/** @internal */
export const Tag = Context.Tag<CliConfig.CliConfig>()
export const Tag = Context.GenericTag<CliConfig.CliConfig>("@effect/cli/CliConfig")

/** @internal */
export const defaultConfig: CliConfig.CliConfig = {
Expand Down
10 changes: 6 additions & 4 deletions packages/cli/src/internal/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,14 +136,14 @@ const getDescriptor = <Name extends string, R, E, A>(self: Command.Command<Name,
const makeProto = <Name extends string, R, E, A>(
descriptor: Descriptor.Command<A>,
handler: (_: A) => Effect.Effect<R, E, void>,
tag?: Context.Tag<any, any>,
tag: Context.Tag<any, any>,
transform: Command.Command.Transform<R, E, A> = identity
): Command.Command<Name, R, E, A> => {
const self = Object.create(Prototype)
self.descriptor = descriptor
self.handler = handler
self.transform = transform
self.tag = tag ?? Context.Tag()
self.tag = tag
return self
}

Expand Down Expand Up @@ -190,7 +190,8 @@ export const fromDescriptor = dual<
const self: Command.Command<any, any, any, any> = makeProto(
descriptor,
handler ??
((_) => Effect.failSync(() => ValidationError.helpRequested(getDescriptor(self))))
((_) => Effect.failSync(() => ValidationError.helpRequested(getDescriptor(self)))),
Context.GenericTag(`@effect/cli/Command/(${Array.from(InternalDescriptor.getNames(descriptor)).join("|")})`)
)
return self as any
}
Expand Down Expand Up @@ -305,7 +306,8 @@ export const prompt = <Name extends string, A, R, E>(
InternalDescriptor.prompt(name, prompt),
(_) => _.value
),
handler
handler,
Context.GenericTag(`@effect/cli/Prompt/${name}`)
)

/** @internal */
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/test/Command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const clone = Command.make("clone", {
}
})).pipe(Command.withDescription("Clone a repository into a new directory"))

const AddService = Context.Tag<"AddService">()
const AddService = Context.GenericTag<"AddService">("AddService")

const add = Command.make("add", {
pathspec: Args.text({ name: "pathspec" })
Expand Down Expand Up @@ -138,7 +138,7 @@ interface Messages {
readonly log: (message: string) => Effect.Effect<never, never, void>
readonly messages: Effect.Effect<never, never, ReadonlyArray<string>>
}
const Messages = Context.Tag<Messages>()
const Messages = Context.GenericTag<Messages>("Messages")
const MessagesLive = Layer.sync(Messages, () => {
const messages: Array<string> = []
return Messages.of({
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/test/services/MockConsole.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export interface MockConsole extends Console.Console {
) => Effect.Effect<never, never, ReadonlyArray<string>>
}

export const MockConsole = Context.Tag<Console.Console, MockConsole>(
export const MockConsole = Context.GenericTag<Console.Console, MockConsole>(
"effect/Console"
)
const pattern = new RegExp(
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/test/services/MockTerminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export declare namespace MockTerminal {
// Context
// =============================================================================

export const MockTerminal = Context.Tag<Terminal.Terminal, MockTerminal>(
export const MockTerminal = Context.GenericTag<Terminal.Terminal, MockTerminal>(
"@effect/platform/Terminal"
)

Expand Down
82 changes: 52 additions & 30 deletions packages/effect/src/Context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,31 @@ export interface Tag<in out Identifier, in out Service> extends Pipeable, Inspec
of(self: Service): Service
context(self: Service): Context<Identifier>
readonly stack?: string | undefined
readonly identifier?: unknown
readonly key: string
[Unify.typeSymbol]?: unknown
[Unify.unifySymbol]?: TagUnify<this>
[Unify.ignoreSymbol]?: TagUnifyIgnore
}

/**
* @since 2.0.0
* @category models
*/
export interface TagClassShape<Id, Shape> {
readonly [TagTypeId]: {
readonly Id: Id
readonly Shape: Shape
}
}

/**
* @since 2.0.0
* @category models
*/
export interface TagClass<Self, Id, Shape> extends Tag<Self, Shape> {
new(_: never): TagClassShape<Id, Shape>
}

/**
* @category models
* @since 2.0.0
Expand All @@ -64,35 +83,32 @@ export declare namespace Tag {
/**
* @since 2.0.0
*/
export type Service<T extends Tag<any, any>> = T extends Tag<any, infer A> ? A : never
export type Service<T extends Tag<any, any> | TagClassShape<any, any>> = T extends Tag<any, infer A> ? A
: T extends TagClassShape<any, infer A> ? A
: never
/**
* @since 2.0.0
*/
export type Identifier<T extends Tag<any, any>> = T extends Tag<infer A, any> ? A : never
export type Identifier<T extends Tag<any, any> | TagClassShape<any, any>> = T extends Tag<infer A, any> ? A
: T extends TagClassShape<infer A, any> ? A
: never
}

/**
* Creates a new `Tag` instance with an optional key parameter.
*
* Specifying the `key` will make the `Tag` global, meaning two tags with the same
* key will map to the same instance.
*
* Note: this is useful for cases where live reload can happen and it is
* desireable to preserve the instance across reloads.
*
* @param key - An optional key that makes the `Tag` global.
* @param key - A key that will be used to compare tags.
*
* @example
* import * as Context from "effect/Context"
*
* assert.strictEqual(Context.Tag() === Context.Tag(), false)
* assert.strictEqual(Context.Tag("PORT") === Context.Tag("PORT"), true)
* assert.strictEqual(Context.GenericTag("PORT").key === Context.GenericTag("PORT").key, true)
*
* @since 2.0.0
* @category constructors
*/
export const Tag: <Identifier, Service = Identifier>(identifier?: unknown) => Tag<Identifier, Service> =
internal.makeTag
export const GenericTag: <Identifier, Service = Identifier>(key: string) => Tag<Identifier, Service> =
internal.makeGenericTag

const TypeId: unique symbol = internal.TypeId as TypeId

Expand All @@ -116,14 +132,14 @@ export interface Context<in Services> extends Equal, Pipeable, Inspectable {
readonly [TypeId]: {
readonly _Services: Types.Contravariant<Services>
}
readonly unsafeMap: Map<Tag<any, any>, any>
readonly unsafeMap: Map<string, any>
}

/**
* @since 2.0.0
* @category constructors
*/
export const unsafeMake: <Services>(unsafeMap: Map<Tag<any, any>, any>) => Context<Services> = internal.makeContext
export const unsafeMake: <Services>(unsafeMap: Map<string, any>) => Context<Services> = internal.makeContext

/**
* Checks if the provided argument is a `Context`.
Expand All @@ -148,7 +164,7 @@ export const isContext: (input: unknown) => input is Context<never> = internal.i
* @example
* import * as Context from "effect/Context"
*
* assert.strictEqual(Context.isTag(Context.Tag()), true)
* assert.strictEqual(Context.isTag(Context.GenericTag("Tag")), true)
*
* @since 2.0.0
* @category guards
Expand All @@ -174,7 +190,7 @@ export const empty: () => Context<never> = internal.empty
* @example
* import * as Context from "effect/Context"
*
* const Port = Context.Tag<{ PORT: number }>()
* const Port = Context.GenericTag<{ PORT: number }>("Port")
*
* const Services = Context.make(Port, { PORT: 8080 })
*
Expand All @@ -193,8 +209,8 @@ export const make: <T extends Tag<any, any>>(tag: T, service: Tag.Service<T>) =>
* import * as Context from "effect/Context"
* import { pipe } from "effect/Function"
*
* const Port = Context.Tag<{ PORT: number }>()
* const Timeout = Context.Tag<{ TIMEOUT: number }>()
* const Port = Context.GenericTag<{ PORT: number }>("Port")
* const Timeout = Context.GenericTag<{ TIMEOUT: number }>("Timeout")
*
* const someContext = Context.make(Port, { PORT: 8080 })
*
Expand Down Expand Up @@ -230,8 +246,8 @@ export const add: {
* import * as Context from "effect/Context"
* import { pipe } from "effect/Function"
*
* const Port = Context.Tag<{ PORT: number }>()
* const Timeout = Context.Tag<{ TIMEOUT: number }>()
* const Port = Context.GenericTag<{ PORT: number }>("Port")
* const Timeout = Context.GenericTag<{ TIMEOUT: number }>("Timeout")
*
* const Services = pipe(
* Context.make(Port, { PORT: 8080 }),
Expand Down Expand Up @@ -260,8 +276,8 @@ export const get: {
* @example
* import * as Context from "effect/Context"
*
* const Port = Context.Tag<{ PORT: number }>()
* const Timeout = Context.Tag<{ TIMEOUT: number }>()
* const Port = Context.GenericTag<{ PORT: number }>("Port")
* const Timeout = Context.GenericTag<{ TIMEOUT: number }>("Timeout")
*
* const Services = Context.make(Port, { PORT: 8080 })
*
Expand All @@ -287,8 +303,8 @@ export const unsafeGet: {
* import * as Context from "effect/Context"
* import * as O from "effect/Option"
*
* const Port = Context.Tag<{ PORT: number }>()
* const Timeout = Context.Tag<{ TIMEOUT: number }>()
* const Port = Context.GenericTag<{ PORT: number }>("Port")
* const Timeout = Context.GenericTag<{ TIMEOUT: number }>("Timeout")
*
* const Services = Context.make(Port, { PORT: 8080 })
*
Expand All @@ -312,8 +328,8 @@ export const getOption: {
* @example
* import * as Context from "effect/Context"
*
* const Port = Context.Tag<{ PORT: number }>()
* const Timeout = Context.Tag<{ TIMEOUT: number }>()
* const Port = Context.GenericTag<{ PORT: number }>("Port")
* const Timeout = Context.GenericTag<{ TIMEOUT: number }>("Timeout")
*
* const firstContext = Context.make(Port, { PORT: 8080 })
* const secondContext = Context.make(Timeout, { TIMEOUT: 5000 })
Expand Down Expand Up @@ -341,8 +357,8 @@ export const merge: {
* import { pipe } from "effect/Function"
* import * as O from "effect/Option"
*
* const Port = Context.Tag<{ PORT: number }>()
* const Timeout = Context.Tag<{ TIMEOUT: number }>()
* const Port = Context.GenericTag<{ PORT: number }>("Port")
* const Timeout = Context.GenericTag<{ TIMEOUT: number }>("Timeout")
*
* const someContext = pipe(
* Context.make(Port, { PORT: 8080 }),
Expand All @@ -367,3 +383,9 @@ export const omit: <Services, S extends Array<ValidTagsById<Services>>>(
...tags: S
) => (self: Context<Services>) => Context<Exclude<Services, { [k in keyof S]: Tag.Identifier<S[k]> }[keyof S]>> =
internal.omit

/**
* @since 2.0.0
* @category constructors
*/
export const Tag: <const Id extends string>(id: Id) => <Self, Shape>() => TagClass<Self, Id, Shape> = internal.Tag
Loading
Loading