Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,24 @@ $chained = $effect->flatMap(function($content) {
});
```

### Combining and Recovering

```php
use function Phunkie\Effect\Functions\io\io;

// Independent effects, run in order, combined by one function
$page = io(fn() => loadUser($id))
->mapN([io(fn() => loadOrders($id)), io(fn() => loadAddress($id))], fn($user, $orders, $address) => compact('user', 'orders', 'address'));

// Recover with a value, with another effect, or only from one exception class
$safe = $risky->handleError(fn($e) => 'default');
$fresh = io(fn() => readCache($key))->handleErrorWith(fn($e) => io(fn() => readOrigin($key)));
$found = io(fn() => $users->get($id))->recover(UserNotFound::class, fn($e) => io(fn() => null));

// Or keep the error as a value
$outcome = $risky->attempt(); // IO<Validation<Throwable, A>>
```

### Async Execution with start()

The `start()` method allows you to fork computations into background fibers, enabling fire-and-forget patterns:
Expand Down Expand Up @@ -97,8 +115,8 @@ $handle = $heavyComputation

- Pure functional effect handling
- Type-safe IO operations
- Composable effect chains
- Error handling through Either
- Composable effect chains, sequential with `flatMap` and `mapN`, parallel with `parMapN`
- Error handling as values with `attempt()`, or recovery with `handleError()`, `handleErrorWith()` and `recover()`
- Resource management
- **Async execution with `start()`** - Fork computations to background fibers
- **Custom execution contexts** - Control how effects are executed
Expand Down
30 changes: 27 additions & 3 deletions docs/combinators.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,33 @@ $safe = $risky->handleError(function($error) {
$result = $safe->unsafeRun(); // "Recovered from: Something went wrong"
```

### HandleErrorWith

`handleErrorWith` recovers with another effect, which runs in place of the failed one:

```php
use function Phunkie\Effect\Functions\io\io;

$content = io(fn() => readCache($key))
->handleErrorWith(fn($error) => io(fn() => readOrigin($key)));
```

### Recover

`recover` handles one exception class and lets every other error propagate:

```php
use function Phunkie\Effect\Functions\io\io;

$user = io(fn() => $users->get($id))
->recover(UserNotFound::class, fn(UserNotFound $e) => io(fn() => null));

$user->unsafeRun(); // null when the user is missing, still throws on anything else
```

### Attempt

`attempt` wraps the result in an `Either` type to handle success and failure:
`attempt` wraps the outcome in a `Validation`, `Success` or `Failure`, instead of throwing:

```php
use function Phunkie\Effect\Functions\io\io;
Expand All @@ -155,8 +179,8 @@ $risky = io(function() {
throw new \Exception("Something went wrong");
});

$result = $risky->attempt()->unsafeRun();
// Returns Left(Exception) instead of throwing
$result = $risky->attempt()->unsafeRun(); // Failure(Exception)
$message = $result->fold(fn($error) => $error->getMessage())(fn($value) => "ok: $value");
```

## Best Practices
Expand Down
50 changes: 39 additions & 11 deletions docs/io.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,21 @@ $sequential = io(fn() => step1())
->flatMap(fn($b) => io(fn() => step3($b)));
```

### mapN - Combine Independent Effects

When the effects do not depend on each other, `mapN` runs them in order and hands every result to one function, which keeps a value built from several sources flat instead of nesting `flatMap`s:

```php
$view = io(fn() => fetchUser($id))
->mapN([io(fn() => fetchOrders($id)), io(fn() => fetchAddress($id))], fn($user, $orders, $address) => [
'user' => $user,
'orders' => $orders,
'address' => $address,
]);
```

`map2` does the same for exactly two effects; `parMapN` below runs them in parallel instead.

### Parallel Execution

```php
Expand Down Expand Up @@ -240,13 +255,10 @@ $risky = io(fn() => throw new \RuntimeException('Oops'));
$safe = $risky->attempt(); // IO<Validation<Throwable, A>>

$result = $safe->unsafeRun();
$result->match(
Success: fn($value) => "Got: $value",
Failure: fn($error) => "Error: {$error->getMessage()}"
);
$result->fold(fn($error) => "Error: {$error->getMessage()}")(fn($value) => "Got: $value");
```

### handleError() - Recover from Errors
### handleError() - Recover with a Value

```php
$risky = io(fn() => riskyOperation());
Expand All @@ -258,6 +270,25 @@ $getUser = io(fn() => $db->findUser($id))
->handleError(fn($e) => null); // Return null if not found
```

### handleErrorWith() - Recover with Another Effect

When the recovery is itself an effect, `handleErrorWith` runs it in place of the failed one:

```php
$content = io(fn() => readCache($key))
->handleErrorWith(fn($e) => io(fn() => readOrigin($key)));
```

### recover() - Recover from One Exception Class

`recover` handles only the named class and lets every other error keep propagating, so a handler can answer the failures it understands without swallowing the rest:

```php
$response = io(fn() => $users->get($id))
->flatMap(fn($user) => Ok($user))
->recover(UserNotFound::class, fn(UserNotFound $e) => NotFound(['error' => $e->getMessage()]));
```

### ensure() - Fail When a Value Is Not Acceptable

`ensure` raises an error when the produced value does not satisfy a predicate. It
Expand Down Expand Up @@ -285,12 +316,9 @@ plain `for { $x <- io } yield ...` (no guard) works as usual, desugaring to

```php
$program = fetchFromApi()
->attempt()
->flatMap(fn($result) => $result->match(
Success: fn($data) => io(fn() => processData($data)),
Failure: fn($error) => io(fn() => logError($error))
->productR(io(fn() => getDefaultData()))
));
->flatMap(fn($data) => io(fn() => processData($data)))
->handleErrorWith(fn($error) => io(fn() => logError($error))
->productR(io(fn() => getDefaultData())));
```

## Running IO
Expand Down
Loading