| title | Workshop | Effect Days 2025 | ||||||
|---|---|---|---|---|---|---|---|
| favicon | /favicon.png | ||||||
| twoslash | true | ||||||
| fonts |
|
||||||
| defaults |
|
||||||
| colorSchema | dark | ||||||
| lineNumbers | true | ||||||
| theme | ./theme |
https://github.com/Effect-TS/effect-days-2025-workshop.git
| Speaker | Time Slot | Duration | |
|---|---|---|---|
| Max | Session 1 | 9:00 AM – 10:30 AM | 1.5 hours |
| Break | 10:30 AM – 10:45 AM | 15 minutes | |
| Session 2 | 10:45 AM – 12:15 PM | 1.5 hours | |
| Lunch | 12:15 PM – 1:15 PM | 1 hour | |
| Tim | Session 3 | 1:15 PM – 2:45 PM | 1.5 hours |
| Break | 2:45 PM – 3:00 PM | 15 minutes | |
| Session 4 | 3:00 PM – 4:30 PM | 1.5 hours | |
| Q & A | 4:30 PM – 5:00 PM | 30 minutes |
- Understand the concept of a "service" in Effect
- Gain experience with building and composing services
- Explore the motivation behind the
Layertype - Learn best practices for structuring an application
Code to an interface, not an implementation
- Abstracts Functionality
- Useful for Prototyping
- Facilitates Easier Testing
- Composition & Modularity
| Term | Definition |
|---|---|
Service |
An interface that exposes a particular set of functionality |
Tag |
A unique type-level & runtime identifier for a service |
Context |
A container which holds a map of Tag -> Service |
<<< @/src/examples/section-1/001_defining-a-service-01.ts ts
<<< @/src/examples/section-1/001_defining-a-service-02.ts ts
src/exercises/section-1/001_creating-a-service.ts
<template #1>
A behavioral management system that weaponizes puns as a disciplinary tool
Matches severity of child misbehavior with a proportionally painful pun
- Simple pun for minor offenses
- Elaborate, multi-punchline groaners for serious infractions
<template #2>
Key components include:
- Pun Distribution Network (PDN)
- Immunity Token Manager
- PUNSTER
Pun Utility for Neurological Sentiment Testing & Emotional Response
<template #3>
graph TD
A["PunsterClient"] -- depends on --> B["ImmunityTokenManager"]
C["PunDistributionNetwork"] -- depends on --> A
src/exercises/section-1/001_creating-a-service.ts
Create the service definitions for the following:
PunDistributionNetworkPunsterClientImmunityTokenManager
src/exercises/section-1/001_creating-a-service-solution.ts
- We imported the
Contextmodule from"effect" - We used
Context.Tagto create unique service identifiers
ServiceTag.pipe(
Effect.andThen((serviceImpl) => ...)
)
// or
Effect.gen(function*() {
const serviceImpl = yield* ServiceTag
...
})<<< @/src/examples/section-1/002_using-a-service.ts ts {|7-9|14-15|16-18|11-13}
src/exercises/section-1/002_using-a-service.ts
Define the main Effect:
- Access the requisite services in the program
- Use the service interfaces to implement the business logic
src/exercises/section-1/002_using-a-service-solution.ts
- We accessed services via their
Tags - We used the service interfaces to implement our business logic
- We observed that we have not implemented our services yet
- We observed that services are tracked in the
Requirementstype
effect.pipe(
// Associate a concrete implementation with its Tag
Effect.provideService(ServiceTag, serviceImpl)
)
// or
effect.pipe(
// Associate an Effect that produces a concrete implementation
// with its Tag
Effect.provideServiceEffect(ServiceTag, Effect<serviceImpl, ...>)
)<<< @/src/examples/section-1/003_providing-a-service.ts ts {1-17|19-}{maxHeight:'400px'}
<style> /* Hide the scrollbar */ .slidev-code-wrapper { -ms-overflow-style: none; scrollbar-width: none; } .slidev-code-wrapper::-webkit-scrollbar { display: none; /* Safari and Chrome */ } /* Remove margin from code block */ .slidev-code.shiki { margin: 0; } </style>src/demos/section-1/001_providing-a-service-00.ts
src/demos/section-1/001_providing-a-service-01.ts
src/exercises/section-1/003_providing-a-service.ts
Create a test implementation of the PunsterClient:
- Should always return the same
PunfromcreatePun - Should always return the same evaluation from
evaluatePun - Provide the test implementation to the
mainprogram
src/exercises/section-1/003_providing-a-service-solution.ts
- We created a test implementation of our
PunsterClient - We provided the implementation to
main - We observed that swapping implementations is trivial
This is not testable!
import { Context, Data, Effect } from "effect"
import * as fs from "node:fs/promises"
// ... <snip> ...
const FileSystemCache = Cache.of({
lookup: (key) =>
Effect.tryPromise({
try: () => fs.readFile(`src/demos/section-1/cache/${key}`, "utf-8"),
catch: () =>
new CacheMissError({
message: `Failed to read file for cache key: "${key}"`
})
})
})- Services can have dependencies on other services
- Naturally results in a directed acyclic graph of services
graph LR
A[UserService] -- depends on --> B[DatabaseService]
A -- depends on --> C[LoggerService]
B -- depends on --> D[ConfigService]
graph TD
A["PunsterClient"] -- depends on --> B["ImmunityTokenManager"]
C["PunDistributionNetwork"] -- depends on --> A
graph LR
A[Cache] -- depends on --> B[FileSystem]
A cache that depends on a file system
<<< @/src/examples/section-1/004_avoid-leaking-requirements-01.ts ts {|4-10|}
<<< @/src/examples/section-1/004_avoid-leaking-requirements-02.ts ts {|17-18}
<<< @/src/examples/section-1/004_avoid-leaking-requirements-03.ts ts {17-18}
<<< @/src/examples/section-1/005_providing-dependent-services.ts ts {1-18|20-29|31-47|49-56|58-}{maxHeight:'400px'}
<style> /* Hide the scrollbar */ .slidev-code-wrapper { -ms-overflow-style: none; scrollbar-width: none; } .slidev-code-wrapper::-webkit-scrollbar { display: none; /* Safari and Chrome */ } /* Remove margin from code block */ .slidev-code.shiki { margin: 0; } </style>As the number of services grow, thair relational complexity grows
How do we deal with...
- Service composition?
- Singleton services?
- Resource safety?
flowchart LR
subgraph DocRepo[ ]
direction TB
A[DocRepo] --> B[Logging]
A[DocRepo] --> C[Database]
A[DocRepo] --> D[BlobStorage] --> E[Logging]
end
subgraph UserRepo[ ]
direction TB
F[UserRepo] --> G[Logging]
F[UserRepo] --> H[Database]
end
subgraph Dependency Graph
direction LR
DocRepo<--->UserRepo
end
- A service may have one or more dependencies
- A service must have dependencies provided in the correct order
- A service might be resourceful
A data type which represents a constructor for one or more services
- May depend on other services
- May fail to construct a service, producing some error value
- May manage the acquisition / release of resources
- Easily composable with other
Layers - Are memoized during resolution of the dependency graph
┌─── The service(s) to be created
│ ┌─── The possible error
│ │ ┌─── The required dependencies
▼ ▼ ▼
Layer<RequirementsOut, Error, RequirementsIn>
Technically...
graph LR
A[Layer<Services>] -- build --> B[Context<Services>]
// Static layers
Layer.succeed(
ServiceTag,
// Define a concrete service implementation
ServiceShape
) // -> Layer<ServiceTag, never, never>
// Synchronous layers
Layer.sync(
ServiceTag,
// A thunk that produces the concrete service implementation
() => ServiceShape
) // -> Layer<ServiceTag, never, never>
// Asynchronous layers
Layer.effect(
ServiceTag,
// An Effect that produces the concrete service implementation
Effect<ServiceShape, ...>
) // -> Layer<ServiceTag, never, never>
// Resourceful layers
Layer.scoped(
ServiceTag,
// A scoped Effect that resourcefully produces the concrete
// service implementation
Effect<ServiceShape, ..., Scope>
) // -> Layer<ServiceTag, never, Scope><<< @/src/examples/section-1/006_creating-a-layer.ts ts {1-18|20-26|28-44|46-51|53-61|63-68|70-74}{maxHeight:'400px'}
<style> /* Hide the scrollbar */ .slidev-code-wrapper { -ms-overflow-style: none; scrollbar-width: none; } .slidev-code-wrapper::-webkit-scrollbar { display: none; /* Safari and Chrome */ } /* Remove margin from code block */ .slidev-code.shiki { margin: 0; } </style>src/exercises/section-1/004_creating-a-layer.ts
Define Layers for each of our services:
PunDistributionNetworkLayerPunsterClientLayerImmunityTokenManagerLayer
src/exercises/section-1/004_creating-a-layer-solution.ts
- We imported
Layerfrom the"effect"module - We used
Layer.effectfor non-resourceful services - We used
Layercombinators to composeLayers together - We have a lingering
Scopein our requirements
import { Effect } from "effect"
// ...<snip>...
class Cache extends Effect.Service<Cache>()("app/Cache", {
effect: Effect.gen(function*() {
const fs = yield* FileSystem
function lookup(key: string): Effect.Effect<string, CacheMissError> {
return fs.readFileString(`./src/demos/section-1/cache/${key}`).pipe(
Effect.mapError(() => {
return new CacheMissError({ message: `failed to read file for cache key: "${key}"` })
})
)
}
return { lookup } as const
}),
// Provide service dependencies (optional)
dependencies: [FileSystemLayer]
}) {}
// This layer is automatically generated by `Effect.Service` and
// will build the `Cache` service
//
// ┌─── Layer<Cache, never, never>
// ▼
Cache.Default
// This layer is automatically generated by `Effect.Service` and
// will build the `Cache` service without any dependencies provided
// (only generated if you specify `dependencies`)
//
// ┌─── Layer<Cache, never, FileSystem>
// ▼
Cache.DefaultWithoutDependencies<<< @/src/examples/section-1/007_simplifying-service-definitions.ts ts {1-16|18-20|22-39|41-43|45-51|53-}{maxHeight:'400px'}
<style> /* Hide the scrollbar */ .slidev-code-wrapper { -ms-overflow-style: none; scrollbar-width: none; } .slidev-code-wrapper::-webkit-scrollbar { display: none; /* Safari and Chrome */ } /* Remove margin from code block */ .slidev-code.shiki { margin: 0; } </style>src/exercises/section-1/005_simplifying-service-definitions.ts
Re-define our services using Effect.Service:
PunDistributionNetworkPunsterClientImmunityTokenManager
src/exercises/section-1/005_simplifying-service-definitions-solution.ts
- We used
Effect.Serviceto define both aTagandLayer - We used the
dependenciesto locally provide dependencies - We still have that lingering scope in the requirements
flowchart LR
R1["Register Finalizer 1"]
R2["Register Finalizer 2"]
R3["Register Finalizer 3"]
subgraph Scope
F1["Finalizer 1"]
F2["Finalizer 2"]
F3["Finalizer 3"]
end
subgraph Close[ ]
direction TB
C1["Running Finalizer 3"]
C2["Running Finalizer 2"]
C3["Running Finalizer 1"]
end
R1 --> F1
R2 --> F2
R3 --> F3
F3 -- Scope.close --> C1
classDef default fill:#1a1a1a,stroke:#ffffff,color:#ffffff,stroke-width:2px
classDef container fill:#2d2d2d,stroke:#ffffff,color:#ffffff,stroke-width:2px
class R1,R2,R3,F1,F2,F3 default
class Scope container
<<< @/src/examples/section-1/008_closing-a-scope.ts ts {|3-13|15-26|27-}{maxHeight:'400px'}
<style> /* Hide the scrollbar */ .slidev-code-wrapper { -ms-overflow-style: none; scrollbar-width: none; } .slidev-code-wrapper::-webkit-scrollbar { display: none; /* Safari and Chrome */ } /* Remove margin from code block */ .slidev-code.shiki { margin: 0; } </style><<< @/src/examples/section-1/009_effect-scoped.ts ts {|4-5|7-23|10-13|14-22|25|27|29}{maxHeight:'400px'}
<style> /* Hide the scrollbar */ .slidev-code-wrapper { -ms-overflow-style: none; scrollbar-width: none; } .slidev-code-wrapper::-webkit-scrollbar { display: none; /* Safari and Chrome */ } /* Remove margin from code block */ .slidev-code.shiki { margin: 0; } </style>flowchart TB
A["Layer"] --> B["Access Scope / MemoMap"]
B --> C["Traverse Layer Graph"]
C --> D["Check MemoMap"]
D -- "Service Found" --> G["Continue"]
D -- "Service Not Found" --> E["Construct Service"]
E -- "Requires Scope" --> F["Use Scope"]
F --> H["Add Service to MemoMap"]
E -- "No Scope Required" --> H
H --> G
G -- "More Dependencies" --> D
G -- "All Dependencies Constructed" --> I["Context"]
<<< @/src/examples/section-1/010_providing-a-layer.ts ts {|3-7|9-13|16-23|25-30|32-37|39-44|46-51|53-54|56-57}{maxHeight:'400px'}
<style> /* Hide the scrollbar */ .slidev-code-wrapper { -ms-overflow-style: none; scrollbar-width: none; } .slidev-code-wrapper::-webkit-scrollbar { display: none; /* Safari and Chrome */ } /* Remove margin from code block */ .slidev-code.shiki { margin: 0; } </style><<< @/src/examples/section-1/011_resourceful-layers.ts ts {|9-26|11-16|17-20|33-50|37,40,47|34|52-60|62-65|63}{maxHeight:'400px'}
<style> /* Hide the scrollbar */ .slidev-code-wrapper { -ms-overflow-style: none; scrollbar-width: none; } .slidev-code-wrapper::-webkit-scrollbar { display: none; /* Safari and Chrome */ } /* Remove margin from code block */ .slidev-code.shiki { margin: 0; } </style>src/exercises/section-1/006_resourceful-layers.ts
Remove the Scope requirement from our final Layer:
- Resourceful services should cleanup when the program ends
src/exercises/section-1/006_resourceful-layers-solution.ts
- We used
Layer.scopedto control the liftime of resources acquired during service construction - We locally eliminated requirements for each of our services
- We created a
MainLayerwhich combines all of our services - We provided a
NodeHttpClientto theMainLayerto satisfy all requirements
There are two primary methods for Layer composition:
- Merging - merges the inputs and outputs of two layers together
- Providing - provides the outputs of one layer as inputs to another
<<< @/src/examples/section-1/012_merging-layers.ts ts
<<< @/src/examples/section-1/013_providing-layers.ts ts
<<< @/src/examples/section-1/014_providing-and-merging-layers.ts ts
src/exercises/section-1/007_layer-composition.ts
Remove the Scope dependency from our final Layer:
- Resourceful services should cleanup when the program ends
src/exercises/section-1/007_layer-composition-solution.ts
- We practiced
Layercomposition using the following methods:Layer.provideLayer.mergeLayer.provideMerge
- We observed why local elimination of requirements is recommended
- Identify key subsystems within an application
- Decompose these subsystems into services
- Build up a set of top-level layers to provide
- Use
Effect.Servicewherever possible - Locally provide service dependencies (if possible)
- Avoid multiple calls to
Effect.provide - Remember that
Layers are memoized by reference
src/exercises/section-1/008_running-the-application.ts
Run the Pun-ishment Protocol application!
- You can use the following command to run the file
pnpm exercise ./src/exercises/section-1/008_running-the-application.ts
src/exercises/section-1/008_running-the-application.ts
- We combined our
Layers into aMainLayer - We used
Effect.provideto provide ourLayerto themainprogram - We ran our program and observed the output
- Learn how to integrate Effect into an existing codebase
- Understand strategies for wrapping existing business logic into Effect layers
- Gain experience in incrementally adopting Effect
- Explore interoperability with non-Effect code
- But you are already using other frameworks
- You are using libraries with Promise-based APIs
- Existing code isn't written holistically
- Error handling
- Resource management
- Interruption
- Observability
When adopting Effect, you start thinking about things that you might have overlooked before.
- What kind of errors can occur?
- Are resources being alloacted here? And how should I release them?
- How do I abort expensive computations?
- How do I monitor the execution of this code?
A common approach to wrapping Promise-based libraries with Effect is to create
an Effect.Service that exposes an use method.
interface SomeApi {}
declare const use: <A>(f: (api: SomeApi) => Promise<A>) => Effect<A>src/demos/section-2/openai-00.ts
- We used Effect.Service with the
usepattern to wrap the OpenAI library - We used the
Configmodule to retrieve the client configuration - We used
Schema.TaggedErrorto wrap errors (you could also useData.TaggedError) - We considered interruption by passing an
AbortSignal - We used
Effect.fnto add tracing to ourusemethod - There was no "resources" that needed to be managed
src/exercises/section-2/sqlite-01.ts
A lot of APIs return paginated data. How do we wrap them with Effect?
- Streams!
import { Stream } from "effect"
- Stream.paginate*
Allows you to continuously fetch data using some kind of cursor.
export const paginateChunkEffect: <S, A, E, R>(
s: S,
f: (s: S) => Effect.Effect<readonly [Chunk.Chunk<A>, Option.Option<S>], E, R>,
) => Stream<A, E, R>import { Chunk, Effect, Option, Stream } from "effect"
Stream.paginateChunkEffect(1, (page) =>
fetchPage(page).pipe(
Effect.map((items) => [Chunk.unsafeFromArray(items), Option.some(page + 1)]),
),
)src/demos/section-2/openai-paginate-00.ts
- We used
Stream.paginateChunkEffectto continuously fetch data - We tracked the cursor using OpenAI's Page api
- We used
Effect.fn&Stream.withSpanto add tracing to our stream
- Some libraries have more specific APIs, like streaming completions from OpenAI
- This may require adding additional service methods to make usage more ergonomic
src/demos/section-2/openai-completions-00.ts
- For commonly used APIs, it may be beneficial to create seperate service methods to improve ergonomics
- There is often Effect API's you can use to wrap common JavaScript data types.
I.e. we used
Stream.fromAsyncIterableto wrap the OpenAI stream. - Creating ergonomic APIs can require some reverse engineering effort
src/exercises/section-2/sqlite-02.ts
In some scenarios, you need to wrap an API that invokes a callback multiple times, such as request handlers or event listeners.
- You often want to access your Effect services in these callbacks
- You need to ensure that fibers are properly managed, to prevent leaks
- Fibers represent a running Effect computation
- Directly fork a fiber in the callback, and subscribe to the result
-
Indirectly fork a fiber, by adding requests to a queue and processing them
in a worker. If the callback requires a response, send back a signal using a
Deferred. - Convert the callback into a stream (if the callback doesn't require a response)
If you don't need to return a value to the callback, you can convert the
multi-shot callback into a Stream, using the Stream.async family of functions.
This is useful for event based APIs.
Stream.async&Stream.asyncScoped- for when the callback supports back-pressureStream.asyncPush- for when the backing API doesn't support back-pressure
src/demo/section-2/events-00.ts
There are several utilities provided by Effect for wrapping JavaScript data sources:
Stream.fromEventListener- for wrapping event listenersStream.fromAsyncIterable- for wrapping async iterablesStream.fromReadableStream- for wrapping web readable streamsNodeStream.fromReadable- for wrapping Node.js readable streamsimport { NodeStream } from "@effect/platform-node"
NodeSink.fromWritable- for wrapping Node.js writable streamsimport { NodeSink } from "@effect/platform-node"
There are several options for running Effect's:
- Use
Effect.runFork,Effect.runPromiseetc. - Use
Effect.runtimeto access the current runtime for running Effect's, which is useful if you need to access services - Use the
FiberSetmodule to manage fibers, which adds life-cycle management
When managing one or many fibers, the Fiber{Handle,Set,Map} modules can be
used to ensure that the lifecycle of the fibers are managed correctly.
FiberHandle- for managing a single fiber- Useful for managing a server that needs to be started and stopped
FiberSet- for managing multiple fibers without any identity- Useful for managing request handlers
FiberMap- for managing multiple fibers with keys / identity- Useful for managing a well-known set of fibers, like a group of background tasks indexed by a key
src/demos/section-2/express-00.ts
- We used
FiberSetto run the request handlers - We used
Effect.acquireReleaseto ensure the server is properly shut down - We used our Effect services by accessing them outside of the request handlers
- How do we compose different parts of a large application together?
- Maybe we want to test different parts of the application in isolation?
- Improve error handling and make it more ergonomic
src/demos/section-2/express-layer-00.ts
- We used
Layerto compose different parts of the application together - We added a
addRoutehelper to ensure request handlers consider:- Error handling
- Interruption
- Observability
src/exercises/section-2/express/main.ts
When using Effect in frontend frameworks, it requires a different approach compared to the backend, as you often don't control the "main" entry-point.
- Use
ManagedRuntimeto integrate Effect services into components- Use React's context API to provide the runtime to your components
- Experimental:
@effect-rx/rxpackage- Provides a jotai-like API for integrating Effect with frameworks like React
- Integration packages:
@effect-rx/rx-react&@effect-rx/rx-vue
Create a runtime that can execute Effect's from a Layer
import { Effect, ManagedRuntime } from "effect"
import { OpenAi } from "./services.js"
// OpenAi.Default: Layer<OpenAi>
const runtime = ManagedRuntime.make(OpenAi.Default)
declare const effect: Effect.Effect<void, never, OpenAi>
runtime.runPromise(effect)- A "black box" data type that can memoize the result of building a Layer.
- Relatively low-level, but can be used to ensure the same Layer is only built once across your application.
import { Effect, Layer, ManagedRuntime } from "effect"
import { OpenAi } from "./services.js"
const memoMap = Effect.runSync(Layer.makeMemoMap)
const runtimeA = ManagedRuntime.make(OpenAi.Default, memoMap)
const runtimeB = ManagedRuntime.make(OpenAi.Default, memoMap)src/demos/section-2/react
- We used
ManagedRuntimeto consume Effect services in React components- Wrapped with React's context API and
useEffectto manage the runtime lifecycle
- Wrapped with React's context API and
- We used a global
MemoMapto ensure that the same Layer is only built once if used in multipleManagedRuntimeinstances - We passed the
AbortSignalfrom@tanstack/react-queryto therunPromisecall, to integrate with Effect's interruption model - We integrated
Streamwith React'suseEffect
src/exercises/section-2/react
- Run it with
pnpm vite src/exercises/section-2/react