stillwater 1.1.0

Pragmatic effect composition and validation for Rust - pure core, imperative shell
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
# Parallel Effect Execution

## Overview

Stillwater provides free functions for running independent effects concurrently while preserving the same environment and error model used by sequential effects.

There are two families of parallel helpers:

- Fixed-arity helpers: `par2`, `par3`, and `par4` for heterogeneous effects without boxing.
- Collection helpers: `par_all`, `par_try_all`, `race`, and `par_all_limit` for homogeneous `Vec<BoxedEffect<...>>` batches.

This guide focuses on when to use each helper and how to structure real application code around them.

## Why Parallel Effects?

Many real-world operations are independent and can run concurrently:

- Fetching multiple records from a database
- Making several API calls at once
- Loading independent configuration sources
- Validating independent inputs
- Processing batches with a concurrency limit

Sequential execution waits for each operation before starting the next:

```rust
let user = fetch_user(id).run(&env).await?;
let settings = fetch_settings(id).run(&env).await?;
let preferences = fetch_preferences(id).run(&env).await?;
```

Parallel execution starts independent work together:

```rust
use stillwater::effect::prelude::*;

let (user, settings, preferences) = par3(
    fetch_user(id),
    fetch_settings(id),
    fetch_preferences(id),
    &env,
).await;

let profile = UserProfile {
    user: user?,
    settings: settings?,
    preferences: preferences?,
};
```

## Choosing A Helper

| Need | Helper | Shape |
|------|--------|-------|
| 2-4 effects with different output types | `par2`, `par3`, `par4` | Returns a tuple of `Result`s |
| A batch where all errors should be reported | `par_all` | `Result<Vec<T>, Vec<E>>` |
| A batch where one error is enough | `par_try_all` | `Result<Vec<T>, E>` |
| The first completed result should decide | `race` | `Result<T, E>` |
| A large batch needs bounded concurrency | `par_all_limit` | `Result<Vec<T>, Vec<E>>` |

Collection helpers require boxed effects because a `Vec` needs one concrete item type:

```rust
use stillwater::effect::prelude::*;

let effects: Vec<BoxedEffect<User, DbError, AppEnv>> = user_ids
    .into_iter()
    .map(|id| fetch_user(id).boxed())
    .collect();

let users = par_all(effects, &env).await?;
```

## Heterogeneous Parallel Effects

Use `par2`, `par3`, or `par4` when effects have different output types, or when you want to avoid boxing.

```rust
use stillwater::effect::prelude::*;

let (price, inventory, shipping) = par3(
    fetch_price(item_id),
    fetch_inventory(item_id),
    fetch_shipping_options(item_id),
    &env,
).await;

let quote = Quote {
    price: price?,
    inventory: inventory?,
    shipping: shipping?,
};
```

These helpers return a tuple of results instead of short-circuiting. That makes each outcome explicit:

```rust
let (database, cache) = par2(check_database(), check_cache(), &env).await;

match (database, cache) {
    (Ok(db), Ok(cache)) => Health::healthy(db, cache),
    (db_result, cache_result) => Health::degraded(db_result.err(), cache_result.err()),
}
```

This is useful for diagnostics and health checks where you want to inspect every independent subsystem.

## `par_all` - Collect All Results Or All Errors

Use `par_all` when every operation should run to completion and callers benefit from a complete error report.

```rust
use stillwater::effect::prelude::*;

async fn validate_import(records: Vec<Record>, env: &AppEnv) -> Result<Vec<ValidRecord>, Vec<ValidationError>> {
    let effects: Vec<BoxedEffect<ValidRecord, ValidationError, AppEnv>> = records
        .into_iter()
        .map(|record| validate_record(record).boxed())
        .collect();

    par_all(effects, env).await
}
```

If any effect fails, `par_all` returns all failures:

