@zambit/elevate-ts 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLA-CORPORATE.md +72 -0
- package/CLA-INDIVIDUAL.md +71 -0
- package/DUAL-PUBLISHING-STRATEGY.md +571 -0
- package/LICENSE +610 -0
- package/README.md +77 -0
- package/dist/cjs/Either.d.ts +67 -0
- package/dist/cjs/Either.d.ts.map +1 -0
- package/dist/cjs/Either.js +147 -0
- package/dist/cjs/Either.js.map +1 -0
- package/dist/cjs/EitherAsync.d.ts +139 -0
- package/dist/cjs/EitherAsync.d.ts.map +1 -0
- package/dist/cjs/EitherAsync.js +171 -0
- package/dist/cjs/EitherAsync.js.map +1 -0
- package/dist/cjs/Function.d.ts +153 -0
- package/dist/cjs/Function.d.ts.map +1 -0
- package/dist/cjs/Function.js +110 -0
- package/dist/cjs/Function.js.map +1 -0
- package/dist/cjs/List.d.ts +134 -0
- package/dist/cjs/List.d.ts.map +1 -0
- package/dist/cjs/List.js +243 -0
- package/dist/cjs/List.js.map +1 -0
- package/dist/cjs/Maybe.d.ts +64 -0
- package/dist/cjs/Maybe.d.ts.map +1 -0
- package/dist/cjs/Maybe.js +122 -0
- package/dist/cjs/Maybe.js.map +1 -0
- package/dist/cjs/MaybeAsync.d.ts +115 -0
- package/dist/cjs/MaybeAsync.d.ts.map +1 -0
- package/dist/cjs/MaybeAsync.js +151 -0
- package/dist/cjs/MaybeAsync.js.map +1 -0
- package/dist/cjs/NonEmptyList.d.ts +86 -0
- package/dist/cjs/NonEmptyList.d.ts.map +1 -0
- package/dist/cjs/NonEmptyList.js +128 -0
- package/dist/cjs/NonEmptyList.js.map +1 -0
- package/dist/cjs/Reader.d.ts +53 -0
- package/dist/cjs/Reader.d.ts.map +1 -0
- package/dist/cjs/Reader.js +60 -0
- package/dist/cjs/Reader.js.map +1 -0
- package/dist/cjs/State.d.ts +71 -0
- package/dist/cjs/State.d.ts.map +1 -0
- package/dist/cjs/State.js +94 -0
- package/dist/cjs/State.js.map +1 -0
- package/dist/cjs/Tuple.d.ts +69 -0
- package/dist/cjs/Tuple.d.ts.map +1 -0
- package/dist/cjs/Tuple.js +73 -0
- package/dist/cjs/Tuple.js.map +1 -0
- package/dist/cjs/Validation.d.ts +53 -0
- package/dist/cjs/Validation.d.ts.map +1 -0
- package/dist/cjs/Validation.js +77 -0
- package/dist/cjs/Validation.js.map +1 -0
- package/dist/cjs/index.d.ts +12 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +25 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/tokens/config.d.ts +43 -0
- package/dist/cjs/tokens/config.d.ts.map +1 -0
- package/dist/cjs/tokens/config.js +162 -0
- package/dist/cjs/tokens/config.js.map +1 -0
- package/dist/cjs/tokens/cssGen.d.ts +18 -0
- package/dist/cjs/tokens/cssGen.d.ts.map +1 -0
- package/dist/cjs/tokens/cssGen.js +144 -0
- package/dist/cjs/tokens/cssGen.js.map +1 -0
- package/dist/cjs/tokens/generateCSS.d.ts +18 -0
- package/dist/cjs/tokens/generateCSS.d.ts.map +1 -0
- package/dist/cjs/tokens/generateCSS.js +83 -0
- package/dist/cjs/tokens/generateCSS.js.map +1 -0
- package/dist/cjs/tokens/index.d.ts +13 -0
- package/dist/cjs/tokens/index.d.ts.map +1 -0
- package/dist/cjs/tokens/index.js +12 -0
- package/dist/cjs/tokens/index.js.map +1 -0
- package/dist/cjs/tsconfig.tsbuildinfo +1 -0
- package/dist/esm/Either.d.ts +67 -0
- package/dist/esm/Either.d.ts.map +1 -0
- package/dist/esm/Either.js +136 -0
- package/dist/esm/Either.js.map +1 -0
- package/dist/esm/EitherAsync.d.ts +139 -0
- package/dist/esm/EitherAsync.d.ts.map +1 -0
- package/dist/esm/EitherAsync.js +173 -0
- package/dist/esm/EitherAsync.js.map +1 -0
- package/dist/esm/Function.d.ts +153 -0
- package/dist/esm/Function.d.ts.map +1 -0
- package/dist/esm/Function.js +109 -0
- package/dist/esm/Function.js.map +1 -0
- package/dist/esm/List.d.ts +134 -0
- package/dist/esm/List.d.ts.map +1 -0
- package/dist/esm/List.js +243 -0
- package/dist/esm/List.js.map +1 -0
- package/dist/esm/Maybe.d.ts +64 -0
- package/dist/esm/Maybe.d.ts.map +1 -0
- package/dist/esm/Maybe.js +113 -0
- package/dist/esm/Maybe.js.map +1 -0
- package/dist/esm/MaybeAsync.d.ts +115 -0
- package/dist/esm/MaybeAsync.d.ts.map +1 -0
- package/dist/esm/MaybeAsync.js +150 -0
- package/dist/esm/MaybeAsync.js.map +1 -0
- package/dist/esm/NonEmptyList.d.ts +86 -0
- package/dist/esm/NonEmptyList.d.ts.map +1 -0
- package/dist/esm/NonEmptyList.js +128 -0
- package/dist/esm/NonEmptyList.js.map +1 -0
- package/dist/esm/Reader.d.ts +53 -0
- package/dist/esm/Reader.d.ts.map +1 -0
- package/dist/esm/Reader.js +60 -0
- package/dist/esm/Reader.js.map +1 -0
- package/dist/esm/State.d.ts +71 -0
- package/dist/esm/State.d.ts.map +1 -0
- package/dist/esm/State.js +94 -0
- package/dist/esm/State.js.map +1 -0
- package/dist/esm/Tuple.d.ts +69 -0
- package/dist/esm/Tuple.d.ts.map +1 -0
- package/dist/esm/Tuple.js +73 -0
- package/dist/esm/Tuple.js.map +1 -0
- package/dist/esm/Validation.d.ts +53 -0
- package/dist/esm/Validation.d.ts.map +1 -0
- package/dist/esm/Validation.js +79 -0
- package/dist/esm/Validation.js.map +1 -0
- package/dist/esm/index.d.ts +12 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +22 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/src/Either.d.ts +67 -0
- package/dist/esm/src/Either.d.ts.map +1 -0
- package/dist/esm/src/Either.js +147 -0
- package/dist/esm/src/Either.js.map +1 -0
- package/dist/esm/src/EitherAsync.d.ts +139 -0
- package/dist/esm/src/EitherAsync.d.ts.map +1 -0
- package/dist/esm/src/EitherAsync.js +171 -0
- package/dist/esm/src/EitherAsync.js.map +1 -0
- package/dist/esm/src/Function.d.ts +153 -0
- package/dist/esm/src/Function.d.ts.map +1 -0
- package/dist/esm/src/Function.js +110 -0
- package/dist/esm/src/Function.js.map +1 -0
- package/dist/esm/src/List.d.ts +134 -0
- package/dist/esm/src/List.d.ts.map +1 -0
- package/dist/esm/src/List.js +243 -0
- package/dist/esm/src/List.js.map +1 -0
- package/dist/esm/src/Maybe.d.ts +64 -0
- package/dist/esm/src/Maybe.d.ts.map +1 -0
- package/dist/esm/src/Maybe.js +122 -0
- package/dist/esm/src/Maybe.js.map +1 -0
- package/dist/esm/src/MaybeAsync.d.ts +115 -0
- package/dist/esm/src/MaybeAsync.d.ts.map +1 -0
- package/dist/esm/src/MaybeAsync.js +151 -0
- package/dist/esm/src/MaybeAsync.js.map +1 -0
- package/dist/esm/src/NonEmptyList.d.ts +86 -0
- package/dist/esm/src/NonEmptyList.d.ts.map +1 -0
- package/dist/esm/src/NonEmptyList.js +128 -0
- package/dist/esm/src/NonEmptyList.js.map +1 -0
- package/dist/esm/src/Reader.d.ts +53 -0
- package/dist/esm/src/Reader.d.ts.map +1 -0
- package/dist/esm/src/Reader.js +60 -0
- package/dist/esm/src/Reader.js.map +1 -0
- package/dist/esm/src/State.d.ts +71 -0
- package/dist/esm/src/State.d.ts.map +1 -0
- package/dist/esm/src/State.js +94 -0
- package/dist/esm/src/State.js.map +1 -0
- package/dist/esm/src/Tuple.d.ts +69 -0
- package/dist/esm/src/Tuple.d.ts.map +1 -0
- package/dist/esm/src/Tuple.js +73 -0
- package/dist/esm/src/Tuple.js.map +1 -0
- package/dist/esm/src/Validation.d.ts +53 -0
- package/dist/esm/src/Validation.d.ts.map +1 -0
- package/dist/esm/src/Validation.js +77 -0
- package/dist/esm/src/Validation.js.map +1 -0
- package/dist/esm/src/index.d.ts +12 -0
- package/dist/esm/src/index.d.ts.map +1 -0
- package/dist/esm/src/index.js +25 -0
- package/dist/esm/src/index.js.map +1 -0
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -0
- package/elevate-ts-vs-effect-critique.md +806 -0
- package/eslint.config.js +104 -0
- package/package.json +139 -0
|
@@ -0,0 +1,806 @@
|
|
|
1
|
+
# elevate-ts vs Effect-TS: Technical Comparison & Critique
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-04-10
|
|
4
|
+
**Scope:** Runtime-agnostic functional programming libraries for TypeScript
|
|
5
|
+
**Excluded:** Node.js-specific concerns (elevate-ts does not target Node.js)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Core Type Design
|
|
10
|
+
|
|
11
|
+
### elevate-ts: Simple, Focused Monads
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
// Maybe<A>
|
|
15
|
+
type Maybe<A> = Just<A> | Nothing;
|
|
16
|
+
|
|
17
|
+
// Either<L, R>
|
|
18
|
+
type Either<L, R> = Left<L> | Right<R>;
|
|
19
|
+
|
|
20
|
+
// Reader<R, A> — simple environment threading
|
|
21
|
+
type Reader<R, A> = { readonly tag: 'Reader'; readonly run: (env: R) => A };
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Characteristics:**
|
|
25
|
+
|
|
26
|
+
- Single-value result types; no intrinsic dependency context
|
|
27
|
+
- Reader is just an alias for `(env: R) => A`, wrapped for branded clarity
|
|
28
|
+
- All three core monads are **discriminated unions** with tagged structures
|
|
29
|
+
- No concept of a unifying effect type; composition is modular but not hierarchical
|
|
30
|
+
- ~50 lines per monad implementation
|
|
31
|
+
|
|
32
|
+
**Strengths:**
|
|
33
|
+
|
|
34
|
+
- Dead simple to understand and teach
|
|
35
|
+
- Zero learning curve for JavaScript developers
|
|
36
|
+
- Minimal surface area means fewer bugs in the library itself
|
|
37
|
+
- Extremely fast—no hidden allocations or fiber overhead
|
|
38
|
+
|
|
39
|
+
**Weaknesses:**
|
|
40
|
+
|
|
41
|
+
- No way to encode "this computation requires dependency X" _in the type itself_
|
|
42
|
+
- Splitting error handling concerns (expected errors vs unexpected failures) requires separate conventions
|
|
43
|
+
- No first-class way to model concurrent or cancellable computations
|
|
44
|
+
- Reader is lazy but stateless; cannot model resource acquisition/release
|
|
45
|
+
- Composing independent effects (fan-out) requires manual lifting into arrays or tuples
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
### Effect-TS: Unified Effect Type
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// Effect<A, E, R> — three-dimensional computation
|
|
53
|
+
type Effect<out A, out E = never, out R = never> = ...
|
|
54
|
+
|
|
55
|
+
// Exit<A, E> — the actual result of running an Effect
|
|
56
|
+
type Exit<A, E = never> = Success<A> | Failure<Cause<E>>
|
|
57
|
+
|
|
58
|
+
// Cause<E> — lossless error structure
|
|
59
|
+
type Cause<E> = Fail<E> | Die | Interrupt | Sequential<E> | Parallel<E>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Characteristics:**
|
|
63
|
+
|
|
64
|
+
- Single unified effect type; three type parameters encode **success** (A), **error** (E), and **environment/dependencies** (R)
|
|
65
|
+
- Exit/Cause model captures complete failure history: typed errors, unexpected defects, interruptions, and compositions
|
|
66
|
+
- All effects are **lazy** (not executed until run); enables precise control over resource lifecycle
|
|
67
|
+
- Environment (R) is threaded through the type system; used to encode dependencies and resource scopes
|
|
68
|
+
- ~1000+ lines across Effect, Exit, Cause, Context, Layer modules (feature-complete)
|
|
69
|
+
|
|
70
|
+
**Strengths:**
|
|
71
|
+
|
|
72
|
+
- **R parameter is mandatory in the type.** Can never accidentally run an effect without providing required dependencies
|
|
73
|
+
- **Cause model is lossless.** If your effect throws, fails, or gets cancelled—all of that is captured. Debugging concurrent systems becomes tractable
|
|
74
|
+
- **Single effect type simplifies composition.** `Effect.all`, `Effect.race`, etc. work on the same type
|
|
75
|
+
- **Resource safety by default.** Layer handles acquisition, cleanup, and error recovery automatically
|
|
76
|
+
- Type system prevents incorrect sequencing (e.g., you cannot run an effect that requires a service without providing it)
|
|
77
|
+
|
|
78
|
+
**Weaknesses:**
|
|
79
|
+
|
|
80
|
+
- Steep learning curve; three type parameters require understanding of variance and multiple monadic laws
|
|
81
|
+
- Cause model adds abstraction overhead; debugging simple, non-concurrent code requires understanding why your error is wrapped in a Cause
|
|
82
|
+
- Library size is massive (~150+ modules); tree-shaking is essential for smaller bundles
|
|
83
|
+
- Effect execution requires understanding of Fiber, Runtime, and Executor concepts for troubleshooting
|
|
84
|
+
- Generator syntax (`Effect.gen`) is syntactic sugar that hides the underlying monadic structure
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 2. Error Handling
|
|
89
|
+
|
|
90
|
+
### elevate-ts: Binary Simplicity
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// Either<L, R> — Left is error, Right is success
|
|
94
|
+
const attempt = (f: () => Value): Either<string, Value> => Either.tryCatch(f, (e) => String(e));
|
|
95
|
+
|
|
96
|
+
// Flat error type: no distinction between expected errors and unexpected exceptions
|
|
97
|
+
const result = pipe(
|
|
98
|
+
attempt(() => JSON.parse(input)),
|
|
99
|
+
Either.chain((parsed) => validate(parsed))
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// If JSON.parse throws: Left("SyntaxError: ...")
|
|
103
|
+
// If validate returns Left: Left("Validation failed")
|
|
104
|
+
// Both Left—no structural difference
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Design Philosophy:**
|
|
108
|
+
|
|
109
|
+
- Errors are **first-class values** but **not deeply modeled**
|
|
110
|
+
- No distinction between recoverable (expected) and unrecoverable (unexpected) failures
|
|
111
|
+
- Stack traces are converted to strings; context is lost
|
|
112
|
+
- Parallel errors are modeled as an array of Lefts; sequential errors require manual sequencing
|
|
113
|
+
|
|
114
|
+
**Strengths:**
|
|
115
|
+
|
|
116
|
+
- Dead simple for single-threaded, synchronous error handling
|
|
117
|
+
- Fast to implement and reason about
|
|
118
|
+
- Suitable for small scripts and form validation (classical use case)
|
|
119
|
+
|
|
120
|
+
**Weaknesses:**
|
|
121
|
+
|
|
122
|
+
- **Cannot distinguish exception types programmatically.** If you need "retry on timeout, fail on auth error," you must encode both in the error type manually
|
|
123
|
+
- **No execution trace.** If an effect is interrupted or times out, you lose that information
|
|
124
|
+
- **Parallel/concurrent errors require convention.** No native way to collect multiple independent failures
|
|
125
|
+
- **Stack traces are lost.** Converting exceptions to strings destroys the original context
|
|
126
|
+
- **Defects (unexpected exceptions) blend with expected errors.** No way to say "this error was supposed to be caught; this one wasn't"
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
### Effect-TS: Lossless Cause Model
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
// Cause<E> — models failure in complete detail
|
|
134
|
+
type Cause<E> =
|
|
135
|
+
| Fail<E> // Expected, typed error
|
|
136
|
+
| Die // Unexpected exception (preserves stack trace)
|
|
137
|
+
| Interrupt // Effect was cancelled
|
|
138
|
+
| Sequential<E> // Chain failed; cleanup also failed
|
|
139
|
+
| Parallel<E>; // Multiple concurrent failures
|
|
140
|
+
|
|
141
|
+
// Exit<A, E>
|
|
142
|
+
type Exit<A, E> = Success<A> | Failure<Cause<E>>;
|
|
143
|
+
|
|
144
|
+
// Example: concurrent errors are captured structurally
|
|
145
|
+
const results = Effect.allPar([effect1, effect2, effect3]);
|
|
146
|
+
// If effect1 fails with Fail("error1") and effect2 throws new Error("bug"),
|
|
147
|
+
// Cause captures both:
|
|
148
|
+
// Parallel([Fail("error1"), Die(Error("bug"))])
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Design Philosophy:**
|
|
152
|
+
|
|
153
|
+
- Errors are **completely modeled** in the type system
|
|
154
|
+
- **Expected errors** (Fail) vs **unexpected exceptions** (Die) are structurally distinct
|
|
155
|
+
- **Interruption/cancellation is a first-class failure mode**
|
|
156
|
+
- **Sequential failures preserve causality** (main error + cleanup error are both retained)
|
|
157
|
+
|
|
158
|
+
**Strengths:**
|
|
159
|
+
|
|
160
|
+
- **Precise error discrimination.** Can match on error type without string parsing
|
|
161
|
+
- **Full execution traces preserved.** Debugging concurrent failures shows exactly which fiber failed and why
|
|
162
|
+
- **Cleanup errors don't hide main errors.** If a finalizer throws while handling another error, both are captured
|
|
163
|
+
- **Timeout/cancellation is explicit.** A cancelled effect looks different from a failed one
|
|
164
|
+
- **Observability ready.** Structured error information integrates with logging/tracing systems
|
|
165
|
+
|
|
166
|
+
**Weaknesses:**
|
|
167
|
+
|
|
168
|
+
- **Cause is abstract.** End users cannot directly pattern-match on it; must use helper functions (Exit.match, etc.)
|
|
169
|
+
- **Causes are verbose for simple cases.** A single validation error is still wrapped in `Cause<ValidationError>`; no "unwrapping" helper for the common case
|
|
170
|
+
- **Serialization complexity.** Cause structures with circular references (common in concurrent scenarios) are harder to log
|
|
171
|
+
- **Die (exception wrapper) still loses information.** If the exception is not designed to be meaningful (e.g., `throw {}` in user code), Cause doesn't recover that
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## 3. Dependency Injection & Service Management
|
|
176
|
+
|
|
177
|
+
### elevate-ts: Reader Monad + Manual Threading
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
// Reader<R, A> — essentially (env: R) => A, branded
|
|
181
|
+
type Reader<R, A> = { tag: 'Reader'; run: (env: R) => A }
|
|
182
|
+
|
|
183
|
+
// Define a service
|
|
184
|
+
type Database = { query: (sql: string) => Promise<any[]> }
|
|
185
|
+
|
|
186
|
+
// Create a Reader that depends on Database
|
|
187
|
+
const getUser = (userId: string): Reader<Database, Promise<User>> =>
|
|
188
|
+
Reader((db) => db.query(`SELECT * FROM users WHERE id = ${userId}`))
|
|
189
|
+
|
|
190
|
+
// Compose readers with chaining
|
|
191
|
+
const getUserWithPosts = (userId: string): Reader<Database, Promise<{ user: User; posts: Post[] }>> =>
|
|
192
|
+
pipe(
|
|
193
|
+
Reader.ask<Database>(),
|
|
194
|
+
Reader.chain((db) =>
|
|
195
|
+
Reader((db2) => ({
|
|
196
|
+
user: db.query('SELECT * FROM users WHERE id = ?', [userId]),
|
|
197
|
+
posts: db2.query('SELECT * FROM posts WHERE user_id = ?', [userId]),
|
|
198
|
+
}))
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
// Run with a concrete database
|
|
203
|
+
const env: Database = { query: (...) => ... }
|
|
204
|
+
const result = getUser('123').run(env)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Characteristics:**
|
|
208
|
+
|
|
209
|
+
- Reader is **just a lazy function** with environment threading
|
|
210
|
+
- No built-in memoization; if you ask for the same service twice, it's computed twice
|
|
211
|
+
- No resource lifecycle management; acquiring a database connection requires manual wrapping
|
|
212
|
+
- Composing multiple services requires manually threading the environment through each Reader
|
|
213
|
+
|
|
214
|
+
**Strengths:**
|
|
215
|
+
|
|
216
|
+
- **Minimal overhead.** Reader is literally just a function; no runtime indirection
|
|
217
|
+
- **Transparent.** Easy to see what dependencies flow through the computation
|
|
218
|
+
- **Works fine for static, compile-time-known dependencies**
|
|
219
|
+
|
|
220
|
+
**Weaknesses:**
|
|
221
|
+
|
|
222
|
+
- **No resource safety.** If a Reader acquires a resource (database, file handle, HTTP connection), cleanup is manual and easy to forget
|
|
223
|
+
- **No memoization.** Multiple calls to `Reader.ask()` don't reuse a cached service; each call recreates it
|
|
224
|
+
- **Composing independent services is awkward.** No native way to say "I need service A and service B in parallel; fail if either setup fails"
|
|
225
|
+
- **No typed access.** Environment is just `R`; nothing prevents accidentally threading the wrong environment into a sub-Reader
|
|
226
|
+
- **No dependency graph.** Cannot inspect what services are required without running the Reader
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
### Effect-TS: Layer + Context/Tag System
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
// Tag<Id, Service> — a typed key for a service
|
|
234
|
+
class UserRepository extends Context.Tag("UserRepository")<UserRepository, {
|
|
235
|
+
getUser: (id: string) => Effect<User>
|
|
236
|
+
}>() {}
|
|
237
|
+
|
|
238
|
+
// Layer<ROut, E, RIn> — a recipe for building services
|
|
239
|
+
// ROut: what services this layer provides
|
|
240
|
+
// E: errors that can occur during setup
|
|
241
|
+
// RIn: dependencies required to build these services
|
|
242
|
+
const UserRepositoryLive: Layer<UserRepository, Error, never> =
|
|
243
|
+
Layer.sync(() => ({
|
|
244
|
+
getUser: (id: string) => Effect.promise(() => db.query(...)),
|
|
245
|
+
}))
|
|
246
|
+
|
|
247
|
+
// Dependency: a UserRepository requires a Database
|
|
248
|
+
const DatabaseLive: Layer<Database, Error, never> =
|
|
249
|
+
Layer.succeed(() => createDatabaseConnection())
|
|
250
|
+
|
|
251
|
+
// Compose layers; Effect enforces that dependencies are provided
|
|
252
|
+
const AppLive: Layer<UserRepository | Database, Error, never> =
|
|
253
|
+
Layer.compose(UserRepositoryLive, DatabaseLive)
|
|
254
|
+
|
|
255
|
+
// Use the service in an effect
|
|
256
|
+
const getUser = (id: string): Effect<User, Error, UserRepository> =>
|
|
257
|
+
Effect.serviceWith(UserRepository, (repo) => repo.getUser(id))
|
|
258
|
+
|
|
259
|
+
// Run the effect; layers are automatically acquired, shared, and cleaned up
|
|
260
|
+
const result = pipe(
|
|
261
|
+
getUser('123'),
|
|
262
|
+
Effect.provide(AppLive),
|
|
263
|
+
Effect.runPromise
|
|
264
|
+
)
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Characteristics:**
|
|
268
|
+
|
|
269
|
+
- **Tag** is a **strongly-typed service identifier** with a unique key
|
|
270
|
+
- **Layer** is a **composable service factory** with automatic resource management
|
|
271
|
+
- **Memoization by default.** Identical layers are executed once; results are shared across the effect
|
|
272
|
+
- **Typed dependency graph.** The R parameter in Effect explicitly lists required services
|
|
273
|
+
- **Sequential composition.** Layers are built in order; if A requires B, and B requires C, the system handles the sequencing
|
|
274
|
+
|
|
275
|
+
**Strengths:**
|
|
276
|
+
|
|
277
|
+
- **Type-safe dependency injection.** Cannot accidentally provide the wrong service; the type system prevents it
|
|
278
|
+
- **Resource safety guaranteed.** Layer handles acquisition, cleanup, and error recovery automatically
|
|
279
|
+
- **Memoization built-in.** Service creation happens once per scope; no accidental duplication
|
|
280
|
+
- **Composable dependency graphs.** Can declare that service A requires B requires C, and the system builds them in the right order
|
|
281
|
+
- **Testability.** Easy to provide a test layer that overrides production services
|
|
282
|
+
- **Observability hooks.** Layer supports spans and logging for each service lifecycle event
|
|
283
|
+
|
|
284
|
+
**Weaknesses:**
|
|
285
|
+
|
|
286
|
+
- **Verbose for simple cases.** A single service requires defining a Tag, creating a Layer, and providing it—boilerplate for Hello World
|
|
287
|
+
- **Implicit ordering.** If Layer A depends on B which depends on C, this is not explicit in the type; you find out at runtime if it fails
|
|
288
|
+
- **Learning curve.** Understanding variance, covariance in the R parameter is non-trivial
|
|
289
|
+
- **No dynamic dependency lookup.** All dependencies must be known at compile time; cannot say "use whatever service matches this interface"
|
|
290
|
+
- **Error during layer setup hides original effect.** If a dependency fails to initialize, the original effect error is lost (though Cause captures this)
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## 4. Composability: Pipe vs Pipe vs Gen
|
|
295
|
+
|
|
296
|
+
### elevate-ts: `pipe()`
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
import { pipe } from '@zambit/elevate-ts/Function';
|
|
300
|
+
|
|
301
|
+
const result = pipe(initialValue, step1, step2, step3);
|
|
302
|
+
// Explicitly chains operations left-to-right
|
|
303
|
+
// Each step is a simple function: (A) => B
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**Characteristics:**
|
|
307
|
+
|
|
308
|
+
- **Function composition** via pipe
|
|
309
|
+
- **Data flows left-to-right** in a visually clear manner
|
|
310
|
+
- **Works with any function** that takes one argument
|
|
311
|
+
- **No special syntax** required
|
|
312
|
+
|
|
313
|
+
**Strengths:**
|
|
314
|
+
|
|
315
|
+
- **Visually clear.** Easy to follow the data flow
|
|
316
|
+
- **Minimal indirection.** No operators, no operator precedence surprises
|
|
317
|
+
- **Works with plain functions.** Can mix monadic and non-monadic operations
|
|
318
|
+
|
|
319
|
+
**Weaknesses:**
|
|
320
|
+
|
|
321
|
+
- **Requires explicit wrapping.** `map`, `chain`, etc. must be curried and passed explicitly
|
|
322
|
+
- **Verbose for deeply nested operations.** 10+ steps = hard to read
|
|
323
|
+
- **No compile-time error checking.** Typos in function names appear at runtime
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
### Effect-TS: `pipe()` + `Effect.gen()`
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
import { pipe, Effect } from 'effect';
|
|
331
|
+
|
|
332
|
+
// 1. Using pipe (similar to elevate-ts)
|
|
333
|
+
const result1 = pipe(
|
|
334
|
+
Effect.succeed(5),
|
|
335
|
+
Effect.map((x) => x * 2),
|
|
336
|
+
Effect.flatMap((y) => Effect.succeed(y + 1))
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
// 2. Using Effect.gen (generator-based, looks imperative)
|
|
340
|
+
const result2 = Effect.gen(function* () {
|
|
341
|
+
const x = yield* Effect.succeed(5);
|
|
342
|
+
const y = x * 2;
|
|
343
|
+
const z = yield* Effect.succeed(y + 1);
|
|
344
|
+
return z;
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Both are equivalent; gen is syntactic sugar
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
**Characteristics:**
|
|
351
|
+
|
|
352
|
+
- **pipe() works like elevate-ts** but with Effect-specific operators
|
|
353
|
+
- **gen() uses JavaScript generators** to hide the monadic structure
|
|
354
|
+
- **Generator syntax looks imperative** but executes as a monad under the hood
|
|
355
|
+
|
|
356
|
+
**Strengths (pipe):**
|
|
357
|
+
|
|
358
|
+
- **Same as elevate-ts** plus Effect-specific helpers
|
|
359
|
+
- **Forced explicit sequencing** (easier to debug)
|
|
360
|
+
|
|
361
|
+
**Strengths (gen):**
|
|
362
|
+
|
|
363
|
+
- **Reads like imperative code** (familiar to non-FP developers)
|
|
364
|
+
- **Automatic monadic binding** (no explicit `flatMap` calls)
|
|
365
|
+
- **Easier for developers coming from async/await**
|
|
366
|
+
|
|
367
|
+
**Weaknesses (pipe):**
|
|
368
|
+
|
|
369
|
+
- **More verbose than gen for complex effects**
|
|
370
|
+
- **Operator precedence must be understood** (e.g., `map` before `flatMap`)
|
|
371
|
+
|
|
372
|
+
**Weaknesses (gen):**
|
|
373
|
+
|
|
374
|
+
- **Hides the monadic structure.** Beginners don't learn what's actually happening
|
|
375
|
+
- **Magical.** JavaScript generators are notoriously hard to debug; stack traces are confusing
|
|
376
|
+
- **Not idiomatic FP.** Experts find it unreadable; looks like old-school imperative code
|
|
377
|
+
- **Cannot inspect the generator** to understand dependencies without running it
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## 5. Runtime Agnosticism
|
|
382
|
+
|
|
383
|
+
### elevate-ts: Explicit Cloudflare Workers Target
|
|
384
|
+
|
|
385
|
+
**Design choices:**
|
|
386
|
+
|
|
387
|
+
- No Node.js built-ins (no `fs`, `path`, `stream`, etc.)
|
|
388
|
+
- No DOM APIs (no `window`, `document`)
|
|
389
|
+
- **Works on:** Node.js, Cloudflare Workers, browsers, Bun
|
|
390
|
+
- **Design goal:** Runtime-agnostic by avoiding platform-specific APIs altogether
|
|
391
|
+
- Zero dependencies; no reliance on platform-specific libraries
|
|
392
|
+
|
|
393
|
+
**Trade-off:**
|
|
394
|
+
|
|
395
|
+
- **Simpler:** Smaller bundle, no platform-specific code paths
|
|
396
|
+
- **Limited:** Cannot access filesystem, process APIs, or Node-specific packages
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
### Effect-TS: Multi-Platform Facade
|
|
401
|
+
|
|
402
|
+
**Design choices:**
|
|
403
|
+
|
|
404
|
+
- Core `effect` package is runtime-agnostic
|
|
405
|
+
- Platform-specific implementations in separate packages:
|
|
406
|
+
- `@effect/platform-node` — Node.js APIs (fs, path, child_process, etc.)
|
|
407
|
+
- `@effect/platform-bun` — Bun-specific APIs
|
|
408
|
+
- `@effect/platform-browser` — Browser APIs (fetch, localStorage, etc.)
|
|
409
|
+
- `@effect/platform` — Abstract interfaces (e.g., FileSystem trait)
|
|
410
|
+
- Each platform provides concrete Layer implementations
|
|
411
|
+
|
|
412
|
+
**Example:**
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
// Abstract: works on any platform with a FileSystem
|
|
416
|
+
const readFile = (path: string): Effect<string, Error, FileSystem> =>
|
|
417
|
+
Effect.serviceWith(FileSystem, (fs) => fs.readFileUtf8(path))
|
|
418
|
+
|
|
419
|
+
// Provide platform-specific implementation
|
|
420
|
+
import { FileSystemLive } from '@effect/platform-node'
|
|
421
|
+
const result = pipe(
|
|
422
|
+
readFile('/etc/hosts'),
|
|
423
|
+
Effect.provide(FileSystemLive)
|
|
424
|
+
Effect.runPromise
|
|
425
|
+
)
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
**Strengths:**
|
|
429
|
+
|
|
430
|
+
- **Single effect type across platforms** (no `NodeEffect<A,E>` vs `BrowserEffect<A,E>`)
|
|
431
|
+
- **Swappable implementations.** Test with mock FileSystem, deploy with node FileSystem
|
|
432
|
+
- **Type-safe platform features.** Cannot call Node-only APIs in browser code if properly layered
|
|
433
|
+
|
|
434
|
+
**Weaknesses:**
|
|
435
|
+
|
|
436
|
+
- **Bloated for Cloudflare Workers.** Must tree-shake platform-node, platform-bun to keep workers small
|
|
437
|
+
- **Abstraction overhead.** FileSystem is abstract; concrete implementations add a layer of indirection
|
|
438
|
+
- **Not designed for Workers.** The platform ecosystem is Node-centric; Workers support is secondary
|
|
439
|
+
|
|
440
|
+
---
|
|
441
|
+
|
|
442
|
+
## 6. Schema & Validation
|
|
443
|
+
|
|
444
|
+
### elevate-ts: None Built-in
|
|
445
|
+
|
|
446
|
+
```typescript
|
|
447
|
+
// Validation monad exists but is for applicative-style error collection, not schema validation
|
|
448
|
+
import { Validation } from '@zambit/elevate-ts';
|
|
449
|
+
|
|
450
|
+
// Manual validation
|
|
451
|
+
const validateUser = (data: unknown): Either<string[], User> => {
|
|
452
|
+
if (typeof data !== 'object' || data === null) return Left(['Not an object']);
|
|
453
|
+
const obj = data as Record<string, unknown>;
|
|
454
|
+
if (typeof obj.id !== 'string') return Left(['id must be a string']);
|
|
455
|
+
if (typeof obj.name !== 'string') return Left(['name must be a string']);
|
|
456
|
+
return Right({ id: obj.id, name: obj.name });
|
|
457
|
+
};
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
**Status:**
|
|
461
|
+
|
|
462
|
+
- Validation monad exists for **collecting errors during applicative operations** (not for schema)
|
|
463
|
+
- No schema language; validation is hand-written
|
|
464
|
+
- No type derivation; types and validators are separate
|
|
465
|
+
|
|
466
|
+
**Suitable for:**
|
|
467
|
+
|
|
468
|
+
- Small, hand-rolled validators
|
|
469
|
+
- Form validation (classical use case)
|
|
470
|
+
- Exploratory prototyping
|
|
471
|
+
|
|
472
|
+
**Not suitable for:**
|
|
473
|
+
|
|
474
|
+
- API contract validation
|
|
475
|
+
- Serialization/deserialization
|
|
476
|
+
- Type-driven code generation
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
### Effect-TS: First-Class Schema
|
|
481
|
+
|
|
482
|
+
```typescript
|
|
483
|
+
import { Schema, Effect } from 'effect';
|
|
484
|
+
|
|
485
|
+
// Define schema once; used for validation, serialization, docs
|
|
486
|
+
const UserSchema = Schema.Struct({
|
|
487
|
+
id: Schema.String,
|
|
488
|
+
name: Schema.String,
|
|
489
|
+
email: Schema.String,
|
|
490
|
+
age: Schema.optional(Schema.Int)
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
// Automatic validation
|
|
494
|
+
const parseUser = (data: unknown): Effect<User, ParseError> => Schema.decode(UserSchema)(data);
|
|
495
|
+
|
|
496
|
+
// Automatic serialization
|
|
497
|
+
const encodeUser = (user: User): Effect<unknown, ParseError> => Schema.encode(UserSchema)(user);
|
|
498
|
+
|
|
499
|
+
// Type is derived automatically
|
|
500
|
+
type User = Schema.Type<typeof UserSchema>;
|
|
501
|
+
// User = { id: string; name: string; email: string; age?: number }
|
|
502
|
+
|
|
503
|
+
// Used throughout the ecosystem
|
|
504
|
+
// - HTTP API endpoint definitions (Schema for request/response)
|
|
505
|
+
// - RPC serialization (Schema for protocol messages)
|
|
506
|
+
// - Database mappings (Schema for row types)
|
|
507
|
+
// - OpenAPI/Swagger generation (Schema for documentation)
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
**Characteristics:**
|
|
511
|
+
|
|
512
|
+
- **Single source of truth** for types and validation
|
|
513
|
+
- **Composable.** Schemas nest and combine declaratively
|
|
514
|
+
- **Errorless.** Schema errors are captured in the Effect type, not thrown
|
|
515
|
+
- **Type-safe serialization.** Encoding/decoding are type-checked
|
|
516
|
+
|
|
517
|
+
**Strengths:**
|
|
518
|
+
|
|
519
|
+
- **No duplication.** Type and schema are one
|
|
520
|
+
- **Ecosystem integration.** HttpApi, RPC, SQL all use Schema
|
|
521
|
+
- **Automatic code generation.** OpenAPI docs, client libraries derived from Schema
|
|
522
|
+
- **Round-trip safety.** Can encode then decode; result is equivalent to original
|
|
523
|
+
|
|
524
|
+
**Weaknesses:**
|
|
525
|
+
|
|
526
|
+
- **Learning curve.** Schema combinators are numerous; composition is not obvious
|
|
527
|
+
- **Performance cost.** Validation during decode is slower than a hand-written check
|
|
528
|
+
- **Strict by default.** Schema rejects unknown fields; must explicitly allow them
|
|
529
|
+
- **Serialization semantics differ.** Decoding integers from JSON strings requires explicit `.pipe(Schema.parseJson)`
|
|
530
|
+
|
|
531
|
+
---
|
|
532
|
+
|
|
533
|
+
## 7. Ecosystem & Feature Breadth
|
|
534
|
+
|
|
535
|
+
### elevate-ts: Minimal, Focused
|
|
536
|
+
|
|
537
|
+
| Feature | Status |
|
|
538
|
+
| ----------------- | ------------------------------------ |
|
|
539
|
+
| Core monads | [YES] Complete (Maybe, Either, etc.) |
|
|
540
|
+
| Async support | [YES] (EitherAsync, MaybeAsync) |
|
|
541
|
+
| DI / Environment | [YES] Basic (Reader) |
|
|
542
|
+
| Schema validation | [NO] Not built-in |
|
|
543
|
+
| HTTP server | [NO] Not included |
|
|
544
|
+
| Database | [NO] Not included |
|
|
545
|
+
| CLI tools | [NO] Not included |
|
|
546
|
+
| Testing utilities | [NO] Not included |
|
|
547
|
+
| Observability | [NO] Not included |
|
|
548
|
+
|
|
549
|
+
**Philosophy:**
|
|
550
|
+
|
|
551
|
+
- Minimal, batteries-not-included
|
|
552
|
+
- Users assemble their own stack
|
|
553
|
+
- ~500 lines of source code
|
|
554
|
+
|
|
555
|
+
**Suitable for:**
|
|
556
|
+
|
|
557
|
+
- Microlibraries
|
|
558
|
+
- Cloudflare Worker projects (form processing, data transformation)
|
|
559
|
+
- Educational purposes
|
|
560
|
+
|
|
561
|
+
---
|
|
562
|
+
|
|
563
|
+
### Effect-TS: Comprehensive Ecosystem
|
|
564
|
+
|
|
565
|
+
| Feature | Package | Status |
|
|
566
|
+
| ----------------- | --------------------- | -------------------------------------------------- |
|
|
567
|
+
| Core effects | effect | [YES] Complete |
|
|
568
|
+
| Schema validation | effect | [YES] Built-in |
|
|
569
|
+
| HTTP server | @effect/platform | [YES] Declarative HttpApi |
|
|
570
|
+
| Database | @effect/sql | [YES] 8+ implementations (PG, MySQL, SQLite, etc.) |
|
|
571
|
+
| CLI | @effect/cli | [YES] Command parsing, help generation |
|
|
572
|
+
| Testing | @effect/vitest | [YES] Vitest integration |
|
|
573
|
+
| Observability | @effect/opentelemetry | [YES] Tracing, metrics, logs |
|
|
574
|
+
| AI integration | @effect/ai | [YES] OpenAI, Anthropic, Bedrock, Google |
|
|
575
|
+
| RPC | @effect/rpc | [YES] Type-safe RPC |
|
|
576
|
+
| Cluster | @effect/cluster | [YES] Distributed computing |
|
|
577
|
+
| Durable workflows | @effect/workflow | [YES] Temporal-like workflows |
|
|
578
|
+
|
|
579
|
+
**Philosophy:**
|
|
580
|
+
|
|
581
|
+
- Comprehensive; most production concerns are addressed
|
|
582
|
+
- Layered ecosystem; use only what you need
|
|
583
|
+
- ~200K lines across all packages
|
|
584
|
+
|
|
585
|
+
**Suitable for:**
|
|
586
|
+
|
|
587
|
+
- Full-stack applications
|
|
588
|
+
- Microservices
|
|
589
|
+
- Cloud deployments (any runtime)
|
|
590
|
+
- Production grade requirements
|
|
591
|
+
|
|
592
|
+
---
|
|
593
|
+
|
|
594
|
+
## 8. Specific Strengths & Weaknesses
|
|
595
|
+
|
|
596
|
+
### Where elevate-ts Wins
|
|
597
|
+
|
|
598
|
+
1. **Simplicity.** Learning curve is hours, not weeks. No magical generators, no three-type-parameter monads.
|
|
599
|
+
2. **Bundle size.** [YES] ~3KB minified for all core modules. Perfect for Cloudflare Workers.
|
|
600
|
+
3. **Performance.** No fiber scheduling, no runtime allocations for trivial effects.
|
|
601
|
+
4. **Clarity.** Code intent is immediately obvious; no hidden abstractions.
|
|
602
|
+
5. **Cloudflare Workers.** Explicitly designed for this; no Node.js baggage.
|
|
603
|
+
6. **Teaching.** Perfect for learning functional programming without getting lost in production complexity.
|
|
604
|
+
|
|
605
|
+
### Where elevate-ts Struggles
|
|
606
|
+
|
|
607
|
+
1. **Dependency injection.** Reader is fine for simple cases but lacks resource safety; no memoization.
|
|
608
|
+
2. **Concurrent error handling.** Multiple independent failures must be manually coordinated.
|
|
609
|
+
3. **Error diagnostics.** No structured error model; stack traces are lost; timeout/cancellation look the same as normal failures.
|
|
610
|
+
4. **Observability.** No built-in logging, tracing, or metrics integration.
|
|
611
|
+
5. **Ecosystem.** No HTTP server, database, or CLI support; users must roll their own or import unrelated libraries.
|
|
612
|
+
6. **Scaling.** Not designed for applications with 20+ services; the manual dependency threading gets unwieldy.
|
|
613
|
+
|
|
614
|
+
---
|
|
615
|
+
|
|
616
|
+
### Where Effect-TS Wins
|
|
617
|
+
|
|
618
|
+
1. **Type safety in dependencies.** R parameter in Effect prevents accidentally running code without required services.
|
|
619
|
+
2. **Resource management.** Layer handles acquisition, cleanup, error recovery automatically; nearly impossible to leak resources.
|
|
620
|
+
3. **Error diagnostics.** Cause model preserves stack traces, distinguishes failures types, captures cancellation.
|
|
621
|
+
4. **Concurrency.** First-class structured concurrency; `Effect.all`, `Effect.race` handle multiple failures correctly.
|
|
622
|
+
5. **Observability.** OpenTelemetry integration, structured logging, built-in tracing.
|
|
623
|
+
6. **Ecosystem.** SQL, HTTP, CLI, RPC, workflows all use the same effect type; no impedance mismatch between libraries.
|
|
624
|
+
7. **Scaling.** Designed for applications with dozens of services; the type system scales with complexity.
|
|
625
|
+
8. **Schema-driven development.** Type and validator are one; eliminates a large class of bugs.
|
|
626
|
+
|
|
627
|
+
### Where Effect-TS Struggles
|
|
628
|
+
|
|
629
|
+
1. **Learning curve.** Three-month ramp-up for a team unfamiliar with effect systems. Generators confuse beginners.
|
|
630
|
+
2. **Bundle size.** Even with tree-shaking, [NOTE] ~50KB for core + Schema + Platform abstractions.
|
|
631
|
+
3. **Cloudflare Workers.** Runtime overhead and bundle size make it suboptimal for edge workers with strict latency/size constraints.
|
|
632
|
+
4. **Simple cases.** Writing "hello world" or a small form validator requires more boilerplate than plain TypeScript.
|
|
633
|
+
5. **Operational overhead.** Fiber scheduling, runtime management, executor configuration—teams need expertise to debug.
|
|
634
|
+
6. **Library size.** 200+ modules means maintenance burden; new major versions can break many downstream projects.
|
|
635
|
+
|
|
636
|
+
---
|
|
637
|
+
|
|
638
|
+
## 9. Runtime Agnosticism in Practice
|
|
639
|
+
|
|
640
|
+
### elevate-ts
|
|
641
|
+
|
|
642
|
+
```typescript
|
|
643
|
+
// Works everywhere: browser, Worker, Bun
|
|
644
|
+
import { pipe, Either, Just } from '@zambit/elevate-ts';
|
|
645
|
+
|
|
646
|
+
const validate = (input: string): Either<string, number> => {
|
|
647
|
+
try {
|
|
648
|
+
return Either.Right(parseInt(input, 10));
|
|
649
|
+
} catch (e) {
|
|
650
|
+
return Either.Left('Invalid number');
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
const result = pipe(
|
|
655
|
+
Just('42'),
|
|
656
|
+
Maybe.chain((s) => Either.toMaybe(validate(s)))
|
|
657
|
+
);
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
**Reality:** Works because it doesn't rely on ANY platform APIs. It's pure computation.
|
|
661
|
+
|
|
662
|
+
---
|
|
663
|
+
|
|
664
|
+
### Effect-TS
|
|
665
|
+
|
|
666
|
+
```typescript
|
|
667
|
+
// Browser: works with @effect/platform-browser
|
|
668
|
+
import { Effect } from 'effect';
|
|
669
|
+
import { HttpClient } from '@effect/platform-browser';
|
|
670
|
+
|
|
671
|
+
const fetchUser = (id: string): Effect<User, Error, HttpClient> =>
|
|
672
|
+
Effect.gen(function* () {
|
|
673
|
+
const client = yield* HttpClient.HttpClient;
|
|
674
|
+
const response = yield* client.get(`/api/users/${id}`);
|
|
675
|
+
return response.json;
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
// Cloudflare Worker: would need @effect/platform-worker (doesn't exist; would use @effect/platform-browser)
|
|
679
|
+
// Node.js: would use @effect/platform-node
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
**Reality:** Core Effect is runtime-agnostic, but platform-specific modules are required for I/O. Developers must know which platform they're targeting and provide the correct Layer.
|
|
683
|
+
|
|
684
|
+
---
|
|
685
|
+
|
|
686
|
+
## 10. Verdict: When to Choose Each
|
|
687
|
+
|
|
688
|
+
### Choose elevate-ts
|
|
689
|
+
|
|
690
|
+
1. **Targeting Cloudflare Workers** and you need FP without bundle bloat
|
|
691
|
+
2. **Teaching FP** to JavaScript developers
|
|
692
|
+
3. **Simple data transformation** (parsing, validation, filtering arrays)
|
|
693
|
+
4. **Single developer or small team** with limited FP experience
|
|
694
|
+
5. **No dependency injection** or simple environment threading suffices
|
|
695
|
+
6. **Offline-first or client-side** app with no backend integration
|
|
696
|
+
7. **Bundle size is critical** ([NOTE] <10KB constraint)
|
|
697
|
+
|
|
698
|
+
**Example Use Case:** [NOTE] Cloudflare Worker: receive JSON, parse with Either, transform with pipe, return JSON. No dependencies, no concurrent operations, no resource cleanup.
|
|
699
|
+
|
|
700
|
+
---
|
|
701
|
+
|
|
702
|
+
### Choose Effect-TS
|
|
703
|
+
|
|
704
|
+
1. **Full-stack application** with database, HTTP, authentication
|
|
705
|
+
2. **Microservices** with strong dependency management requirements
|
|
706
|
+
3. **Concurrent operations** (parallel jobs, pub/sub, worker pools)
|
|
707
|
+
4. **Production observability** is non-negotiable (tracing, metrics, logs)
|
|
708
|
+
5. **Large codebase** (20+ services, 50+ effects) where type safety prevents bugs
|
|
709
|
+
6. **Team is FP-experienced** or willing to invest in training
|
|
710
|
+
7. **Long-term maintenance** is a priority (the type system catches breakages)
|
|
711
|
+
|
|
712
|
+
**Example Use Case:** [NOTE] Full-stack app: Effect handles HTTP server, DB queries, authentication, logging, error recovery all in one type system. A backend refactoring automatically identifies
|
|
713
|
+
where services need to be provided; the compiler fails fast.
|
|
714
|
+
|
|
715
|
+
---
|
|
716
|
+
|
|
717
|
+
### Hybrid Approach
|
|
718
|
+
|
|
719
|
+
Some projects use **both**:
|
|
720
|
+
|
|
721
|
+
```typescript
|
|
722
|
+
// Cloudflare Worker (elevate-ts): receives request, validates input
|
|
723
|
+
import { pipe, Either } from '@zambit/elevate-ts'
|
|
724
|
+
|
|
725
|
+
const validateRequest = (req: Request): Either<string, Payload> => { ... }
|
|
726
|
+
|
|
727
|
+
// Calls backend (Effect-TS): handles orchestration, observability
|
|
728
|
+
// Backend is a full-stack Effect app; Worker is a lightweight gatekeeper
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
**Rationale:**
|
|
732
|
+
|
|
733
|
+
- Worker stays small and fast (elevate-ts)
|
|
734
|
+
- Backend handles complexity (Effect-TS)
|
|
735
|
+
- Clear separation of concerns
|
|
736
|
+
|
|
737
|
+
---
|
|
738
|
+
|
|
739
|
+
## 11. Critical Design Flaws & Observations
|
|
740
|
+
|
|
741
|
+
### elevate-ts Flaws
|
|
742
|
+
|
|
743
|
+
1. **Reader has no resource semantics.** If a Reader acquires a resource (database, connection pool), cleanup is manual. This is fundamentally unsafe for production code.
|
|
744
|
+
|
|
745
|
+
2. **No error context preservation.** Converting exceptions to strings (`onError: (e) => String(e)`) loses the original error type. Downstream handlers cannot discriminate the error.
|
|
746
|
+
|
|
747
|
+
3. **Validation monad is not a schema.** It collects errors, but there's no automatic type derivation, no serialization support, no ecosystem integration. It's a solution looking for a problem.
|
|
748
|
+
|
|
749
|
+
4. **No way to model dependencies in the type.** A Reader<{ db: Database; cache: Cache }, Result> has no compile-time check that the dependencies are provided. This is a type safety gap.
|
|
750
|
+
|
|
751
|
+
---
|
|
752
|
+
|
|
753
|
+
### Effect-TS Flaws
|
|
754
|
+
|
|
755
|
+
1. **Cause is too complex for simple cases.** A single validation error is still `Failure(Cause<ValidationError>)`. The mental model overhead is unjustified for "is this number valid?"
|
|
756
|
+
|
|
757
|
+
2. **Generator syntax is a footgun.** `Effect.gen` looks imperative but is actually monadic; the evaluation order is non-obvious. Stack traces from generators are useless for debugging.
|
|
758
|
+
|
|
759
|
+
3. **Layer composition errors are opaque.** Errors from nested dependency chains lack clear traces.
|
|
760
|
+
|
|
761
|
+
4. **No "async/await"-style Layer syntax.** Writing layers requires understanding monad laws; `Layer.fromEffect`, `Layer.provide`, `Layer.compose` are terse but require deep knowledge.
|
|
762
|
+
|
|
763
|
+
5. **Platform abstractions leak.** FileSystem is abstract; concrete implementations have different semantics (e.g., browsers don't support absolute paths).
|
|
764
|
+
|
|
765
|
+
---
|
|
766
|
+
|
|
767
|
+
## 12. Overall Assessment
|
|
768
|
+
|
|
769
|
+
### elevate-ts Niche, Excellent Tool
|
|
770
|
+
|
|
771
|
+
- Solves **one problem exceptionally well:** lightweight FP for Cloudflare Workers and browsers
|
|
772
|
+
- **Not intended as general-purpose framework** (design explicitly optimizes for edge workers)
|
|
773
|
+
- **Risk:** Backend development will hit resource safety and dependency management ceiling
|
|
774
|
+
- **Sweet spot:** Projects <100 effects, no backend, or client-side validation layer
|
|
775
|
+
|
|
776
|
+
[YES] A+ for intended scope; [NO] C- if used beyond it
|
|
777
|
+
|
|
778
|
+
---
|
|
779
|
+
|
|
780
|
+
### Effect-TS Comprehensive, but Heavyweight
|
|
781
|
+
|
|
782
|
+
- Solves **the complete stack:** dependencies, concurrency, errors, observability, serialization
|
|
783
|
+
- **Uncompromising about correctness:** Type system forces proper error/service/resource handling
|
|
784
|
+
- **Cost:** Learning curve, bundle size, operational complexity
|
|
785
|
+
- **Risk:** Easy to over-engineer simple problems; teams often cargo-cult patterns
|
|
786
|
+
- **Sweet spot:** Distributed systems, long-lived services, FP-experienced teams
|
|
787
|
+
|
|
788
|
+
[YES] A- for production systems; [NO] D for "Hello World"
|
|
789
|
+
|
|
790
|
+
---
|
|
791
|
+
|
|
792
|
+
## 13. Final Thoughts
|
|
793
|
+
|
|
794
|
+
**elevate-ts is not trying to be Effect-TS, and that's okay.**
|
|
795
|
+
|
|
796
|
+
elevate-ts is **intentionally minimalist.** It provides core monads, assumes vanilla TypeScript for I/O, and optimizes for bundle size. It's a **library**, not a framework.
|
|
797
|
+
|
|
798
|
+
Effect-TS is **intentionally maximalist.** It provides everything: scheduling, resources, DI, observability, schema validation. It's a **framework** designed as an application foundation.
|
|
799
|
+
|
|
800
|
+
**When in doubt:**
|
|
801
|
+
|
|
802
|
+
- If building a **Cloudflare Worker** or **client-side validation**, choose elevate-ts.
|
|
803
|
+
- If building a **backend service or distributed system**, choose Effect-TS.
|
|
804
|
+
- If building **both**, use both—they complement each other.
|
|
805
|
+
|
|
806
|
+
The real gap in the TypeScript ecosystem is **middle ground:** something lighter than Effect-TS but with better resource safety than elevate-ts' Reader. As of 2026-04, that gap remains unfilled.
|