An educational pure functional programming library in TypeScript
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add documentation for Validation and Type Classes

+1694 -5
+399
docs/guides/concepts/00-why-these-strange-names.md
··· 1 + # Why These Strange Names? 2 + 3 + If you've encountered terms like "Monad" or "Functor" and felt intimidated, you're not alone. This article demystifies all the FP terminology used in purus-ts before you dive into the concepts. 4 + 5 + --- 6 + 7 + ## Where Do These Names Come From? 8 + 9 + FP terminology comes from several sources: 10 + 11 + - **Category theory** - Abstract mathematics from the 1940s (Functor, Monad, Applicative) 12 + - **Type theory** - Formal logic and programming language theory (Option, Refinement) 13 + - **Haskell community** - The language that popularized FP patterns (Either, Maybe) 14 + - **Metaphors** - Descriptive names for patterns (Railway, Fiber, Effect) 15 + 16 + The names weren't chosen to be scary - they're precise in their original contexts. Think of them as historical artifacts, like how we still say "dialing" a phone. 17 + 18 + **The good news:** You don't need to understand category theory to use these patterns. The concepts are simpler than the names suggest. 19 + 20 + --- 21 + 22 + ## Quick Reference: All purus-ts Terms 23 + 24 + ### Data Types 25 + 26 + | Term | Plain English | Also Called | 27 + |------------|-------------------------------------------------------|-----------------------| 28 + | Result | A value that's either success or failure | Either (Haskell) | 29 + | Ok | The success case of Result | Right | 30 + | Err | The failure case of Result | Left | 31 + | Option | A value that might not exist | Maybe (Haskell) | 32 + | Some | The "value exists" case of Option | Just | 33 + | None | The "no value" case of Option | Nothing | 34 + | Validation | Like Result, but collects all errors | Validated | 35 + | Valid | The success case of Validation | - | 36 + | Invalid | The failure case (with error array) | - | 37 + 38 + ### Effect System 39 + 40 + | Term | Plain English | Also Called | 41 + |-------------|------------------------------------------------------|-----------------------| 42 + | Effect/Eff | A description of a computation (not the computation) | IO, Task, ZIO | 43 + | Fiber | A lightweight thread that can be cancelled | Green thread | 44 + | Fork | Start a fiber running in the background | Spawn | 45 + | Join | Wait for a fiber to complete | Await | 46 + | Race | Run two effects, take whichever finishes first | - | 47 + | All | Run effects in parallel, collect all results | Parallel | 48 + | Environment | Dependencies a computation needs (the R in Eff) | Context, Reader | 49 + 50 + ### Operations 51 + 52 + | Term | Plain English | Also Called | 53 + |-------------|------------------------------------------------------|-----------------------| 54 + | map | Transform the value inside a container | fmap | 55 + | flatMap | Chain operations that return containers | bind, chain, >>= | 56 + | fold | Handle both success and failure cases | match, cata | 57 + | pipe | Pass a value through a series of functions | \|> | 58 + | flow | Compose functions left-to-right | >>> | 59 + | traverse | Map + flip container nesting | - | 60 + | sequence | Flip container nesting (traverse with identity) | - | 61 + 62 + ### Type Class Patterns 63 + 64 + | Term | Plain English | Also Called | 65 + |-------------|------------------------------------------------------|-----------------------| 66 + | Functor | Anything you can map over | Mappable | 67 + | Applicative | Combine independent wrapped values | - | 68 + | Monad | Chain dependent wrapped operations | Flatmappable | 69 + | Bifunctor | Map over either of two type parameters | - | 70 + 71 + ### Type Safety Patterns 72 + 73 + | Term | Plain English | Also Called | 74 + |---------------|----------------------------------------------------|-----------------------| 75 + | Branded Type | A primitive with a compile-time "tag" | Newtype, Opaque type | 76 + | Refinement | A type that guarantees a property (e.g., Positive) | Refined type | 77 + | Typestate | Encoding state machine states as types | Phantom types | 78 + 79 + --- 80 + 81 + ## Real-World Analogies 82 + 83 + ### Result = Shipping Package 84 + 85 + A package either arrives safely (Ok) or gets damaged/lost (Err). You don't know which until you "open" it, but the tracking system (type system) tells you both are possible. 86 + 87 + ```typescript 88 + const fetchUser = (id: string): Result<User, ApiError> => ... 89 + // The return type TELLS you: this might fail with ApiError 90 + ``` 91 + 92 + **Why not just throw?** Thrown exceptions don't appear in the type signature. `Result` makes failure visible. 93 + 94 + --- 95 + 96 + ### Option = Schrodinger's Box 97 + 98 + The box either contains a cat (Some) or is empty (None). You must handle both possibilities - you can't just assume there's a cat. 99 + 100 + ```typescript 101 + const findUser = (id: string): Option<User> => ... 102 + // Might return a user, might not - the type forces you to handle both 103 + ``` 104 + 105 + **Why not just use `null`?** TypeScript's `null` is easy to forget. `Option` forces you to handle the empty case. 106 + 107 + --- 108 + 109 + ### Validation = Report Card 110 + 111 + A report card shows ALL your grades, not just the first failing one. If you failed Math, English, and Science, you see all three - not just "Failed Math, try again." 112 + 113 + ```typescript 114 + // Result: stops at first error 115 + validateUser("", "bad-email", -5) // Err("Name required") - that's all you see 116 + 117 + // Validation: shows everything 118 + validateUser("", "bad-email", -5) // Invalid(["Name required", "Invalid email", "Age must be positive"]) 119 + ``` 120 + 121 + --- 122 + 123 + ### Effect = Recipe Card 124 + 125 + A recipe card isn't food - it's **instructions for making food**. Similarly, an `Eff` isn't a computation - it's **a description of a computation**. 126 + 127 + ```typescript 128 + const fetchUser: Eff<User, Error, HttpClient> = ... 129 + // This doesn't fetch anything yet - it's just a recipe 130 + // The recipe says: "needs HttpClient, might fail with Error, produces User" 131 + 132 + runPromise(fetchUser) // NOW it actually runs 133 + ``` 134 + 135 + **Why not just use Promises?** Promises start immediately. Effects are lazy - you control when they run, and you can compose them before running. 136 + 137 + --- 138 + 139 + ### Fiber = Kitchen Assistant 140 + 141 + Imagine you're the head chef (main thread). You can ask an assistant (fiber) to chop vegetables while you prepare sauce. You can: 142 + 143 + - **Fork**: "Start chopping those onions" (assistant works in parallel) 144 + - **Join**: "Give me the chopped onions" (wait for assistant to finish) 145 + - **Interrupt**: "Stop! We're making pizza instead" (cancel the work) 146 + 147 + ```typescript 148 + const fiber = yield* fork(longComputation) // Start in background 149 + // ... do other work ... 150 + const result = yield* join(fiber) // Get the result when needed 151 + ``` 152 + 153 + --- 154 + 155 + ### Environment (R) = Kitchen Equipment 156 + 157 + A recipe might require specific equipment: an oven, a mixer, a blender. The `R` type parameter lists what "equipment" your effect needs. 158 + 159 + ```typescript 160 + type Eff<A, E, R> 161 + // ^ ^ ^ 162 + // | | └── R = Requirements (what it needs to run) 163 + // | └───── E = Error (what can go wrong) 164 + // └──────── A = Success value (what it produces) 165 + 166 + const fetchUser: Eff<User, ApiError, HttpClient> = ... 167 + // ^^^^^^^^^^ 168 + // Needs HttpClient to run 169 + ``` 170 + 171 + To run this effect, you must **provide** an HttpClient: 172 + 173 + ```typescript 174 + pipe( 175 + fetchUser, 176 + provide({ fetch: globalThis.fetch }) // Here's your equipment 177 + ) 178 + ``` 179 + 180 + --- 181 + 182 + ### pipe = Assembly Line 183 + 184 + Products move through stations, each transforming them: 185 + 186 + ``` 187 + Raw Material → [Cut] → [Shape] → [Paint] → [Package] → Finished Product 188 + ``` 189 + 190 + ```typescript 191 + pipe( 192 + rawData, // Raw material 193 + parse, // Cut 194 + validate, // Shape 195 + transform, // Paint 196 + format // Package 197 + ) 198 + ``` 199 + 200 + **Why not just nest function calls?** `format(transform(validate(parse(rawData))))` reads inside-out. `pipe` reads left-to-right, matching how you think about the steps. 201 + 202 + --- 203 + 204 + ### flatMap = Choose Your Own Adventure 205 + 206 + In a "choose your own adventure" book, each choice leads to a new page with new choices. You can't skip ahead - you must make choice 1 before you see the options for choice 2. 207 + 208 + ```typescript 209 + pipe( 210 + chooseDoor(), // "You chose door 2..." 211 + flatMap(room => exploreRoom(room)), // "In the room, you find..." 212 + flatMap(item => useItem(item)) // "Using the key, you..." 213 + ) 214 + ``` 215 + 216 + Each step depends on the previous result. That's what makes it "flat" - it chains without nesting. 217 + 218 + --- 219 + 220 + ### Functor = Gift Wrapping 221 + 222 + You can X-ray a wrapped gift and transform what you see without unwrapping it. The wrapping stays intact. 223 + 224 + ```typescript 225 + pipe( 226 + ok(42), // Gift box containing 42 227 + mapResult(n => n.toString()) // X-ray transforms it to "42" 228 + ) 229 + // Still wrapped in Ok, now containing "42" 230 + ``` 231 + 232 + --- 233 + 234 + ### Applicative = IKEA Furniture Kit 235 + 236 + Separate boxes containing: (1) instructions, (2) panels, (3) screws. Open them in any order, combine at the end. If any box is missing parts, you find out immediately (not after opening box 1, then 2...). 237 + 238 + ```typescript 239 + pipe( 240 + valid(makeUser), 241 + apValidation(validateName(name)), // Box A 242 + apValidation(validateEmail(email)), // Box B 243 + apValidation(validateAge(age)) // Box C 244 + ) 245 + // All boxes checked; all errors reported at once 246 + ``` 247 + 248 + --- 249 + 250 + ### Monad = Cooking Recipe 251 + 252 + Sequential steps where each depends on the previous: 253 + - Can't frost before baking 254 + - Can't bake before mixing 255 + - Can't mix before measuring 256 + 257 + ```typescript 258 + pipe( 259 + measureIngredients(), 260 + flatMap(ingredients => mixBatter(ingredients)), 261 + flatMap(batter => bake(batter)), 262 + flatMap(cake => frost(cake)) 263 + ) 264 + ``` 265 + 266 + --- 267 + 268 + ### Railway Oriented Programming = Train Tracks 269 + 270 + Happy path is one track. Errors switch you to a parallel error track. Once on the error track, you stay there. 271 + 272 + ``` 273 + ┌─────────┐ ┌─────────┐ ┌─────────┐ 274 + Success ──▶│ Step 1 │──◆──│ Step 2 │──◆──│ Step 3 │──▶ Ok 275 + └─────────┘ │ └─────────┘ │ └─────────┘ 276 + │ │ 277 + ▼ ▼ 278 + Error ───────────────────────────────────────────────▶ Err 279 + ``` 280 + 281 + ```typescript 282 + pipe( 283 + step1(), // Might fail 284 + chainResult(step2), // Only runs if step1 succeeded 285 + chainResult(step3) // Only runs if step2 succeeded 286 + ) 287 + // If any step fails, you get that error; later steps don't run 288 + ``` 289 + 290 + --- 291 + 292 + ### Branded Type = Wristband 293 + 294 + At a concert, a wristband proves you paid. The wristband is just plastic, but it **brands** you as "paid attendee." 295 + 296 + ```typescript 297 + type UserId = string & { readonly __brand: "UserId" } 298 + type OrderId = string & { readonly __brand: "OrderId" } 299 + 300 + // Both are strings at runtime, but TypeScript won't let you mix them 301 + const processOrder = (orderId: OrderId, userId: UserId) => ... 302 + 303 + processOrder(userId, orderId) // ERROR! Wrong order 304 + ``` 305 + 306 + --- 307 + 308 + ### Typestate = Boarding Pass Stages 309 + 310 + A boarding pass goes through stages: Booked → CheckedIn → Boarded. You can't board without checking in first - the type system enforces this. 311 + 312 + ```typescript 313 + type Ticket<Status> = { id: string; _status: Status } 314 + 315 + const checkIn = (ticket: Ticket<"Booked">): Ticket<"CheckedIn"> => ... 316 + const board = (ticket: Ticket<"CheckedIn">): Ticket<"Boarded"> => ... 317 + 318 + board(bookedTicket) // ERROR! Must check in first 319 + ``` 320 + 321 + --- 322 + 323 + ## The Key Distinctions 324 + 325 + ### Monad vs Applicative 326 + 327 + | Question | Answer | Pattern | 328 + |---------------------------------------|-------------|-------------| 329 + | Does step 2 need step 1's result? | Yes | Monad | 330 + | Are the steps independent? | Yes | Applicative | 331 + | Want to stop on first error? | Yes | Monad | 332 + | Want to collect all errors? | Yes | Applicative | 333 + 334 + ### Result vs Validation 335 + 336 + | Question | Answer | Type | 337 + |---------------------------------------|-------------|-------------| 338 + | Steps depend on each other? | Yes | Result | 339 + | Steps are independent validations? | Yes | Validation | 340 + | Show first error only? | Yes | Result | 341 + | Show all errors? | Yes | Validation | 342 + 343 + ### Effect vs Promise 344 + 345 + | Question | Answer | Type | 346 + |---------------------------------------|-------------|-------------| 347 + | Should it start immediately? | Yes | Promise | 348 + | Should I control when it starts? | Yes | Effect | 349 + | Need cancellation? | Yes | Effect | 350 + | Need typed errors? | Yes | Effect | 351 + | Need dependency injection? | Yes | Effect | 352 + 353 + --- 354 + 355 + ## Etymology (For the Curious) 356 + 357 + | Term | Origin | 358 + |-------------|---------------------------------------------------------------------------| 359 + | Functor | Latin *functio* ("performance"). Math: mapping between categories. | 360 + | Applicative | You "apply" wrapped functions to wrapped values. | 361 + | Monad | Greek *monas* ("unit"). Leibniz used it for fundamental units of reality. | 362 + | Bifunctor | Latin *bi* ("two") + functor. | 363 + | Option | The value is "optional" - might not exist. | 364 + | Result | The "result" of an operation that might fail. | 365 + | Effect | Describes a side "effect" (IO, state change, etc.). | 366 + | Fiber | Thin threads; lighter than OS threads. | 367 + | Traverse | Latin *traversare* ("to cross over"). Crosses container boundaries. | 368 + 369 + --- 370 + 371 + ## The Takeaway 372 + 373 + Every scary FP term maps to a simple idea: 374 + 375 + | Scary Name | Simple Idea | 376 + |--------------|------------------------------------------------| 377 + | Functor | Transform inside a container | 378 + | Applicative | Combine independent containers | 379 + | Monad | Chain dependent operations | 380 + | Result | Success or failure (not exceptions) | 381 + | Option | Value or nothing (not null) | 382 + | Validation | Collect all errors (not just first) | 383 + | Effect | Recipe for computation (not the computation) | 384 + | Fiber | Cancellable background task | 385 + | pipe | Pass value through functions left-to-right | 386 + | flatMap | Chain operations that return containers | 387 + 388 + Once you use these patterns a few times, they become second nature - like "downloading" once felt like jargon but now feels obvious. 389 + 390 + --- 391 + 392 + ## What's Next? 393 + 394 + Now that you know what the names mean, dive into the details: 395 + 396 + - [Why Errors as Values?](./01-errors-as-values.md) - The foundation: Result type 397 + - [Branded Types In Depth](./02-branded-types.md) - Compile-time type safety 398 + - [Validation and Error Accumulation](./03-validation-and-error-accumulation.md) - Applicative in action 399 + - [Type Classes in TypeScript](./04-type-classes-in-typescript.md) - Functor, Applicative, Monad patterns
+556
docs/guides/concepts/03-validation-and-error-accumulation.md
··· 1 + # Validation and Error Accumulation 2 + 3 + This article explains why Result's short-circuit behavior isn't always what you want, and how the Validation type accumulates all errors instead of stopping at the first. 4 + 5 + --- 6 + 7 + ## The Short-Circuit Problem 8 + 9 + Consider a user registration form with three fields: name, email, and age. Using Result for validation: 10 + 11 + ```typescript 12 + import { type Result, ok, err, chainResult, pipe } from "purus-ts" 13 + 14 + type ValidationError = string 15 + 16 + const validateName = (name: string): Result<string, ValidationError> => 17 + name.length > 0 ? ok(name) : err("Name is required") 18 + 19 + const validateEmail = (email: string): Result<string, ValidationError> => 20 + email.includes("@") ? ok(email) : err("Invalid email format") 21 + 22 + const validateAge = (age: number): Result<number, ValidationError> => 23 + age >= 18 ? ok(age) : err("Must be 18 or older") 24 + ``` 25 + 26 + Now chain them together: 27 + 28 + ```typescript 29 + type User = { name: string; email: string; age: number } 30 + 31 + const validateUser = ( 32 + name: string, 33 + email: string, 34 + age: number 35 + ): Result<User, ValidationError> => 36 + pipe( 37 + validateName(name), 38 + chainResult(validName => 39 + pipe( 40 + validateEmail(email), 41 + chainResult(validEmail => 42 + pipe( 43 + validateAge(age), 44 + chainResult(validAge => ok({ name: validName, email: validEmail, age: validAge })) 45 + ) 46 + ) 47 + ) 48 + ) 49 + ) 50 + ``` 51 + 52 + What happens when a user submits invalid data? 53 + 54 + ```typescript 55 + const result = validateUser("", "not-an-email", 16) 56 + // Err("Name is required") 57 + ``` 58 + 59 + The user only sees **one error**. They fix the name, submit again: 60 + 61 + ```typescript 62 + const result = validateUser("Alice", "not-an-email", 16) 63 + // Err("Invalid email format") 64 + ``` 65 + 66 + Another single error. They fix the email, submit again: 67 + 68 + ```typescript 69 + const result = validateUser("Alice", "alice@example.com", 16) 70 + // Err("Must be 18 or older") 71 + ``` 72 + 73 + **Three submissions to find three errors.** This is frustrating UX. 74 + 75 + Here's what's happening visually: 76 + 77 + ``` 78 + Result (short-circuits): 79 + ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ 80 + ───▶│ validateName│──◆──│validateEmail│──◆──│ validateAge │───▶ Ok 81 + └─────────────┘ │ └─────────────┘ │ └─────────────┘ 82 + │ │ 83 + ▼ ▼ 84 + Err(e1) (never reached) 85 + ``` 86 + 87 + When the first check fails, execution stops. Subsequent validations never run. 88 + 89 + --- 90 + 91 + ## Monadic vs Applicative Composition 92 + 93 + Why does Result short-circuit? Because it's **monadic**. 94 + 95 + ### Monadic: Steps Depend on Previous Results 96 + 97 + `chainResult` (also called `flatMap`) has this signature: 98 + 99 + ```typescript 100 + chainResult: <T, U, E>(f: (t: T) => Result<U, E>) => (result: Result<T, E>) => Result<U, E> 101 + ``` 102 + 103 + The function `f` takes the **value from the previous step**. If there's no value (because the previous step failed), `f` can't run. 104 + 105 + This is correct for dependent operations: 106 + 107 + ```typescript 108 + // Can't fetch user's orders without user ID 109 + pipe( 110 + fetchUser(userId), 111 + chainResult(user => fetchOrders(user.id)) // Needs user.id 112 + ) 113 + ``` 114 + 115 + ### Applicative: Steps Are Independent 116 + 117 + But form fields are **independent**. The name validation doesn't need the email to run. The age validation doesn't need the name. 118 + 119 + ```typescript 120 + // These don't depend on each other 121 + validateName("Alice") // Independent 122 + validateEmail("a@b.c") // Independent 123 + validateAge(25) // Independent 124 + ``` 125 + 126 + Applicative composition runs all operations regardless of previous failures, collecting the results. 127 + 128 + --- 129 + 130 + ## Introducing Validation<A, E> 131 + 132 + purus provides `Validation<A, E>` for error accumulation: 133 + 134 + ```typescript 135 + import { 136 + type Validation, 137 + valid, 138 + invalid, 139 + invalidOne, 140 + isValid, 141 + isInvalid 142 + } from "purus-ts" 143 + ``` 144 + 145 + ### Type Definition 146 + 147 + ```typescript 148 + type Valid<A> = { readonly _tag: "Valid"; readonly value: A } 149 + type Invalid<E> = { readonly _tag: "Invalid"; readonly errors: readonly E[] } 150 + 151 + type Validation<A, E> = Valid<A> | Invalid<E> 152 + ``` 153 + 154 + Notice that `Invalid` contains an **array** of errors, not a single error. 155 + 156 + ### Constructors 157 + 158 + ```typescript 159 + // Create a valid result 160 + const v1 = valid(42) 161 + // Type: Valid<number> 162 + 163 + // Create an invalid result with multiple errors 164 + const v2 = invalid(["Error 1", "Error 2"]) 165 + // Type: Invalid<string> 166 + 167 + // Create an invalid result with a single error (convenience) 168 + const v3 = invalidOne("Error 1") 169 + // Type: Invalid<string> 170 + ``` 171 + 172 + ### Type Guards 173 + 174 + ```typescript 175 + const result: Validation<number, string> = valid(42) 176 + 177 + if (isValid(result)) { 178 + console.log(result.value) // 42 179 + } 180 + 181 + if (isInvalid(result)) { 182 + console.log(result.errors) // string[] 183 + } 184 + ``` 185 + 186 + ### Comparison with Result 187 + 188 + | Aspect | Result<T, E> | Validation<A, E> | 189 + |----------------|------------------------|----------------------------| 190 + | Error storage | Single error | Array of errors | 191 + | Composition | Monadic (short-circuit)| Applicative (accumulate) | 192 + | Use case | Dependent operations | Independent validations | 193 + | Error type | `{ error: E }` | `{ errors: readonly E[] }` | 194 + 195 + --- 196 + 197 + ## apValidation - The Error Accumulator 198 + 199 + The key to error accumulation is `apValidation`: 200 + 201 + ```typescript 202 + import { apValidation, valid } from "purus-ts" 203 + ``` 204 + 205 + ### How It Works 206 + 207 + `apValidation` applies a validation containing a function to a validation containing a value: 208 + 209 + ```typescript 210 + apValidation: <A, B, E>( 211 + va: Validation<A, E> 212 + ) => ( 213 + vf: Validation<(a: A) => B, E> 214 + ) => Validation<B, E> 215 + ``` 216 + 217 + ### Four Cases Truth Table 218 + 219 + | Function | Value | Result | 220 + |------------------|------------------|-------------------------------| 221 + | Valid(f) | Valid(a) | Valid(f(a)) | 222 + | Valid(f) | Invalid([e]) | Invalid([e]) | 223 + | Invalid([e1]) | Valid(a) | Invalid([e1]) | 224 + | Invalid([e1]) | Invalid([e2]) | Invalid([e1, e2]) ← **accumulates!** | 225 + 226 + When **both** are invalid, errors are **concatenated**. 227 + 228 + ### The Curried Function Pattern 229 + 230 + To combine multiple validations, start with a curried constructor: 231 + 232 + ```typescript 233 + type User = { name: string; email: string; age: number } 234 + 235 + // Curried constructor 236 + const makeUser = (name: string) => (email: string) => (age: number): User => 237 + ({ name, email, age }) 238 + ``` 239 + 240 + Then apply each validation: 241 + 242 + ```typescript 243 + import { pipe, valid, apValidation } from "purus-ts" 244 + 245 + const validateAndMakeUser = ( 246 + name: string, 247 + email: string, 248 + age: number 249 + ): Validation<User, string> => 250 + pipe( 251 + valid(makeUser), // Validation<(n) => (e) => (a) => User> 252 + apValidation(validateName(name)), // Validation<(e) => (a) => User> 253 + apValidation(validateEmail(email)), // Validation<(a) => User> 254 + apValidation(validateAge(age)) // Validation<User> 255 + ) 256 + ``` 257 + 258 + Here's the flow visually: 259 + 260 + ``` 261 + valid(makeUser) validateName("") validateAge(-1) 262 + │ │ │ 263 + ▼ ▼ ▼ 264 + ┌───────────┐ ┌───────────┐ ┌───────────┐ 265 + │ (n)(e)(a) │────ap────▶│ Invalid │────ap─────▶│ Invalid │ 266 + │ => {} │ │ ["Name"] │ │ ["Name", │ 267 + └───────────┘ └───────────┘ │ "Age"] │ 268 + └───────────┘ 269 + ``` 270 + 271 + Each `apValidation` either: 272 + - Applies the function (if both valid) 273 + - Passes through the error (if one invalid) 274 + - Merges the errors (if both invalid) 275 + 276 + ### Complete Validation Example 277 + 278 + Now our validators return `Validation`: 279 + 280 + ```typescript 281 + import { type Validation, valid, invalidOne, apValidation, pipe } from "purus-ts" 282 + 283 + const validateName = (name: string): Validation<string, string> => 284 + name.length > 0 ? valid(name) : invalidOne("Name is required") 285 + 286 + const validateEmail = (email: string): Validation<string, string> => 287 + email.includes("@") ? valid(email) : invalidOne("Invalid email format") 288 + 289 + const validateAge = (age: number): Validation<number, string> => 290 + age >= 18 ? valid(age) : invalidOne("Must be 18 or older") 291 + 292 + const makeUser = (name: string) => (email: string) => (age: number) => 293 + ({ name, email, age }) 294 + 295 + const validateUser = (name: string, email: string, age: number) => 296 + pipe( 297 + valid(makeUser), 298 + apValidation(validateName(name)), 299 + apValidation(validateEmail(email)), 300 + apValidation(validateAge(age)) 301 + ) 302 + 303 + // All errors at once! 304 + const result = validateUser("", "not-an-email", 16) 305 + // Invalid(["Name is required", "Invalid email format", "Must be 18 or older"]) 306 + ``` 307 + 308 + **One submission reveals all three errors.** 309 + 310 + ``` 311 + Validation (accumulates): 312 + ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ 313 + ───▶│ validateName│ │validateEmail│ │ validateAge │───▶ Valid 314 + └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ 315 + │ │ │ 316 + ▼ ▼ ▼ 317 + err? err? err? 318 + │ │ │ 319 + └───────────────────┴───────────────────┘ 320 + 321 + 322 + Invalid([e1, e2, e3]) 323 + ``` 324 + 325 + --- 326 + 327 + ## Converting Between Result and Validation 328 + 329 + Sometimes you have existing Result-based validators or need to return a Result for compatibility. 330 + 331 + ### fromResult - Result to Validation 332 + 333 + ```typescript 334 + import { fromResult, ok, err } from "purus-ts" 335 + 336 + fromResult(ok(42)) // Valid(42) 337 + fromResult(err("oops")) // Invalid(["oops"]) 338 + ``` 339 + 340 + ### toResult - Validation to Result (loses errors) 341 + 342 + ```typescript 343 + import { toResult, valid, invalid } from "purus-ts" 344 + 345 + toResult(valid(42)) // Ok(42) 346 + toResult(invalid(["err1", "err2", "err3"])) // Err("err1") - only first! 347 + ``` 348 + 349 + **Warning:** `toResult` discards all errors except the first. This is sometimes acceptable (e.g., when you only need to know "did it fail?"), but usually you want `toResultAll`. 350 + 351 + ### toResultAll - Preserves All Errors 352 + 353 + ```typescript 354 + import { toResultAll, valid, invalid } from "purus-ts" 355 + 356 + toResultAll(valid(42)) // Ok(42) 357 + toResultAll(invalid(["err1", "err2", "err3"])) // Err(["err1", "err2", "err3"]) 358 + ``` 359 + 360 + ### When to Convert 361 + 362 + | Scenario | Use | 363 + |---------------------------------|-------------------------------| 364 + | Have Result validators | `fromResult` before combining | 365 + | API expects Result | `toResult` or `toResultAll` | 366 + | Display all errors | Use `toResultAll` | 367 + | Short-circuit after collecting | `toResult` after Validation | 368 + 369 + --- 370 + 371 + ## Real-World Example: Form Validation 372 + 373 + For production code, use structured error types instead of strings: 374 + 375 + ```typescript 376 + import { 377 + type Validation, 378 + valid, 379 + invalidOne, 380 + apValidation, 381 + matchValidation, 382 + pipe 383 + } from "purus-ts" 384 + 385 + // Rich error types 386 + type ValidationError = 387 + | { _tag: "Required"; field: string } 388 + | { _tag: "TooShort"; field: string; min: number; actual: number } 389 + | { _tag: "TooLong"; field: string; max: number; actual: number } 390 + | { _tag: "InvalidFormat"; field: string; expected: string } 391 + | { _tag: "OutOfRange"; field: string; min: number; max: number; actual: number } 392 + 393 + // Validators return rich errors 394 + const validateUsername = (input: string): Validation<string, ValidationError> => { 395 + if (input.length === 0) { 396 + return invalidOne({ _tag: "Required", field: "username" }) 397 + } 398 + if (input.length < 3) { 399 + return invalidOne({ 400 + _tag: "TooShort", 401 + field: "username", 402 + min: 3, 403 + actual: input.length 404 + }) 405 + } 406 + if (input.length > 20) { 407 + return invalidOne({ 408 + _tag: "TooLong", 409 + field: "username", 410 + max: 20, 411 + actual: input.length 412 + }) 413 + } 414 + if (!/^[a-zA-Z0-9_]+$/.test(input)) { 415 + return invalidOne({ 416 + _tag: "InvalidFormat", 417 + field: "username", 418 + expected: "alphanumeric with underscores" 419 + }) 420 + } 421 + return valid(input) 422 + } 423 + 424 + const validateEmail = (input: string): Validation<string, ValidationError> => { 425 + if (input.length === 0) { 426 + return invalidOne({ _tag: "Required", field: "email" }) 427 + } 428 + if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input)) { 429 + return invalidOne({ 430 + _tag: "InvalidFormat", 431 + field: "email", 432 + expected: "valid email address" 433 + }) 434 + } 435 + return valid(input) 436 + } 437 + 438 + const validateAge = (input: number): Validation<number, ValidationError> => { 439 + if (input < 13 || input > 120) { 440 + return invalidOne({ 441 + _tag: "OutOfRange", 442 + field: "age", 443 + min: 13, 444 + max: 120, 445 + actual: input 446 + }) 447 + } 448 + return valid(input) 449 + } 450 + 451 + // Form data type 452 + type RegistrationForm = { 453 + username: string 454 + email: string 455 + age: number 456 + } 457 + 458 + // Curried constructor 459 + const makeForm = (username: string) => (email: string) => (age: number): RegistrationForm => 460 + ({ username, email, age }) 461 + 462 + // Combine all validations 463 + const validateRegistration = ( 464 + username: string, 465 + email: string, 466 + age: number 467 + ): Validation<RegistrationForm, ValidationError> => 468 + pipe( 469 + valid(makeForm), 470 + apValidation(validateUsername(username)), 471 + apValidation(validateEmail(email)), 472 + apValidation(validateAge(age)) 473 + ) 474 + 475 + // Format errors for display 476 + const formatError = (error: ValidationError): string => { 477 + switch (error._tag) { 478 + case "Required": 479 + return `${error.field} is required` 480 + case "TooShort": 481 + return `${error.field} must be at least ${error.min} characters (got ${error.actual})` 482 + case "TooLong": 483 + return `${error.field} must be at most ${error.max} characters (got ${error.actual})` 484 + case "InvalidFormat": 485 + return `${error.field} must be a ${error.expected}` 486 + case "OutOfRange": 487 + return `${error.field} must be between ${error.min} and ${error.max} (got ${error.actual})` 488 + } 489 + } 490 + 491 + // Usage 492 + const result = validateRegistration("ab", "invalid", 10) 493 + 494 + matchValidation( 495 + form => console.log("Valid registration:", form), 496 + errors => { 497 + console.log("Validation errors:") 498 + errors.forEach(e => console.log(` - ${formatError(e)}`)) 499 + } 500 + )(result) 501 + 502 + // Output: 503 + // Validation errors: 504 + // - username must be at least 3 characters (got 2) 505 + // - email must be a valid email address 506 + // - age must be between 13 and 120 (got 10) 507 + ``` 508 + 509 + ### Benefits of Structured Errors 510 + 511 + 1. **Type-safe handling** - Pattern matching ensures all error types are handled 512 + 2. **Rich context** - Each error includes relevant details for debugging or display 513 + 3. **i18n ready** - Error data can be formatted in any language 514 + 4. **Testing** - Assert specific error types, not fragile string matches 515 + 516 + --- 517 + 518 + ## When to Use Validation vs Result 519 + 520 + | Use Case | Type | Why | 521 + |--------------------------------------|------------|----------------------------------------| 522 + | Form validation | Validation | Show all errors at once | 523 + | Config file parsing | Validation | Report all missing/invalid fields | 524 + | Data import validation | Validation | List all problems in the import | 525 + | Database operation | Result | Each query depends on previous | 526 + | HTTP request chain | Result | Can't fetch orders without user | 527 + | File operations | Result | Steps are sequential and dependent | 528 + | Authentication flow | Result | Each step depends on previous | 529 + | Independent API calls | Validation | Collect all API errors | 530 + 531 + ### Rule of Thumb 532 + 533 + - **Independent operations?** → Validation (accumulate) 534 + - **Dependent operations?** → Result (short-circuit) 535 + - **Unsure?** → Start with Result, switch to Validation if users complain about one-at-a-time errors 536 + 537 + --- 538 + 539 + ## Key Takeaways 540 + 541 + 1. **Result short-circuits** - First error stops execution, others never run 542 + 2. **Validation accumulates** - All errors are collected before returning 543 + 3. **Use curried constructors** - `(a) => (b) => (c) => result` enables `apValidation` chaining 544 + 4. **`apValidation` merges errors** - When both sides are invalid, errors concatenate 545 + 5. **Convert as needed** - `fromResult`, `toResult`, `toResultAll` bridge the types 546 + 6. **Structured errors** - Use discriminated unions, not strings, for production code 547 + 548 + The choice between Result and Validation isn't about which is better - it's about whether your operations are dependent (use Result) or independent (use Validation). 549 + 550 + --- 551 + 552 + ## See Also 553 + 554 + - [Tutorial Chapter 3: Typed Errors with Result](../tutorial/03-typed-errors-with-result.md) - Result fundamentals 555 + - [Why Errors as Values?](./01-errors-as-values.md) - The philosophy behind typed errors 556 + - [Type Classes in TypeScript](./04-type-classes-in-typescript.md) - Monads vs Applicatives explained
+688
docs/guides/concepts/04-type-classes-in-typescript.md
··· 1 + # Type Classes in TypeScript 2 + 3 + This article explains functional programming type class patterns and how purus implements them within TypeScript's type system limitations. 4 + 5 + --- 6 + 7 + ## The Problem Type Classes Solve 8 + 9 + Look at these three functions: 10 + 11 + ```typescript 12 + // Transform values inside a Result 13 + const mapResult = <T, U, E>(f: (t: T) => U) => 14 + (result: Result<T, E>): Result<U, E> => 15 + result._tag === "Ok" ? ok(f(result.value)) : result 16 + 17 + // Transform values inside an Option 18 + const mapOption = <T, U>(f: (t: T) => U) => 19 + (option: Option<T>): Option<U> => 20 + option._tag === "Some" ? some(f(option.value)) : option 21 + 22 + // Transform values inside an Array 23 + const mapArray = <T, U>(f: (t: T) => U) => 24 + (arr: T[]): U[] => 25 + arr.map(f) 26 + ``` 27 + 28 + All three do the same thing: apply a function to values **inside** a container. 29 + 30 + In Haskell, you'd write one generic `map`: 31 + 32 + ```haskell 33 + class Functor f where 34 + fmap :: (a -> b) -> f a -> f b 35 + ``` 36 + 37 + Then Result, Option, and Array would all be instances of Functor, sharing the same `fmap` interface. 38 + 39 + ### Why TypeScript Can't Do This 40 + 41 + TypeScript lacks **higher-kinded types** (HKT). You can't write: 42 + 43 + ```typescript 44 + // This is NOT valid TypeScript 45 + interface Functor<F> { 46 + map: <A, B>(f: (a: A) => B) => (fa: F<A>) => F<B> 47 + } 48 + ``` 49 + 50 + The problem is `F<A>`. TypeScript doesn't support type constructors as type parameters. You can pass `number` as a type, but you can't pass `Result` (without its type arguments) as a type. 51 + 52 + ### What purus Does Instead 53 + 54 + purus provides type class functions for each concrete type: 55 + 56 + - `mapResult` for Result 57 + - `mapOption` for Option 58 + - `mapEff` for Eff 59 + 60 + The patterns are consistent, even if we can't abstract over them perfectly. 61 + 62 + --- 63 + 64 + ## What Are Higher-Kinded Types? 65 + 66 + A quick explanation for the curious. 67 + 68 + ### Types vs Type Constructors 69 + 70 + - `number` is a **type** - it's complete, you can have values of type `number` 71 + - `Result` is a **type constructor** - it needs arguments to become a type 72 + - `Result<number, string>` is a **type** - now it's complete 73 + 74 + Think of it like functions: 75 + 76 + - A value like `42` is complete 77 + - A function like `(x) => x * 2` needs an argument to produce a value 78 + - `((x) => x * 2)(21)` produces `42` 79 + 80 + Type constructors are like functions at the type level. 81 + 82 + ### The Limitation 83 + 84 + TypeScript lets you pass types as parameters: 85 + 86 + ```typescript 87 + const identity = <T>(x: T): T => x 88 + identity<number>(42) // T = number 89 + ``` 90 + 91 + But you can't pass type constructors: 92 + 93 + ```typescript 94 + // Hypothetical - NOT valid TypeScript 95 + const mapGeneric = <F, A, B>(f: (a: A) => B, fa: F<A>): F<B> => ... 96 + mapGeneric<Result, number, string>(...) // Can't pass Result like this 97 + ``` 98 + 99 + ### Workarounds Exist But Are Complex 100 + 101 + Libraries like fp-ts use encoding tricks (TypeScript's module augmentation) to simulate HKT. purus opts for simplicity: explicit functions per type, consistent patterns. 102 + 103 + > **New to FP terminology?** Read [Why These Strange Names?](./00-why-these-strange-names.md) first for plain-English explanations and real-world analogies. 104 + 105 + --- 106 + 107 + ## The Type Class Hierarchy 108 + 109 + Type classes form a hierarchy where each level adds capabilities: 110 + 111 + ``` 112 + ┌──────────┐ 113 + │ Functor │ 114 + │ map() │ 115 + └────┬─────┘ 116 + 117 + 118 + ┌─────────────┐ 119 + │ Applicative │ 120 + │ of(), ap() │ 121 + └──────┬──────┘ 122 + 123 + 124 + ┌─────────┐ 125 + │ Monad │ 126 + │flatMap()│ 127 + └─────────┘ 128 + 129 + 130 + ┌────────────┐ 131 + │ Bifunctor │ (separate hierarchy) 132 + │ bimap() │ 133 + └────────────┘ 134 + ``` 135 + 136 + Every Monad is also an Applicative, and every Applicative is also a Functor. 137 + 138 + --- 139 + 140 + ## Functor: Mapping in Context 141 + 142 + A Functor lets you transform values inside a container without changing the container's structure. 143 + 144 + ### The Operation 145 + 146 + ```typescript 147 + map: <A, B>(f: (a: A) => B) => (fa: F<A>) => F<B> 148 + ``` 149 + 150 + Apply function `f` to the value inside `F`, producing a new `F` with the transformed value. 151 + 152 + ### Examples in purus 153 + 154 + ```typescript 155 + import { ok, err, mapResult, pipe } from "purus-ts" 156 + 157 + // Result as Functor 158 + pipe( 159 + ok(10), 160 + mapResult(n => n * 2) 161 + ) // Ok(20) 162 + 163 + pipe( 164 + err("oops"), 165 + mapResult(n => n * 2) 166 + ) // Err("oops") - structure unchanged 167 + ``` 168 + 169 + ```typescript 170 + import { some, none, mapOption, pipe } from "purus-ts" 171 + 172 + // Option as Functor 173 + pipe( 174 + some(10), 175 + mapOption(n => n * 2) 176 + ) // Some(20) 177 + 178 + pipe( 179 + none, 180 + mapOption(n => n * 2) 181 + ) // None - structure unchanged 182 + ``` 183 + 184 + ### The Laws 185 + 186 + Functors must obey two laws: 187 + 188 + **Identity:** Mapping the identity function does nothing 189 + 190 + ```typescript 191 + pipe(ok(42), mapResult(x => x)) 192 + // Must equal: ok(42) 193 + ``` 194 + 195 + **Composition:** Mapping f then g equals mapping their composition 196 + 197 + ```typescript 198 + const f = (x: number) => x * 2 199 + const g = (x: number) => x + 1 200 + 201 + // These must be equal: 202 + pipe(ok(10), mapResult(f), mapResult(g)) 203 + pipe(ok(10), mapResult(x => g(f(x)))) 204 + // Both: Ok(21) 205 + ``` 206 + 207 + ### Intuition 208 + 209 + Think of `map` as reaching inside the container: 210 + 211 + - **Array**: transform each element 212 + - **Option**: transform the value if present 213 + - **Result**: transform the success value 214 + - **Promise**: transform the resolved value (via `.then`) 215 + 216 + --- 217 + 218 + ## Applicative: Independent Composition 219 + 220 + Applicative extends Functor with two operations: 221 + 222 + - `of` (also called `pure`): wrap a value in the context 223 + - `ap`: apply a wrapped function to a wrapped value 224 + 225 + ### The Operations 226 + 227 + ```typescript 228 + of: <A>(a: A) => F<A> 229 + ap: <A, B>(fa: F<A>) => (fab: F<(a: A) => B>) => F<B> 230 + ``` 231 + 232 + ### Why Applicative Matters 233 + 234 + Applicative enables combining **independent** computations. 235 + 236 + ``` 237 + Monad (sequential, dependent): 238 + ┌───┐ ┌───┐ ┌───┐ 239 + │ A │───▶│ B │───▶│ C │ B needs A's result 240 + └───┘ └─┬─┘ └───┘ C needs B's result 241 + 242 + depends 243 + 244 + Applicative (parallel, independent): 245 + ┌───┐ 246 + │ A │──┐ 247 + └───┘ │ 248 + ┌───┐ ├──▶ combine 249 + │ B │──┤ 250 + └───┘ │ 251 + ┌───┐ │ 252 + │ C │──┘ 253 + └───┘ 254 + ``` 255 + 256 + With Monad, each step depends on the previous - you can't compute B until A completes. 257 + 258 + With Applicative, A, B, and C are independent - they can run in parallel (or in Validation's case, all errors can be collected). 259 + 260 + ### apResult in purus 261 + 262 + ```typescript 263 + import { ok, err, apResult, pipe } from "purus-ts" 264 + 265 + // Apply a function in Result to a value in Result 266 + pipe( 267 + ok((x: number) => x * 2), 268 + apResult(ok(10)) 269 + ) // Ok(20) 270 + 271 + // Short-circuits on first error (Result is monadic) 272 + pipe( 273 + ok((x: number) => x * 2), 274 + apResult(err("no value")) 275 + ) // Err("no value") 276 + ``` 277 + 278 + ### apValidation - Applicative with Error Accumulation 279 + 280 + ```typescript 281 + import { valid, invalid, apValidation, pipe } from "purus-ts" 282 + 283 + // Both valid - applies function 284 + pipe( 285 + valid((x: number) => x * 2), 286 + apValidation(valid(10)) 287 + ) // Valid(20) 288 + 289 + // Both invalid - accumulates errors! 290 + pipe( 291 + invalid(["error 1"]), 292 + apValidation(invalid(["error 2"])) 293 + ) // Invalid(["error 1", "error 2"]) 294 + ``` 295 + 296 + This is the key difference: `apResult` short-circuits, `apValidation` accumulates. 297 + 298 + ### liftA2 - Lifting Binary Functions 299 + 300 + A common pattern is lifting a binary function to work with wrapped values: 301 + 302 + ```typescript 303 + import { liftA2Result, ok, err } from "purus-ts" 304 + 305 + const add = (a: number, b: number) => a + b 306 + const addResults = liftA2Result(add) 307 + 308 + addResults(ok(1), ok(2)) // Ok(3) 309 + addResults(ok(1), err("!")) // Err("!") 310 + addResults(err("!"), ok(2)) // Err("!") 311 + ``` 312 + 313 + This is equivalent to: 314 + 315 + ```typescript 316 + pipe( 317 + ok((a: number) => (b: number) => a + b), 318 + apResult(ok(1)), 319 + apResult(ok(2)) 320 + ) 321 + ``` 322 + 323 + --- 324 + 325 + ## Monad: Sequential Composition 326 + 327 + Monad extends Applicative with `flatMap` (also called `bind` or `chain`): 328 + 329 + ```typescript 330 + flatMap: <A, B>(f: (a: A) => F<B>) => (fa: F<A>) => F<B> 331 + ``` 332 + 333 + The function `f` returns a new wrapped value, and `flatMap` "flattens" the result (avoiding `F<F<B>>`). 334 + 335 + ### Why Monad Matters 336 + 337 + Monad enables **dependent** sequencing - each step can use the result of the previous step. 338 + 339 + ```typescript 340 + import { ok, err, chainResult, pipe } from "purus-ts" 341 + 342 + const fetchUser = (id: string): Result<User, Error> => ... 343 + const fetchOrders = (userId: string): Result<Order[], Error> => ... 344 + 345 + // fetchOrders depends on fetchUser's result 346 + pipe( 347 + fetchUser("123"), 348 + chainResult(user => fetchOrders(user.id)) // Uses user.id 349 + ) 350 + ``` 351 + 352 + ### The Three Monad Laws 353 + 354 + **Left Identity:** wrapping a value then flatMapping equals calling the function directly 355 + 356 + ```typescript 357 + const f = (n: number): Result<string, never> => ok(n.toString()) 358 + 359 + // These must be equal: 360 + pipe(ok(42), chainResult(f)) 361 + f(42) 362 + // Both: Ok("42") 363 + ``` 364 + 365 + **Right Identity:** flatMapping with the wrapper function does nothing 366 + 367 + ```typescript 368 + // These must be equal: 369 + pipe(ok(42), chainResult(ok)) 370 + ok(42) 371 + ``` 372 + 373 + **Associativity:** order of composition doesn't matter 374 + 375 + ```typescript 376 + const f = (n: number): Result<number, string> => ok(n * 2) 377 + const g = (n: number): Result<number, string> => ok(n + 1) 378 + 379 + // These must be equal: 380 + pipe(ok(10), chainResult(f), chainResult(g)) 381 + pipe(ok(10), chainResult(x => pipe(f(x), chainResult(g)))) 382 + // Both: Ok(21) 383 + ``` 384 + 385 + ### Result and Option as Monads 386 + 387 + ```typescript 388 + // Result Monad 389 + pipe( 390 + ok(10), 391 + chainResult(n => n > 0 ? ok(n * 2) : err("not positive")), 392 + chainResult(n => ok(n.toString())) 393 + ) // Ok("20") 394 + 395 + // Option Monad 396 + pipe( 397 + some(10), 398 + flatMapOption(n => n > 0 ? some(n * 2) : none), 399 + flatMapOption(n => some(n.toString())) 400 + ) // Some("20") 401 + ``` 402 + 403 + ### Monad vs Applicative: When to Use Each 404 + 405 + | Pattern | Use When | Example | 406 + |--------------|---------------------------------------------|----------------------------------| 407 + | Monad | Steps depend on previous results | Fetch user, then fetch their orders | 408 + | Applicative | Steps are independent | Validate name AND email AND age | 409 + 410 + --- 411 + 412 + ## Bifunctor: Mapping Both Sides 413 + 414 + Some types have two type parameters that can both be mapped. Result is the canonical example: you might want to transform the success value OR the error value. 415 + 416 + ### The Operation 417 + 418 + ```typescript 419 + bimap: <A, B, C, D>( 420 + first: (a: A) => B, 421 + second: (c: C) => D 422 + ) => (fa: F<A, C>) => F<B, D> 423 + ``` 424 + 425 + For Result, this is: 426 + 427 + ```typescript 428 + bimap: <T, U, E, F>( 429 + onOk: (t: T) => U, 430 + onErr: (e: E) => F 431 + ) => (result: Result<T, E>) => Result<U, F> 432 + ``` 433 + 434 + ### bimap in purus 435 + 436 + ```typescript 437 + import { ok, err, bimap, pipe } from "purus-ts" 438 + 439 + // Transform the Ok value 440 + pipe( 441 + ok(10), 442 + bimap( 443 + n => n * 2, 444 + e => e.toUpperCase() 445 + ) 446 + ) // Ok(20) 447 + 448 + // Transform the Err value 449 + pipe( 450 + err("oops"), 451 + bimap( 452 + n => n * 2, 453 + e => e.toUpperCase() 454 + ) 455 + ) // Err("OOPS") 456 + ``` 457 + 458 + ### Practical Use: Error Type Conversion 459 + 460 + `bimap` is useful at module boundaries where error types differ: 461 + 462 + ```typescript 463 + type DbError = { _tag: "DbError"; message: string } 464 + type ApiError = { _tag: "ApiError"; code: number; message: string } 465 + 466 + const dbResultToApiResult = <T>(result: Result<T, DbError>): Result<T, ApiError> => 467 + pipe( 468 + result, 469 + bimap( 470 + value => value, // Pass through success unchanged 471 + dbErr => ({ _tag: "ApiError", code: 500, message: dbErr.message }) 472 + ) 473 + ) 474 + ``` 475 + 476 + Or more commonly, just use `mapErr`: 477 + 478 + ```typescript 479 + pipe( 480 + dbResult, 481 + mapErr(dbErr => ({ _tag: "ApiError", code: 500, message: dbErr.message })) 482 + ) 483 + ``` 484 + 485 + ### Bifunctor Laws 486 + 487 + **Identity:** mapping both sides with identity does nothing 488 + 489 + ```typescript 490 + pipe(ok(42), bimap(x => x, e => e)) 491 + // Must equal: ok(42) 492 + ``` 493 + 494 + **Composition:** composing bimaps equals bimap of compositions 495 + 496 + ```typescript 497 + const f1 = (n: number) => n * 2 498 + const f2 = (n: number) => n + 1 499 + const g1 = (s: string) => s.toUpperCase() 500 + const g2 = (s: string) => s + "!" 501 + 502 + // These must be equal: 503 + pipe(ok(10), bimap(f1, g1), bimap(f2, g2)) 504 + pipe(ok(10), bimap(x => f2(f1(x)), e => g2(g1(e)))) 505 + ``` 506 + 507 + --- 508 + 509 + ## Traverse and Sequence 510 + 511 + Traverse and sequence flip the nesting of two type constructors. 512 + 513 + ### The Intuition 514 + 515 + You have an array of things that produce effects: 516 + 517 + ```typescript 518 + const userIds: string[] = ["1", "2", "3"] 519 + const fetchUser = (id: string): Eff<User, Error, HttpClient> => ... 520 + ``` 521 + 522 + If you map `fetchUser` over the array, you get: 523 + 524 + ```typescript 525 + const effects: Eff<User, Error, HttpClient>[] = userIds.map(fetchUser) 526 + // Array of effects - not what we want 527 + ``` 528 + 529 + But you want: 530 + 531 + ```typescript 532 + const effect: Eff<User[], Error, HttpClient> = ??? 533 + // One effect that produces an array 534 + ``` 535 + 536 + That transformation - flipping `Array<Eff<A>>` to `Eff<Array<A>>` - is what traverse and sequence do. 537 + 538 + ``` 539 + traverse(fetchUser): 540 + 541 + ["1", "2", "3"] Eff<[User, User, User]> 542 + ┌───┬───┬───┐ ┌─────────────────────┐ 543 + │"1"│"2"│"3"│ ───── traverse ────▶│ [User1, User2, User3]│ 544 + └───┴───┴───┘ (fetchUser) └─────────────────────┘ 545 + Array<String> Eff<Array<User>> 546 + 547 + "Flips" the nesting: [F<A>] → F<[A]> 548 + ``` 549 + 550 + ### traverse in purus 551 + 552 + ```typescript 553 + import { traverse, pipe } from "purus-ts" 554 + 555 + const fetchUser = (id: string): Eff<User, Error, HttpClient> => ... 556 + 557 + // Apply fetchUser to each id, collect results 558 + pipe( 559 + ["1", "2", "3"], 560 + traverse(fetchUser) 561 + ) // Eff<readonly User[], Error, HttpClient> 562 + ``` 563 + 564 + Runs sequentially, short-circuits on first error. 565 + 566 + ### sequence in purus 567 + 568 + When you already have an array of effects: 569 + 570 + ```typescript 571 + import { sequence } from "purus-ts" 572 + 573 + const effects: Eff<User, Error, HttpClient>[] = [ 574 + fetchUser("1"), 575 + fetchUser("2"), 576 + fetchUser("3") 577 + ] 578 + 579 + const combined = sequence(effects) 580 + // Eff<readonly User[], Error, HttpClient> 581 + ``` 582 + 583 + `sequence` is just `traverse` with the identity function. 584 + 585 + ### Parallel Variants 586 + 587 + For independent effects that can run concurrently: 588 + 589 + ```typescript 590 + import { traversePar, sequencePar } from "purus-ts" 591 + 592 + // Run all fetches in parallel 593 + pipe( 594 + ["1", "2", "3"], 595 + traversePar(fetchUser) 596 + ) // All run concurrently, fail-fast on first error 597 + 598 + // Parallel sequence 599 + const combined = sequencePar(effects) 600 + ``` 601 + 602 + ### When to Use Each 603 + 604 + | Function | Use When | 605 + |---------------|---------------------------------------------| 606 + | traverse | Transform + collect, sequential | 607 + | sequence | Already have effects, just combine | 608 + | traversePar | Independent operations, want concurrency | 609 + | sequencePar | Already have effects, run in parallel | 610 + 611 + --- 612 + 613 + ## The Educational Value 614 + 615 + You might wonder: if TypeScript can't fully express type classes, why learn them? 616 + 617 + ### Recognizing Patterns 618 + 619 + Once you know the Functor/Applicative/Monad pattern, you recognize it everywhere: 620 + 621 + - Promise is a Monad (`.then` is `flatMap`) 622 + - Array is a Monad (`.flatMap` is `flatMap`) 623 + - RxJS Observable is a Monad 624 + - React's `useState` hook follows monadic patterns 625 + 626 + ### Making Design Decisions 627 + 628 + Understanding that Applicative = independent and Monad = dependent helps you choose: 629 + 630 + - Form validation? Applicative (Validation) - collect all errors 631 + - Database transactions? Monad (Result/Eff) - each step depends on previous 632 + 633 + ### Communicating with FP Developers 634 + 635 + If someone says "just use the Applicative instance", you know they mean use `ap` to combine independent computations without short-circuiting. 636 + 637 + ### Understanding Library Documentation 638 + 639 + Many FP libraries (fp-ts, Effect, etc.) use type class terminology. Knowing these concepts makes their documentation accessible. 640 + 641 + --- 642 + 643 + ## Type Class Instances in purus 644 + 645 + purus exports instance objects showing which type classes each type satisfies: 646 + 647 + ```typescript 648 + import { resultInstances, optionInstances } from "purus-ts" 649 + 650 + // Result satisfies Functor, Applicative, Monad, and Bifunctor 651 + resultInstances.map // mapResult 652 + resultInstances.of // ok 653 + resultInstances.ap // apResult 654 + resultInstances.flatMap // chainResult 655 + resultInstances.bimap // bimap 656 + 657 + // Option satisfies Functor, Applicative, and Monad 658 + optionInstances.map // mapOption 659 + optionInstances.of // some 660 + optionInstances.ap // apOption 661 + optionInstances.flatMap // flatMapOption 662 + ``` 663 + 664 + These are for educational purposes - in practice, you'll use the named functions directly. 665 + 666 + --- 667 + 668 + ## Key Takeaways 669 + 670 + 1. **Type classes are interfaces for types** - Functor, Applicative, Monad define operations types must support 671 + 2. **TypeScript lacks HKT** - We can't write truly generic type class code, but patterns remain consistent 672 + 3. **Functor = map** - Transform values inside a container 673 + 4. **Applicative = independent composition** - Combine operations that don't depend on each other 674 + 5. **Monad = sequential composition** - Chain operations where each step depends on the previous 675 + 6. **Bifunctor = map both sides** - Transform either the success or error value 676 + 7. **Traverse/Sequence = flip nesting** - Turn `[F<A>]` into `F<[A]>` 677 + 8. **The patterns transfer** - Learn these once, recognize them in any FP library 678 + 679 + Understanding type classes gives you a vocabulary for describing common patterns and helps you choose the right abstraction for each problem. 680 + 681 + --- 682 + 683 + ## See Also 684 + 685 + - [Validation and Error Accumulation](./03-validation-and-error-accumulation.md) - Applicative in action 686 + - [Why Errors as Values?](./01-errors-as-values.md) - Result fundamentals 687 + - [Effect Composition](./effect-composition.md) - How effects compose internally 688 + - [Tutorial Chapter 3: Typed Errors with Result](../tutorial/03-typed-errors-with-result.md) - Hands-on Result practice
+49 -3
docs/guides/concepts/README.md
··· 2 2 3 3 Deep-dives into specific topics when you need more detail. 4 4 5 - Each article is self-contained - read them in any order based on what you need. 5 + Articles are numbered in recommended reading order, but each is self-contained. 6 6 7 7 --- 8 8 9 9 ## Articles 10 10 11 - ### [Why Errors as Values?](./errors-as-values.md) 11 + ### [00 - Why These Strange Names?](./00-why-these-strange-names.md) 12 + 13 + Demystifying Functor, Monad, and other FP terminology. 14 + 15 + **Topics Covered:** 16 + - Where these names come from (category theory) 17 + - Plain English translations 18 + - Real-world analogies (Gift Box, IKEA Kit, Recipe) 19 + - When to use Monad vs Applicative 20 + 21 + **Best For:** First read before diving into type classes. Reduces intimidation. 22 + 23 + --- 24 + 25 + ### [01 - Why Errors as Values?](./01-errors-as-values.md) 12 26 13 27 The fundamental shift from exceptions to typed errors. 14 28 ··· 23 37 24 38 --- 25 39 26 - ### [Branded Types In Depth](./branded-types.md) 40 + ### [02 - Branded Types In Depth](./02-branded-types.md) 27 41 28 42 Creating distinct types from primitives for compile-time safety. 29 43 ··· 35 49 - Type-level testing techniques 36 50 37 51 **Best For:** Preventing "wrong argument order" bugs, domain modeling. 52 + 53 + --- 54 + 55 + ### [03 - Validation and Error Accumulation](./03-validation-and-error-accumulation.md) 56 + 57 + Collecting all validation errors instead of stopping at the first. 58 + 59 + **Topics Covered:** 60 + - The short-circuit problem with Result 61 + - Monadic vs Applicative composition 62 + - The Validation type and apValidation 63 + - Converting between Result and Validation 64 + - Real-world form validation example 65 + 66 + **Best For:** Form validation, config parsing, any multi-field validation. 67 + 68 + --- 69 + 70 + ### [04 - Type Classes in TypeScript](./04-type-classes-in-typescript.md) 71 + 72 + Understanding Functor, Applicative, Monad, and Bifunctor patterns. 73 + 74 + **Topics Covered:** 75 + - What type classes solve 76 + - Why TypeScript can't fully express them (HKT) 77 + - Functor, Applicative, Monad, Bifunctor 78 + - traverse and sequence 79 + - When to use each pattern 80 + 81 + **Best For:** Understanding FP abstractions, choosing between patterns. 38 82 39 83 --- 40 84 ··· 119 163 |---------------|--------------------------------------| 120 164 | Result | Errors as values, not exceptions | 121 165 | Option | Nullable values without null | 166 + | Validation | Accumulate all errors, not just first| 122 167 | Branded Types | Distinct types from primitives | 123 168 | Typestate | State machines in the type system | 124 169 | Effect | Lazy, composable, typed async | 125 170 | Fiber | Lightweight thread with cancellation | 126 171 | Environment | Dependencies as a type parameter | 172 + | Type Classes | Functor, Applicative, Monad patterns | 127 173 128 174 --- 129 175
docs/guides/concepts/branded-types.md docs/guides/concepts/02-branded-types.md
+1 -1
docs/guides/concepts/errors-as-values.md docs/guides/concepts/01-errors-as-values.md
··· 396 396 397 397 - [Tutorial Chapter 1: Why Functional TypeScript?](../tutorial/01-why-functional-typescript.md) - Introduction with examples 398 398 - [Tutorial Chapter 3: Typed Errors with Result](../tutorial/03-typed-errors-with-result.md) - Hands-on practice 399 - - [Branded Types In Depth](./branded-types.md) - Another compile-time safety technique 399 + - [Branded Types In Depth](./02-branded-types.md) - Another compile-time safety technique
+1 -1
package.json
··· 1 1 { 2 2 "name": "purus-ts", 3 - "version": "0.1.0-alpha.4", 3 + "version": "0.1.0-alpha.5", 4 4 "description": "Pure TypeScript effect system with fiber-based concurrency, brands, refinements, and pattern matching", 5 5 "type": "module", 6 6 "main": "./dist/index.js",