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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,9 @@ $handle = $heavyComputation
- Type-safe IO operations
- 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
- Resource management with `bracket`, `bracketCase`, `bracketE`, and `Resource` as a composable value
- `sequence` and `traverse` over lists of effects, `productR`, `delay`, `both`
- `race`, `either` and `timeout` on effects that run where they can be cancelled
- **Async execution with `start()`** - Fork computations to background fibers
- **Custom execution contexts** - Control how effects are executed

Expand Down
139 changes: 30 additions & 109 deletions docs/combinators.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,88 +37,73 @@ $result = $empty->unsafeRun(); // throws NoSuchElementException

## Sequence and Traverse

These combinators help you work with collections of effects.
Both live in `Phunkie\Effect\Functions\io`, next to `io` and `bracket`.

### Sequence

`sequence` transforms a collection of effects into an effect of a collection:
`sequence` turns a list of effects into one effect of their results, run in order when the effect runs, keys kept:

```php
use function Phunkie\Effect\Functions\io\io;
use function Phunkie\Effect\Functions\io\{io, sequence};

$ios = [
io(function() { return 1; }),
io(function() { return 2; }),
io(function() { return 3; })
];
$ios = [io(fn() => 1), io(fn() => 2), io(fn() => 3)];

$result = sequence($ios)->unsafeRun(); // [1, 2, 3]
```

A blocking effect in the list is awaited, so `sequence` yields values, never handles.

### Traverse

`traverse` is like `sequence` but allows you to transform each value before sequencing:
`traverse` maps each value to an effect and sequences the results:

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

$numbers = [1, 2, 3];

$result = traverse($numbers, fn($n) =>
io(function() use ($n) {
return $n * 2;
})
)->unsafeRun(); // [2, 4, 6]
$result = traverse([1, 2, 3], fn($n) => io(fn() => $n * 2))->unsafeRun(); // [2, 4, 6]
```

## Race and Timeout

These combinators help you handle concurrent operations and timeouts.
`race`, `either` and `timeout` run an effect where it can be polled and cancelled. A blocking effect runs in the context it was given; any other effect runs in the context passed as the last argument, `ParallelExecutionContext` by default, which needs `ext-parallel`. A context whose handles cannot be polled, such as the fiber context, is refused with a `RuntimeException` that says so.

### Race

`race` runs two effects concurrently and returns the result of the first one to complete:
`race` returns the result of whichever effect finishes first and cancels the other:

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

use function Phunkie\Effect\Functions\blocking\blocking;

$fast = io(function() {
return blocking(function() {
usleep(100000); // 0.1 seconds
return "fast";
});
});
$fast = blocking(function() {
usleep(100000);

$slow = io(function() {
return blocking(function() {
usleep(200000); // 0.2 seconds
return "slow";
});
});
return "fast";
}, new ParallelExecutionContext());

$slow = blocking(function() {
usleep(200000);

return "slow";
}, new ParallelExecutionContext());

$result = $fast->race($slow)->unsafeRun(); // "fast"
```

`either` is the same race that says who won: `Left` for the effect it was called on, `Right` for the other.

### Timeout

`timeout` adds a timeout to an effect, failing if it takes too long:
`timeout` gives an effect a number of milliseconds; when they run out the effect is cancelled and a `Phunkie\Effect\Concurrent\TimeoutException` is raised:

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

$slow = io(function() {
return blocking(function() {
sleep(2);
return "done";
});
});
use Phunkie\Effect\Concurrent\TimeoutException;

try {
$result = $slow->timeout(1000)->unsafeRun(); // throws TimeoutException
} catch (\Exception $e) {
// Handle timeout
$result = $slow->timeout(1000)->unsafeRun();
} catch (TimeoutException $e) {
// Timed out after 1000 ms.
}
```

Expand Down Expand Up @@ -195,67 +180,3 @@ $message = $result->fold(fn($error) => $error->getMessage())(fn($value) => "ok:
$result = $value * 2;
```

2. **Use Sequence for Collections**
```php
// Good: Using sequence for collections
$results = sequence($ios)->unsafeRun();

// Bad: Manual sequencing
$results = [];
foreach ($ios as $io) {
$results[] = $io->unsafeRun();
}
```

3. **Use Race for Concurrent Operations**
```php
// Good: Using race for concurrent operations
$result = $operation1->race($operation2)->unsafeRun();

// Bad: Manual concurrent execution
$handle1 = $operation1->unsafeRun();
$handle2 = $operation2->unsafeRun();
$result = $handle1->await(); // Might wait unnecessarily
```

4. **Use Timeout for Long-Running Operations**
```php
// Good: Using timeout for long-running operations
$result = $operation->timeout(1000)->unsafeRun();

// Bad: No timeout
$result = $operation->unsafeRun(); // Might hang indefinitely
```

5. **Use Error Handling Combinators**
```php
// Good: Using error handling combinators
$result = $risky->handleError(fn($e) => "fallback")->unsafeRun();

// Bad: Try-catch blocks
try {
$result = $risky->unsafeRun();
} catch (\Exception $e) {
$result = "fallback";
}
```

Remember that combinators are designed to work together. You can chain them to create complex effect compositions:

```php
$result = $io
->map(fn($x) => $x * 2)
->filter(fn($x) => $x > 10)
->timeout(1000)
->handleError(fn($e) => 0)
->unsafeRun();
```

