Skip to content

Commit

Permalink
feat(breaking): Add ClassTag, improve Effect.serviceConstants, requir…
Browse files Browse the repository at this point in the history
…e string identifier for all tags (#2028)

Co-authored-by: Tim <hello@timsmart.co>
  • Loading branch information
mikearnaldi and tim-smart committed Feb 9, 2024
1 parent 02c3461 commit 9a2d1c1
Show file tree
Hide file tree
Showing 92 changed files with 457 additions and 357 deletions.
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 @@ -302,7 +303,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

0 comments on commit 9a2d1c1

Please sign in to comment.