Skip to content

Commit

Permalink
Allow Effect.Tag to take a constructor, enables direct inference and …
Browse files Browse the repository at this point in the history
…layer construction.
  • Loading branch information
mikearnaldi committed Sep 26, 2024
1 parent ce8b810 commit a895f76
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 29 deletions.
67 changes: 67 additions & 0 deletions .changeset/sour-singers-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
"effect": patch
---

Allow Effect.Tag to take a constructor, enables direct inference and layer construction.

Namely the following is now possible:

```ts
import { Effect } from "effect"

class Logger extends Effect.Tag("Logger", {
sync: () => ({
info: (message: string) => Effect.logInfo(message)
})
})<Logger>() {}
```

Together with:

```ts
import { Effect } from "effect"

class Logger extends Effect.Tag("Logger", {
effect: Effect.sync(() => ({
info: (message: string) => Effect.logInfo(message)
}))
})<Logger>() {}
```

And:

```ts
import { Effect } from "effect"

class Logger extends Effect.Tag("Logger", {
scoped: Effect.gen(function* () {
yield* Effect.addFinalizer(() => Effect.logInfo("Done"))
return {
info: (message: string) => Effect.logInfo(message)
}
})
})<Logger>() {}
```

All the above will lead to automatic layer creation named: `Logger.Live`.

The created layer is also exported with the name `Logger.Layer` to make dependency providing more ergonomic, for example:

```ts
import { Effect, Layer } from "effect"

class Dummy extends Effect.Tag("Dummy", {
sync: () => ({})
})<Dummy>() {}

class Logger extends Effect.Tag("Logger", {
effect: Effect.gen(function* () {
yield* Dummy
return {
info: (message: string) => Effect.logInfo(message)
}
})
})<Logger>() {
static Live = this.Layer.pipe(Layer.provide(Dummy.Live))
}
```
125 changes: 102 additions & 23 deletions packages/effect/src/Effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6200,35 +6200,93 @@ export declare namespace Tag {
* @since 2.0.0
* @category models
*/
export type AllowedType = (Record<PropertyKey, any> & ProhibitedType) | string | number | symbol
export interface ProhibitedTypeLive extends ProhibitedType {
Live?: `property "Live" is forbidden`
Layer?: `property "Layer" is forbidden`
}

/**
* @since 2.0.0
* @category models
*/
export type AllowedType<Prohibited = ProhibitedType> =
| (Record<PropertyKey, any> & Prohibited)
| string
| number
| symbol

/**
* @since 2.0.0
* @category models
*/
export type Return<Self, Id extends string, Type extends Tag.AllowedType> =
& Context.TagClass<Self, Id, Type>
& (Type extends Record<PropertyKey, any> ? {
[
k in keyof Type as Type[k] extends ((...args: [...infer Args]) => infer Ret)
? ((...args: Readonly<Args>) => Ret) extends Type[k] ? k : never
: k
]: Type[k] extends (...args: [...infer Args]) => Effect<infer A, infer E, infer R>
? (...args: Readonly<Args>) => Effect<A, E, Self | R>
: Type[k] extends (...args: [...infer Args]) => infer A ? (...args: Readonly<Args>) => Effect<A, never, Self>
: Type[k] extends Effect<infer A, infer E, infer R> ? Effect<A, E, Self | R>
: Effect<Type[k], never, Self>
} :
{})
& {
use: <X>(
body: (_: Type) => X
) => X extends Effect<infer A, infer E, infer R> ? Effect<A, E, R | Self> : Effect<X, never, Self>
}

/**
* @since 2.0.0
* @category models
*/
export type Maker = {
effect: Effect<AllowedType<ProhibitedTypeLive>, any, any>
} | {
scoped: Effect<AllowedType<ProhibitedTypeLive>, any, any>
} | {
sync: () => AllowedType<ProhibitedTypeLive>
}

/**
* @since 2.0.0
* @category models
*/
export type ReturnWithMaker<Self, Id extends string, Maker extends Tag.Maker> =
& {}
& Maker extends { scoped: Effect<infer Type extends AllowedType<ProhibitedTypeLive>, infer E, infer R> }
? Tag.Return<Self, Id, Type> & {
readonly Layer: Layer.Layer<Self, E, Exclude<R, Scope.Scope>>
readonly Live: Layer.Layer<Self, E, Exclude<R, Scope.Scope>>
}
: Maker extends { effect: Effect<infer Type extends AllowedType<ProhibitedTypeLive>, infer E, infer R> }
? Tag.Return<Self, Id, Type> & {
readonly Layer: Layer.Layer<Self, E, R>
readonly Live: Layer.Layer<Self, E, R>
}
: Maker extends { sync: () => infer Type extends AllowedType<ProhibitedTypeLive> } ? Tag.Return<Self, Id, Type> & {
readonly Layer: Layer.Layer<Self, never, never>
readonly Live: Layer.Layer<Self, never, never>
}
: never
}