```rust
let effects: Vec<BoxedEffect<i32, String, ()>> = vec![
    pure(1).boxed(),
    fail("bad input".to_string()).boxed(),
    fail("missing field".to_string()).boxed(),
];

let result = par_all(effects, &()).await;
assert_eq!(
    result,
    Err(vec!["bad input".to_string(), "missing field".to_string()])
);
```

This is the right choice for form validation, import validation, batch reporting, and admin tools where users need a full list of failures.

## `par_try_all` - Return A Single Error

Use `par_try_all` when one error is enough for the caller.

```rust
use stillwater::effect::prelude::*;

async fn load_required_services(env: &AppEnv) -> Result<Vec<ServiceStatus>, ServiceError> {
    let checks: Vec<BoxedEffect<ServiceStatus, ServiceError, AppEnv>> = vec![
        check_database().boxed(),
        check_cache().boxed(),
        check_queue().boxed(),
    ];

    par_try_all(checks, env).await
}
```

`par_try_all` awaits the batch and then collects with normal `Result` semantics, returning the first error in result order. It is not a cancellation primitive.

```rust
let effects: Vec<BoxedEffect<i32, String, ()>> = vec![
    pure(1).boxed(),
    fail("first error".to_string()).boxed(),
    fail("second error".to_string()).boxed(),
];

let result = par_try_all(effects, &()).await;
assert_eq!(result, Err("first error".to_string()));
```

Use `par_all` when you need every error. Use `par_try_all` when the caller only needs to know that the batch failed.

## `race` - First Completed Result

Use `race` when the first completed result should decide the outcome.

```rust
use stillwater::effect::prelude::*;

async fn fetch_from_fastest_replica(
    key: String,
    env: &AppEnv,
) -> Result<Data, RaceError<FetchError>> {
    let effects: Vec<BoxedEffect<Data, FetchError, AppEnv>> = vec![
        fetch_from_replica_a(key.clone()).boxed(),
        fetch_from_replica_b(key.clone()).boxed(),
        fetch_from_replica_c(key).boxed(),
    ];

    race(effects, env).await
}
```

`race` returns the first completed result, whether success or error. It does not wait to find the first success.

```rust
let effects: Vec<BoxedEffect<i32, String, ()>> = vec![
    fail("fast failure".to_string()).boxed(),
    pure(42).boxed(),
];

let result = race(effects, &()).await;
assert_eq!(result, Err(RaceError::Inner("fast failure".to_string())));
```

This behavior is useful for deadline effects, fastest-result wins workflows, or cases where a fast failure should abort the attempt. For fallback semantics where failures should be ignored until every source fails, compose effects with `or_else`, `fallback_to`, or explicit retry/fallback logic instead of `race`.

## `par_all_limit` - Bounded Concurrency

Use `par_all_limit` for large batches or limited resources such as connection pools, file descriptors, or API rate limits.

```rust
use stillwater::effect::prelude::*;

async fn process_queue(
    queue: Vec<WorkItem>,
    max_concurrent: usize,
    env: &AppEnv,
) -> Result<Vec<ProcessedItem>, Vec<ProcessingError>> {
    let effects: Vec<BoxedEffect<ProcessedItem, ProcessingError, AppEnv>> = queue
        .into_iter()
        .map(|item| process_item(item).boxed())
        .collect();

    par_all_limit(effects, max_concurrent, env).await
}
```

The function still runs every effect and collects all errors, but it only keeps `limit` futures in flight at once.

```rust
let effects: Vec<BoxedEffect<i32, String, ()>> = (1..=10)
    .map(|n| pure(n).boxed())
    .collect();

let result = par_all_limit(effects, 3, &()).await;
assert_eq!(result.as_ref().map(|values| values.len()), Ok(10));
```

## Environment Access

Parallel helpers receive a shared `&Env`. Boxed collection helpers require `Env: Clone + Send + Sync + 'static`, so application environments usually store services in cheap-to-clone handles:

```rust
use std::sync::Arc;

#[derive(Clone)]
struct AppEnv {
    config: Arc<Config>,
    db: Arc<DatabasePool>,
    http: Arc<HttpClient>,
}
```