This composition:
1. Doubles the value
2. Filters out values less than 10
3. Times out after 1 second
4. Returns 0 if any error occurs
5. Executes the entire chain

The power of combinators lies in their ability to compose effects in a declarative way, making your code more readable and maintainable.
145 changes: 26 additions & 119 deletions docs/concurrency.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,130 +207,63 @@ Remember that parallel execution is not a silver bullet. Consider:

When in doubt, prefer simpler concurrency models like fibers for I/O operations and only use parallel execution for CPU-intensive tasks that benefit from true parallelism.

## Sequence and Traverse
## Race, Either and Timeout

These combinators help you work with collections of effects:
These run an effect where it can be polled and cancelled: a blocking effect in the context it was given, any other effect in the context passed as the last argument, `ParallelExecutionContext` by default. The fiber context cannot be polled, so it is refused with a `RuntimeException`.

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

$ios = [
io(function() { return 1; }),
io(function() { return 2; }),
io(function() { return 3; })
];

$result = sequence($ios)->unsafeRun(); // [1, 2, 3]
```
use Phunkie\Effect\Concurrent\ParallelExecutionContext;
use Phunkie\Effect\Concurrent\TimeoutException;

### Traverse
```php
use function Phunkie\Effect\Functions\io\io;
$fast = blocking(fn() => fetchFromCache(), new ParallelExecutionContext());
$slow = blocking(fn() => fetchFromOrigin(), new ParallelExecutionContext());

$numbers = [1, 2, 3];
$result = $fast->race($slow)->unsafeRun(); // whichever finished first; the other is cancelled
$which = $fast->either($slow)->unsafeRun(); // Left(result) when $fast won, Right(result) when $slow did

$result = traverse($numbers, fn($n) =>
io(function() use ($n) {
return $n * 2;
})
)->unsafeRun(); // [2, 4, 6]
try {
$result = $slow->timeout(1000)->unsafeRun(); // the result, or TimeoutException after 1000 ms
} catch (TimeoutException $e) {
}
```

## Race, Both, Either
## Both

These combinators help you handle concurrent operations in different ways:
`both` runs this effect, then the other, and pairs the results:

### Race
```php
$fast = io(function() {
return blocking(function() {
usleep(100000); // 0.1 seconds
return "fast";
});
});
$result = $operation1->both($operation2)->unsafeRun(); // [result1, result2]
```

$slow = io(function() {
return blocking(function() {
usleep(200000); // 0.2 seconds
return "slow";
});
});
It is sequential; `parMap2` is the parallel form.

$result = $fast->race($slow)->unsafeRun(); // "fast"
```
## Delay

### Both
```php
$result = $operation1->both($operation2)->unsafeRun();
// Returns [result1, result2]
```
`delay` runs the effect once the given milliseconds have passed:

### Either
```php
$result = $operation1->either($operation2)->unsafeRun();
// Returns Left(result1) or Right(result2)
$result = $operation->delay(1000)->unsafeRun(); // runs after 1 second
```

## Cancellation

Cancellation allows you to stop operations that are no longer needed:
A handle from the parallel context is a `CancellableHandle`: it can be asked whether the computation has finished and can be cancelled. `race` and `timeout` use that to stop the effect that lost or ran out of time.

```php
$operation = io(function() {
return blocking(function() {
sleep(10);
return "done";
});
});

$handle = $operation->unsafeRun();
$handle = $operation->start(new ParallelExecutionContext())->unsafeRun();
// ... some time later ...
$handle->cancel();
```

## Delay

Delay allows you to schedule operations to run after a specified time:

```php
$operation = io(function() {
return blocking(function() {
return "delayed";
});
});

$result = $operation->delay(1000)->unsafeRun(); // Runs after 1 second
if (! $handle->isDone()) {
$handle->cancel();
}
```

## Channels

Channels provide a way to communicate between concurrent operations:

```php
$channel = new Channel();

$producer = io(function() use ($channel) {
return blocking(function() use ($channel) {
$channel->send("message");
});
});

$consumer = io(function() use ($channel) {
return blocking(function() use ($channel) {
return $channel->receive();
});
});

$result = $producer->both($consumer)->unsafeRun();
```
A handle from the fiber context runs the computation when it is awaited and cannot be cancelled.

## Best Practices

1. **Choose the Right Execution Context**
- Use Fiber context for I/O operations
- Use Parallel context for CPU-intensive tasks
- Use Process context for isolated operations

2. **Handle Resources Properly**
```php
Expand All @@ -350,29 +283,3 @@ $result = $producer->both($consumer)->unsafeRun();
$result = $operation1->flatMap(fn($a) => $operation2->map(fn($b) => $a + $b));
```

4. **Handle Cancellation**
```php
$handle = $operation->unsafeRun();
try {
$result = $handle->await();
} catch (CancellationException $e) {
// Handle cancellation
}
```

5. **Use Error Handling**
```php
$result = $operation
->handleError(fn($e) => "fallback")
->timeout(1000)
->unsafeRun();
```

Remember that concurrency is a powerful tool, but it comes with complexity. Always consider:
- Resource management
- Error handling
- Cancellation
- Timeouts
- State management

The key is to use the right tool for the job and handle all edge cases appropriately.
Loading
Loading