/**
* @since 2.0.0
* @category constructors
*/
export const Tag: <const Id extends string>(id: Id) => <
Self,
Type extends Tag.AllowedType
>() =>
& Context.TagClass<Self, Id, Type>
& (Type extends Record<PropertyKey, any> ? {
[
k in keyof Type as Type[k] extends ((...args: [...infer Args]) => infer Ret) ?
((...args: Readonly<Args>) => Ret) extends Type[k] ? k : never
: k
]: Type[k] extends (...args: [...infer Args]) => Effect<infer A, infer E, infer R> ?
(...args: Readonly<Args>) => Effect<A, E, Self | R>
: Type[k] extends (...args: [...infer Args]) => infer A ? (...args: Readonly<Args>) => Effect<A, never, Self>
: Type[k] extends Effect<infer A, infer E, infer R> ? Effect<A, E, Self | R>
: Effect<Type[k], never, Self>
} :
{})
& {
use: <X>(
body: (_: Type) => X
) => X extends Effect<infer A, infer E, infer R> ? Effect<A, E, R | Self> : Effect<X, never, Self>
} = (id) => () => {
export const Tag: {
<const Id extends string>(id: Id): <Self, Type extends Tag.AllowedType>() => Tag.Return<Self, Id, Type>
<const Id extends string, Maker extends Tag.Maker>(
id: Id,
maker: Maker
): <Self>() => Tag.ReturnWithMaker<Self, Id, Maker>
} = function() {
const [id, maker] = arguments
return () => {
const limit = Error.stackTraceLimit
Error.stackTraceLimit = 2
const creationError = new Error()
Expand All @@ -6241,6 +6299,26 @@ export const Tag: <const Id extends string>(id: Id) => <
return creationError.stack
}
})
if (maker !== undefined) {
if ("effect" in maker) {
// @ts-expect-error
TagClass["Live"] = layer.fromEffect(TagClass, maker["effect"])
// @ts-expect-error
TagClass["Layer"] = layer.fromEffect(TagClass, maker["effect"])
}
if ("scoped" in maker) {
// @ts-expect-error
TagClass["Live"] = layer.scoped(TagClass, maker["scoped"])
// @ts-expect-error
TagClass["Layer"] = layer.scoped(TagClass, maker["scoped"])
}
if ("sync" in maker) {
// @ts-expect-error
TagClass["Live"] = layer.sync(TagClass, maker["sync"])
// @ts-expect-error
TagClass["Layer"] = layer.sync(TagClass, maker["sync"])
}
}
const cache = new Map()
const done = new Proxy(TagClass, {
get(_target: any, prop: any, _receiver) {
Expand Down Expand Up @@ -6277,3 +6355,4 @@ export const Tag: <const Id extends string>(id: Id) => <
})
return done
}
}
28 changes: 22 additions & 6 deletions packages/effect/test/Effect/environment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,26 @@ class DateTag extends Effect.Tag("DateTag")<DateTag, Date>() {
static Live = Layer.succeed(this, this.date)
}

class MapTag extends Effect.Tag("MapTag")<MapTag, Map<string, string>>() {
static Live = Layer.effect(this, Effect.sync(() => new Map()))
}
class MapTag extends Effect.Tag("MapTag", {
effect: Effect.sync(() => new Map<string, string>())
})<MapTag>() {}

class NumberTag extends Effect.Tag("NumberTag", {
sync: () => 100
})<NumberTag>() {}

class NumberTag extends Effect.Tag("NumberTag")<NumberTag, number>() {
static Live = Layer.succeed(this, 100)
class Dummy extends Effect.Tag("Dummy", {
sync: () => ({})
})<Dummy>() {}

class ScopedTag extends Effect.Tag("ScopedTag", {
scoped: Effect.gen(function*() {
yield* Effect.acquireRelease(Effect.sync(() => 100), () => Effect.void)
yield* Dummy
return 100
})
})<ScopedTag>() {
static Live = this.Layer.pipe(Layer.provide(Dummy.Live))
}

describe("Effect", () => {
Expand Down Expand Up @@ -96,6 +110,7 @@ describe("Effect", () => {
Effect.gen(function*($) {
expect(yield* $(DateTag.getTime())).toEqual(DateTag.date.getTime())
expect(yield* $(NumberTag)).toEqual(100)
expect(yield* $(ScopedTag)).toEqual(100)
expect(Array.from(yield* $(MapTag.keys()))).toEqual([])
yield* $(MapTag.set("foo", "bar"))
expect(Array.from(yield* $(MapTag.keys()))).toEqual(["foo"])
Expand All @@ -104,7 +119,8 @@ describe("Effect", () => {
Effect.provide(Layer.mergeAll(
DateTag.Live,
NumberTag.Live,
MapTag.Live
MapTag.Live,
ScopedTag.Live
))
))
it.effect("class tag", () =>
Expand Down

0 comments on commit a895f76

Please sign in to comment.