Each effect still controls how it uses the environment:

```rust
use stillwater::effect::prelude::*;

fn fetch_user(id: UserId) -> impl Effect<Output = User, Error = DbError, Env = AppEnv> {
    from_async(move |env: &AppEnv| {
        let db = env.db.clone();
        async move { db.fetch_user(id).await }
    })
}
```

## Composing Parallel And Sequential Work

Parallel work often appears inside a larger sequential workflow. Use normal Rust control flow around the async helper calls:

```rust
use stillwater::effect::prelude::*;

async fn build_dashboard(user_id: UserId, env: &AppEnv) -> Result<Dashboard, AppError> {
    let user = fetch_user(user_id).run(env).await?;

    let (activity, recommendations, alerts) = par3(
        fetch_activity(user.id),
        fetch_recommendations(user.id),
        fetch_alerts(user.id),
        env,
    ).await;

    Ok(Dashboard {
        user,
        activity: activity?,
        recommendations: recommendations?,
        alerts: alerts?,
    })
}
```

For a second parallel phase, build another effect collection after the first phase succeeds:

```rust
async fn load_and_save(ids: Vec<UserId>, env: &AppEnv) -> Result<Vec<Receipt>, Vec<AppError>> {
    let load_effects: Vec<BoxedEffect<User, AppError, AppEnv>> = ids
        .into_iter()
        .map(|id| fetch_user(id).boxed())
        .collect();

    let users = par_all(load_effects, env).await?;

    let save_effects: Vec<BoxedEffect<Receipt, AppError, AppEnv>> = users
        .into_iter()
        .map(|user| save_user_snapshot(user).boxed())
        .collect();

    par_all(save_effects, env).await
}
```

## Practical Patterns

### Parallel Validation

Use `par_all` when expensive validation checks can run independently and the user should see all failures:

```rust
async fn validate_signup(data: SignupData, env: &AppEnv) -> Result<ValidSignup, Vec<SignupError>> {
    let effects: Vec<BoxedEffect<FieldCheck, SignupError, AppEnv>> = vec![
        validate_email(data.email).boxed(),
        validate_username(data.username).boxed(),
        validate_password(data.password).boxed(),
    ];

    let checks = par_all(effects, env).await?;
    Ok(ValidSignup::from_checks(checks))
}
```

### Health Checks

Use fixed-arity helpers when each subsystem has a distinct result:

```rust
async fn health(env: &AppEnv) -> HealthReport {
    let (database, cache, queue) = par3(
        check_database(),
        check_cache(),
        check_queue(),
        env,
    ).await;

    HealthReport {
        database,
        cache,
        queue,
    }
}
```

### Rate-Limited API Imports

Use `par_all_limit` when the remote system enforces a concurrency cap:

```rust
async fn import_customers(customers: Vec<Customer>, env: &AppEnv) -> ImportSummary {
    let effects: Vec<BoxedEffect<ImportReceipt, ImportError, AppEnv>> = customers
        .into_iter()
        .map(|customer| send_customer(customer).boxed())
        .collect();

    match par_all_limit(effects, 10, env).await {
        Ok(receipts) => ImportSummary::success(receipts),
        Err(errors) => ImportSummary::failure(errors),
    }
}
```

### Fastest Completed Source

Use `race` only when "first completed" is the desired behavior:

```rust
async fn query_fastest_index(
    term: SearchTerm,
    env: &AppEnv,
) -> Result<SearchResults, RaceError<SearchError>> {
    let effects: Vec<BoxedEffect<SearchResults, SearchError, AppEnv>> = vec![
        query_primary_index(term.clone()).boxed(),
        query_replica_index(term).boxed(),
    ];

    race(effects, env).await
}
```

If a fast failure should not win, use a fallback chain:

```rust
fn query_with_fallback(term: SearchTerm) -> impl Effect<Output = SearchResults, Error = SearchError, Env = AppEnv> {
    query_primary_index(term.clone())
        .fallback_to(query_replica_index(term))
}
```

## Performance Considerations

### Actual Concurrency

The parallel helpers use async concurrency. They do not spawn OS threads by themselves; each effect must be asynchronous or otherwise yield for concurrency to matter.

```rust
use stillwater::effect::prelude::*;
use std::time::{Duration, Instant};

let start = Instant::now();

let effects: Vec<BoxedEffect<(), String, ()>> = (0..3)
    .map(|_| {
        from_async(|_: &()| async {
            tokio::time::sleep(Duration::from_millis(100)).await;
            Ok::<_, String>(())
        })
        .boxed()
    })
    .collect();

par_all(effects, &()).await.unwrap();

let elapsed = start.elapsed();
assert!(elapsed < Duration::from_millis(200));
```

### Boxing Cost

Fixed-arity helpers avoid boxing and are the best fit for small, known sets of independent effects.

Collection helpers require boxing because a vector needs one concrete type. Prefer collection helpers for dynamic or large batches where the allocation cost is dominated by I/O.

### Memory Usage

- `par_all` and `par_try_all` keep the whole batch in flight.
- `race` keeps the whole batch in flight until the first result completes.
- `par_all_limit` keeps at most `limit` effects in flight and is the safer default for large batches.

## Testing Parallel Effects

Parallel effects use the same environment pattern as sequential effects:

```rust
#[tokio::test]
async fn loads_users_in_parallel() {
    let env = TestEnv::with_users(vec![
        User::new(1),
        User::new(2),
        User::new(3),
    ]);

    let effects: Vec<BoxedEffect<User, TestError, TestEnv>> = vec![
        fetch_user(1).boxed(),
        fetch_user(2).boxed(),
        fetch_user(3).boxed(),
    ];

    let users = par_all(effects, &env).await.unwrap();
    assert_eq!(users.len(), 3);
}
```

For timing-sensitive tests, keep assertions loose enough to avoid flakes. Prefer testing result shape and concurrency limits over exact elapsed time.

## Common Pitfalls

### Do Not Parallelize Dependent Operations

```rust
// Wrong: sending the email needs the user returned by create_user.
let effects: Vec<BoxedEffect<(), AppError, AppEnv>> = vec![
    create_user(data).map(|_| ()).boxed(),
    send_welcome_email(user_id).boxed(),
];

// Right: compose dependent work sequentially.
create_user(data)
    .and_then(|user| send_welcome_email(user.id))
```

### Do Not Use `race` For "First Success"

`race` returns the first completed result. A fast error wins over a slower success. If you need "try primary, then backup," use `fallback_to` or `or_else`.

### Use `Arc`, Not `Rc`, In Shared Environments

```rust
// Wrong: Rc is not Send + Sync.
struct AppEnv {
    db: Rc<DatabasePool>,
}

// Right: Arc works in async shared environments.
#[derive(Clone)]
struct AppEnv {
    db: Arc<DatabasePool>,
}
```

### Box At Collection Boundaries

Keep individual effect builders zero-cost, and box only when placing them into a homogeneous collection:

```rust
fn fetch_user(id: UserId) -> impl Effect<Output = User, Error = DbError, Env = AppEnv> {
    from_async(move |env: &AppEnv| {
        let db = env.db.clone();
        async move { db.fetch_user(id).await }
    })
}

let effects: Vec<BoxedEffect<User, DbError, AppEnv>> = ids
    .into_iter()
    .map(|id| fetch_user(id).boxed())
    .collect();
```

## Summary

- Use `par2`, `par3`, and `par4` for small heterogeneous sets without boxing.
- Use `par_all` when all errors should be reported.
- Use `par_try_all` when one error is enough, but do not treat it as cancellation.
- Use `race` when the first completed result should decide the outcome.
- Use `par_all_limit` to protect connection pools, memory, rate limits, and other bounded resources.