From 34c6afb89dd5b4aa0af044af1dc56f90332201e9 Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Sat, 6 Dec 2025 16:02:20 +0000 Subject: [PATCH 01/23] Finalize static analysis and prep for 1.0.0 --- composer.json | 13 ++-- src/Cats/Parallel.php | 17 +++++ src/Concurrent/AsyncHandle.php | 13 ++++ src/Concurrent/Blocker.php | 26 +++++++ src/Concurrent/ExecutionContext.php | 17 ++++- src/Concurrent/FiberExecutionContext.php | 23 +++++- src/Concurrent/ParallelExecutionContext.php | 22 +++++- src/Functions/blocking.php | 9 +++ src/Functions/console.php | 79 +++++++++++++++++++++ src/Functions/io.php | 8 ++- src/IO/IO.php | 51 +++++++++++++ src/IO/IOApp.php | 8 ++- src/Ops/IO/ApplicativeOps.php | 14 ++-- src/Ops/IO/FunctorOps.php | 24 +++++-- src/Ops/IO/MonadOps.php | 8 ++- src/Ops/IO/ParallelOps.php | 21 ++++-- 16 files changed, 319 insertions(+), 34 deletions(-) diff --git a/composer.json b/composer.json index 38cebd2..b070656 100644 --- a/composer.json +++ b/composer.json @@ -10,15 +10,14 @@ } ], "require": { - "php": ">=8.2", - "phunkie/phunkie": "^0.11" + "php": "^8.2 || ^8.3 || ^8.4", + "phunkie/phunkie": "^1.0.0" }, "require-dev": { "phpunit/phpunit": "^10.5", - "phunkie/phunkie-console": "dev-master", + "phunkie/console": "^1.0.0", "phpstan/phpstan": "^2.1", "friendsofphp/php-cs-fixer": "^3.75" - }, "suggest": { "ext-parallel": "Required for parallel execution using threads. PHP must be compiled with ZTS support." @@ -39,12 +38,12 @@ "scripts": { "test": "phpunit", "phpstan": "phpstan analyse", - "cs-fix": "php-cs-fixer fix", - "cs-check": "php-cs-fixer fix --dry-run --diff" + "cs-fix": "php-cs-fixer fix --allow-risky=yes", + "cs-check": "php-cs-fixer fix --dry-run --diff --allow-risky=yes" }, "minimum-stability": "dev", "prefer-stable": true, "config": { "bin-dir": "bin" } -} +} \ No newline at end of file diff --git a/src/Cats/Parallel.php b/src/Cats/Parallel.php index a4bf2a9..ef8d229 100644 --- a/src/Cats/Parallel.php +++ b/src/Cats/Parallel.php @@ -11,7 +11,24 @@ namespace Phunkie\Effect\Cats; +/** + * Interface representing a computation that can be run in parallel. + * + * This interface corresponds to Cats' Parallel type class, allowing + * conversion between sequential (Monad) and parallel (Applicative) execution modes. + * + * @template A + */ interface Parallel { + /** + * Combines this parallel computation with another using a combining function. + * + * @template B + * @template C + * @param Parallel $fb The computation to combine with + * @param callable(A, B): C $f The function to combine the results + * @return Parallel The result of combined computation + */ public function parMap2(Parallel $fb, callable $f): Parallel; } diff --git a/src/Concurrent/AsyncHandle.php b/src/Concurrent/AsyncHandle.php index 6a4925f..3e5d92e 100644 --- a/src/Concurrent/AsyncHandle.php +++ b/src/Concurrent/AsyncHandle.php @@ -11,7 +11,20 @@ namespace Phunkie\Effect\Concurrent; +/** + * Represents a handle to an asynchronous computation. + * + * An AsyncHandle allows waiting for the result of an asynchronous operation, + * similar to a Future or Promise in other languages. + * + * @template A + */ interface AsyncHandle { + /** + * Blocks until the asynchronous computation is complete and returns the result. + * + * @return A The result of the computation + */ public function await(): mixed; } diff --git a/src/Concurrent/Blocker.php b/src/Concurrent/Blocker.php index 8ef1c4a..5239657 100644 --- a/src/Concurrent/Blocker.php +++ b/src/Concurrent/Blocker.php @@ -11,8 +11,19 @@ namespace Phunkie\Effect\Concurrent; +/** + * Provides an execution context for blocking operations. + * + * Blocker is designed to isolate blocking code from the main execution context, + * typically by running it on a separate thread or fiber pool to prevent + * starving the main event loop. + */ class Blocker { + /** + * @param \Closure(): mixed $thunk The computation to execute + * @param ExecutionContext|null $context The execution context to use (defaults to FiberExecutionContext) + */ public function __construct( private readonly \Closure $thunk, private ?ExecutionContext $context = null @@ -22,16 +33,31 @@ public function __construct( } } + /** + * Executes the blocking operation asynchronously. + * + * @return AsyncHandle A handle to await the result + */ public function __invoke(): AsyncHandle { return $this->context->executeAsync($this->thunk); } + /** + * Executes the blocking operation synchronously. + * + * @return mixed The result of the computation + */ public function runSync(): mixed { return $this->context?->execute($this->thunk); } + /** + * Returns the underlying execution context. + * + * @return ExecutionContext + */ public function blockingContext(): ExecutionContext { return $this->context ?? new FiberExecutionContext(); diff --git a/src/Concurrent/ExecutionContext.php b/src/Concurrent/ExecutionContext.php index c06f44b..992314a 100644 --- a/src/Concurrent/ExecutionContext.php +++ b/src/Concurrent/ExecutionContext.php @@ -11,14 +11,29 @@ namespace Phunkie\Effect\Concurrent; +/** + * Interface for execution contexts. + * + * An ExecutionContext defines how computations are executed. Implementations + * may execute code synchronously (e.g., in a Fiber) or asynchronously + * (e.g., using ext-parallel threads). + */ interface ExecutionContext { /** * Runs the given thunk in this execution context. * - * @param callable $thunk The operation to run + * @param callable(): mixed $thunk The operation to run * @return mixed The result of running the thunk */ public function execute(callable $thunk): mixed; + + /** + * Runs the given thunk asynchronously in this execution context. + * + * @template A + * @param callable(): A $thunk The operation to run + * @return AsyncHandle A handle for the result + */ public function executeAsync(callable $thunk): AsyncHandle; } diff --git a/src/Concurrent/FiberExecutionContext.php b/src/Concurrent/FiberExecutionContext.php index 6a3fa26..04b1047 100644 --- a/src/Concurrent/FiberExecutionContext.php +++ b/src/Concurrent/FiberExecutionContext.php @@ -14,10 +14,21 @@ use FiberError; use Throwable; +/** + * Execution context based on PHP Fibers. + * + * This implementation uses PHP 8.1+ Fibers to execute computations cooperatively. + * It is single-threaded (uses the main thread) but supports non-blocking behavior + * when used with Fiber-aware primitives. + */ class FiberExecutionContext implements ExecutionContext { /** - * @throws FiberError|Throwable + * Executes the thunk in a new Fiber. + * + * @param callable(): mixed $thunk The operation to run + * @return mixed The result of running the thunk + * @throws FiberError|Throwable If the fiber fails */ public function execute(callable $thunk): mixed { @@ -31,6 +42,16 @@ public function execute(callable $thunk): mixed return $fiber->getReturn(); } + /** + * Executes the thunk asynchronously. + * + * In this implementation, despite the name, the execution happens + * when `await()` is called, effectively deferring execution. + * + * @template A + * @param callable(): A $thunk The operation to run + * @return AsyncHandle A handle for the result + */ public function executeAsync(callable $thunk): AsyncHandle { $that = $this; diff --git a/src/Concurrent/ParallelExecutionContext.php b/src/Concurrent/ParallelExecutionContext.php index baf00a0..dc3bbfc 100644 --- a/src/Concurrent/ParallelExecutionContext.php +++ b/src/Concurrent/ParallelExecutionContext.php @@ -15,10 +15,20 @@ use parallel\Runtime; use Throwable; +/** + * Execution context using ext-parallel for true parallelism. + * + * This implementation utilizes the `parallel` extension (requires ZTS PHP) + * to execute computations in separate threads. + */ class ParallelExecutionContext implements ExecutionContext { /** - * @throws Throwable + * Executes the thunk in a new thread and blocks until completion. + * + * @param callable(): mixed $thunk The operation to run + * @return mixed The result of running the thunk + * @throws Throwable If execution fails or ext-parallel is missing */ public function execute(callable $thunk): mixed { @@ -32,6 +42,16 @@ public function execute(callable $thunk): mixed } } + /** + * Executes the thunk asynchronously in a new thread. + * + * Returns an AsyncHandle that can be awaited to retrieve the result. + * + * @template A + * @param callable(): A $thunk The operation to run + * @return AsyncHandle A handle for the result + * @throws \RuntimeException If ext-parallel is missing + */ public function executeAsync(callable $thunk): AsyncHandle { if (\extension_loaded('parallel')) { diff --git a/src/Functions/blocking.php b/src/Functions/blocking.php index 821ca58..2601f1e 100644 --- a/src/Functions/blocking.php +++ b/src/Functions/blocking.php @@ -20,6 +20,15 @@ use Phunkie\Effect\IO\IO; const blocking = '\Phunkie\Effect\Functions\blocking\blocking'; + +/** + * Wraps a blocking computation in an IO using a Blocker to isolate it. + * + * @template A + * @param \Closure(): A $thunk The blocking operation + * @param ExecutionContext|null $context Optional execution context (uses fiber default if null) + * @return IO + */ function blocking(Closure $thunk, ?ExecutionContext $context = null): IO { return io(new Blocker($thunk, $context)); diff --git a/src/Functions/console.php b/src/Functions/console.php index 48f9e4c..2500414 100644 --- a/src/Functions/console.php +++ b/src/Functions/console.php @@ -15,12 +15,26 @@ use Phunkie\Types\ImmList; const printLn = '\Phunkie\Effect\Functions\console\printLn'; + +/** + * Prints a message to stdout followed by a newline. + * + * @param string $message The message to print + * @return IO + */ function printLn(string $message): IO { return new IO(fn () => print($message . PHP_EOL)); } const printLines = '\Phunkie\Effect\Functions\console\printLines'; + +/** + * Prints a list of messages to stdout, each followed by a newline. + * + * @param ImmList $lines The lines to print + * @return IO + */ function printLines(ImmList $lines): IO { return new IO(fn () => @@ -28,6 +42,14 @@ function printLines(ImmList $lines): IO } const readLine = '\Phunkie\Effect\Functions\console\readLine'; + +/** + * Reads a line from the input stream. + * + * @param string $prompt Optional prompt to display + * @param resource|null $stream Optional input stream (defaults to STDIN) + * @return IO + */ function readLine(string $prompt, $stream = null): IO { return new IO(function () use ($prompt, $stream) { @@ -40,36 +62,78 @@ function readLine(string $prompt, $stream = null): IO } const printError = '\Phunkie\Effect\Functions\console\printError'; + +/** + * Prints an error message (red) to stdout. + * + * @param string $message The error message + * @return IO + */ function printError(string $message): IO { return new IO(fn () => print("\033[31mError: {$message}\033[0m" . PHP_EOL)); } const printWarning = '\Phunkie\Effect\Functions\console\printWarning'; + +/** + * Prints a warning message (yellow) to stdout. + * + * @param string $message The warning message + * @return IO + */ function printWarning(string $message): IO { return new IO(fn () => print("\033[33mWarning: {$message}\033[0m" . PHP_EOL)); } const printSuccess = '\Phunkie\Effect\Functions\console\printSuccess'; + +/** + * Prints a success message (green) to stdout. + * + * @param string $message The success message + * @return IO + */ function printSuccess(string $message): IO { return new IO(fn () => print("\033[32mSuccess: {$message}\033[0m" . PHP_EOL)); } const printInfo = '\Phunkie\Effect\Functions\console\printInfo'; + +/** + * Prints an info message (cyan) to stdout. + * + * @param string $message The info message + * @return IO + */ function printInfo(string $message): IO { return new IO(fn () => print("\033[36mInfo: {$message}\033[0m" . PHP_EOL)); } const printDebug = '\Phunkie\Effect\Functions\console\printDebug'; + +/** + * Prints a debug message (magenta) to stdout. + * + * @param string $message The debug message + * @return IO + */ function printDebug(string $message): IO { return new IO(fn () => print("\033[35mDebug: {$message}\033[0m" . PHP_EOL)); } const printTable = '\Phunkie\Effect\Functions\console\printTable'; + +/** + * Prints a formatted table to stdout. + * + * @param array> $data 2D array of data to print + * @return IO + */ function printTable(array $data): IO { return new IO(function () use ($data) { @@ -101,6 +165,14 @@ function printTable(array $data): IO } const printProgress = '\Phunkie\Effect\Functions\console\printProgress'; + +/** + * Prints a progress bar to stdout (in-place). + * + * @param int $current Current progress value + * @param int $total Total value + * @return IO + */ function printProgress(int $current, int $total): IO { return new IO(function () use ($current, $total) { @@ -115,6 +187,13 @@ function printProgress(int $current, int $total): IO } const printSpinner = '\Phunkie\Effect\Functions\console\printSpinner'; + +/** + * Prints a spinner animation with a message to stdout. + * + * @param string $message The message to display next to spinner + * @return IO + */ function printSpinner(string $message): IO { return new IO(function () use ($message) { diff --git a/src/Functions/io.php b/src/Functions/io.php index a6655b1..3bb5c3f 100644 --- a/src/Functions/io.php +++ b/src/Functions/io.php @@ -14,13 +14,15 @@ use Phunkie\Effect\IO\IO; /** - * Creates an IO from either a callable or a plain value. + * Creates an IO effect from a value or a callable. + * + * If the input is a callable, it is wrapped in an IO as a thunk (lazy execution). + * If the input is a value, it is wrapped in an IO that returns that value (pure). * * @template A - * @param callable|A $value + * @param callable(): A|A $value The side-effecting function or pure value * @return IO */ - const io = "\\Phunkie\\Effect\\Functions\\io\\io"; const IO = "\\Phunkie\\Effect\\Functions\\io\\io"; function io($value): IO diff --git a/src/IO/IO.php b/src/IO/IO.php index de1466b..a4e24d6 100644 --- a/src/IO/IO.php +++ b/src/IO/IO.php @@ -25,6 +25,17 @@ use Throwable; /** + * The IO Monad. + * + * IO represents a computation that performs side effects. + * It is lazily evaluated, meaning the side effects are not performed until + * `unsafeRun()` or `unsafeRunSync()` is called. + * + * It is the core data type of the effect system, supporting: + * - Sequential execution (flatMap) + * - Parallel execution (via Parallel type class) + * - Error handling + * * @template A */ class IO implements Functor, Applicative, Monad, Parallel, Kind @@ -36,16 +47,34 @@ class IO implements Functor, Applicative, Monad, Parallel, Kind private $unsafeRun; + /** + * @param callable(): A $unsafeRun The effectful computation + */ public function __construct(callable $unsafeRun) { $this->unsafeRun = $unsafeRun; } + /** + * Executes the effect and returns the result. + * + * WARNING: This method performs side effects. It should ideally only be called + * at the "end of the world" (e.g., in the main entry point of the application). + * + * @return A + * @throws Throwable + */ public function unsafeRun(): mixed { return ($this->unsafeRun)(); } + /** + * Executes the effect synchronously, awaiting any async handles. + * + * @return A + * @throws Throwable + */ public function unsafeRunSync(): mixed { $handle = ($this->unsafeRun)(); @@ -57,6 +86,13 @@ public function unsafeRunSync(): mixed return $handle; } + /** + * Handles an error that occurred during the execution of this IO. + * + * @template B + * @param callable(Throwable): B $handler Function to recover from the error + * @return IO + */ public function handleError(callable $handler): IO { return new IO(function () use ($handler) { @@ -69,6 +105,11 @@ public function handleError(callable $handler): IO } /** + * Attempts to execute the effect, capturing any error in a Validation. + * + * This mirrors the `attempt` behavior in Scala/Cats, where the result + * is lifted into an Either (here Validation) to handle errors as values. + * * @return IO> */ public function attempt(): IO @@ -76,11 +117,21 @@ public function attempt(): IO return new IO(fn () => Attempt($this->unsafeRun)); } + /** + * Returns the arity of the IO type constructor. + * + * @return int Always 1 for IO + */ public function getTypeArity(): int { return 1; } + /** + * Returns the type variable names for IO. + * + * @return array Always ['A'] + */ public function getTypeVariables(): array { return ['A']; diff --git a/src/IO/IOApp.php b/src/IO/IOApp.php index ac0c177..b57055d 100644 --- a/src/IO/IOApp.php +++ b/src/IO/IOApp.php @@ -21,7 +21,13 @@ abstract class IOApp { /** - * @return IO + * The main entry point for the application. + * + * This method is called by the runtime and should return an IO effect + * that produces an integer exit code. + * + * @param array|null $args Command line arguments + * @return IO The exit code (0 for success, non-zero for failure) */ abstract public function run(?array $args = []): IO; } diff --git a/src/Ops/IO/ApplicativeOps.php b/src/Ops/IO/ApplicativeOps.php index 7397d3e..ed2faa9 100644 --- a/src/Ops/IO/ApplicativeOps.php +++ b/src/Ops/IO/ApplicativeOps.php @@ -22,8 +22,10 @@ trait ApplicativeOps { /** + * Lifts a value into the IO context. + * * @template B - * @param B $a + * @param B $a The value to lift * @return IO */ public function pure(mixed $a): IO @@ -34,8 +36,10 @@ public function pure(mixed $a): IO } /** + * Applies the function contained in this IO to the value contained in another IO. + * * @template B - * @param Kind|IO $f + * @param Kind|IO $f The IO containing the value * @return IO */ public function apply(Kind|IO $f): IO @@ -52,10 +56,12 @@ public function apply(Kind|IO $f): IO } /** + * Map a function over two IO values. + * * @template B * @template C - * @param Kind|IO $fb - * @param callable(mixed,B):C $f + * @param Kind|IO $fb The second IO value + * @param callable(mixed,B):C $f The function to apply * @return IO */ public function map2(Kind|IO $fb, callable $f): IO diff --git a/src/Ops/IO/FunctorOps.php b/src/Ops/IO/FunctorOps.php index 7f590c7..8047778 100644 --- a/src/Ops/IO/FunctorOps.php +++ b/src/Ops/IO/FunctorOps.php @@ -23,8 +23,10 @@ trait FunctorOps { /** + * Maps a function over the IO value. + * * @template B - * @param callable(A): B $f + * @param callable(A): B $f The function to apply * @return IO */ public function map(callable $f): IO @@ -35,8 +37,10 @@ public function map(callable $f): IO } /** + * Lifts a function to operate on IO values. + * * @template B - * @param callable(A): B $f + * @param callable(A): B $f The function to lift * @return callable(IO): IO */ public function lift($f): callable @@ -47,8 +51,10 @@ public function lift($f): callable } /** + * Replaces the IO value with a constant value. + * * @template B - * @param B $b + * @param B $b The constant value * @return IO */ public function as(mixed $b): IO @@ -59,6 +65,8 @@ public function as(mixed $b): IO } /** + * Discards the IO value and returns Unit. + * * @return IO */ public function void(): IO @@ -67,13 +75,12 @@ public function void(): IO } /** + * Paris the IO value with the result of applying a function to it. + * * @template B - * @param callable(A): B $f + * @param callable(A): B $f The function to produce the second element * @return IO> */ - /** - * @param callable $f - */ public function zipWith($f): IO { return $this->map(function ($a) use ($f): Pair { @@ -82,6 +89,9 @@ public function zipWith($f): IO } /** + * Invariant map (not used for IO, but part of Functor in some contexts). + * IO is covariant, so this delegates to map. + * * @template B * @param callable(A): B $f * @param callable(B): A $g diff --git a/src/Ops/IO/MonadOps.php b/src/Ops/IO/MonadOps.php index 243eafa..ad5634a 100644 --- a/src/Ops/IO/MonadOps.php +++ b/src/Ops/IO/MonadOps.php @@ -19,8 +19,10 @@ trait MonadOps { /** + * Binds a function to the IO value, returning a new IO. + * * @template B - * @param callable(A):IO $f + * @param callable(A):IO $f The function to bind * @return IO */ public function flatMap(callable $f): IO @@ -33,7 +35,9 @@ public function flatMap(callable $f): IO } /** - * @return IO where B is the type parameter of the inner IO + * Flattens a nested IO structure (IO> -> IO). + * + * @return IO where A is the type parameter of the inner IO */ public function flatten(): IO { diff --git a/src/Ops/IO/ParallelOps.php b/src/Ops/IO/ParallelOps.php index 85254b4..1d9fca2 100644 --- a/src/Ops/IO/ParallelOps.php +++ b/src/Ops/IO/ParallelOps.php @@ -26,10 +26,13 @@ trait ParallelOps { /** + * Combines two parallel computations using a combining function. + * Starts execution of both computations immediately (if in a parallel context). + * * @template B * @template C - * @param Parallel $fb - * @param callable(A, B): C $f + * @param Parallel $fb The second computation + * @param callable(A, B): C $f The combining function * @return IO */ public function parMap2(Parallel $fb, callable $f): IO @@ -54,11 +57,13 @@ public function parMap2(Parallel $fb, callable $f): IO } /** + * Combines three parallel computations using a combining function. + * * @template B * @template C - * @param Parallel $fb - * @param Parallel $fc - * @param callable(A, B, C): IO $f + * @param Parallel $fb The second computation + * @param Parallel $fc The third computation + * @param callable(A, B, C): IO $f The combining function * @return IO */ public function parMap3(Parallel $fb, Parallel $fc, callable $f): IO @@ -89,10 +94,12 @@ public function parMap3(Parallel $fb, Parallel $fc, callable $f): IO } /** + * Combines multiple parallel computations using a combining function. + * * @template B * @template C - * @param array> $fbs - * @param callable(B ...$args): C $f + * @param array> $fbs Array of computations + * @param callable(B ...$args): C $f The combining function * @return IO */ public function parMapN(array $fbs, callable $f): IO From 415ead27e2b5a3c5ac8068d498e06984d099794c Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Sat, 6 Dec 2025 18:47:44 +0000 Subject: [PATCH 02/23] Fix Effect documentation index to only link to existing files --- docs/index.md | 42 +++++++----------------------------------- 1 file changed, 7 insertions(+), 35 deletions(-) diff --git a/docs/index.md b/docs/index.md index 5ee3b03..f310c98 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,22 +4,22 @@ A functional effects library for PHP inspired by Scala's cats-effect. Phunkie Ef ## Table of Contents -1. [Introduction](introduction.md) - - What is Phunkie Effect? - - Philosophy and design goals - - When to use Phunkie Effect - -2. [Getting Started](getting-started.md) +1. [Getting Started](getting-started.md) - Installation - Basic Usage - Your First Effect -3. [IO App and IO Console](io-app.md) +2. [IO App and IO Console](io-app.md) - Console Functions - Creating an IO App - Exit Codes and Error Handling - Best Practices +3. [Combinators](combinators.md) + - Core IO Operations + - Error Handling + - Composition Patterns + 4. [Concurrency](concurrency.md) - Blockers - Execution Context @@ -37,34 +37,6 @@ A functional effects library for PHP inspired by Scala's cats-effect. Phunkie Ef - URL - Database -6. [Networks](networks.md) - - TCP - - Server - - Client - - Connection Management - - UDP - - Datagram Handling - - Broadcasting - - Multicasting - -7. [Cookbook](cookbook.md) - - Supervisor Patterns - - One-for-One Strategy - - All-for-One Strategy - - Custom Supervision - - Shared State with Ref - - Atomic Updates - - Cross-Fiber Communication - - Graceful Shutdown - - Resource Cleanup - - Signal Handling - - Circuit Breakers - - Failure Detection - - Automatic Recovery - - Rate Limiting - - Token Bucket - - Leaky Bucket - ## Contributing Contributions are welcome! Please feel free to submit a Pull Request. From 8aa161df65e5786f1523bef5072cb4ac83a86241 Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Sat, 6 Dec 2025 18:52:06 +0000 Subject: [PATCH 03/23] Add Introduction documentation for Effect --- docs/index.md | 15 +++-- docs/introduction.md | 147 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 docs/introduction.md diff --git a/docs/index.md b/docs/index.md index f310c98..ffd24f9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,23 +4,28 @@ A functional effects library for PHP inspired by Scala's cats-effect. Phunkie Ef ## Table of Contents -1. [Getting Started](getting-started.md) +1. [Introduction](introduction.md) + - What is Phunkie Effect? + - Philosophy and design goals + - When to use Phunkie Effect + +2. [Getting Started](getting-started.md) - Installation - Basic Usage - Your First Effect -2. [IO App and IO Console](io-app.md) +3. [IO App and IO Console](io-app.md) - Console Functions - Creating an IO App - Exit Codes and Error Handling - Best Practices -3. [Combinators](combinators.md) +4. [Combinators](combinators.md) - Core IO Operations - Error Handling - Composition Patterns -4. [Concurrency](concurrency.md) +5. [Concurrency](concurrency.md) - Blockers - Execution Context - Parallel Execution @@ -30,7 +35,7 @@ A functional effects library for PHP inspired by Scala's cats-effect. Phunkie Ef - Delay - Channels -5. [Resources](resources.md) +6. [Resources](resources.md) - Brackets - Resource Combinators - Files diff --git a/docs/introduction.md b/docs/introduction.md new file mode 100644 index 0000000..e7604e6 --- /dev/null +++ b/docs/introduction.md @@ -0,0 +1,147 @@ +# Introduction to Phunkie Effect + +## What is Phunkie Effect? + +Phunkie Effect is a functional effects library for PHP that brings the power of purely functional programming to side effect management. Inspired by Scala's cats-effect and built on top of the Phunkie functional programming library, it provides a robust framework for writing predictable, composable, and testable code. + +At its core, Phunkie Effect introduces the `IO` monad - a data structure that represents a computation that may perform side effects. Unlike traditional imperative code where side effects happen immediately, `IO` allows you to describe what should happen without actually executing it. This separation of description from execution is the key to writing purely functional code in PHP. + +```php +use Phunkie\Effect\IO; + +// This doesn't execute anything - it just describes what should happen +$program = IO::of(fn() => file_get_contents('config.json')) + ->map(fn($json) => json_decode($json, true)) + ->flatMap(fn($config) => IO::of(fn() => connectToDatabase($config['db']))); + +// The program only runs when you explicitly call unsafeRun() +$result = $program->unsafeRun(); +``` + +## Philosophy and Design Goals + +### Referential Transparency + +Phunkie Effect is built on the principle of referential transparency - the idea that you should be able to replace an expression with its value without changing the program's behavior. This makes code easier to reason about, test, and refactor. + +```php +// Without IO - side effects happen immediately +$data = file_get_contents('data.txt'); // Executes now! +$result = processData($data); + +// With IO - side effects are described, not executed +$program = IO::of(fn() => file_get_contents('data.txt')) + ->map(fn($data) => processData($data)); +// Nothing has executed yet - we can compose, test, and reason about it +``` + +### Composability + +Effects should compose just like pure functions. Phunkie Effect provides a rich set of combinators that allow you to build complex programs from simple building blocks. + +```php +$readConfig = IO::of(fn() => file_get_contents('config.json')); +$parseJson = fn($json) => IO::of(fn() => json_decode($json, true)); +$validateConfig = fn($config) => IO::of(fn() => validate($config)); + +// Compose them together +$program = $readConfig + ->flatMap($parseJson) + ->flatMap($validateConfig); +``` + +### Resource Safety + +Managing resources (files, database connections, network sockets) is error-prone. Phunkie Effect provides automatic resource management through brackets and the Resource type, ensuring cleanup happens even when errors occur. + +```php +use Phunkie\Effect\Resource; + +$program = Resource::make( + IO::of(fn() => fopen('file.txt', 'r')), + fn($handle) => IO::of(fn() => fclose($handle)) +)->use(fn($handle) => + IO::of(fn() => fread($handle, 1024)) +); +``` + +### Testability + +By separating effect description from execution, your code becomes trivially testable. You can test the logic without performing actual side effects. + +```php +// Your business logic returns IO values +function processOrder(Order $order): IO { + return saveToDatabase($order) + ->flatMap(fn() => sendConfirmationEmail($order->email)) + ->flatMap(fn() => updateInventory($order->items)); +} + +// In tests, you can inspect the IO structure without executing it +$io = processOrder($testOrder); +// Assert on the structure, or provide test implementations +``` + +### Concurrency and Parallelism + +Phunkie Effect provides safe, composable concurrency primitives that make it easy to write concurrent programs without the typical pitfalls of shared mutable state. + +```php +use Phunkie\Effect\IO; + +// Run multiple effects in parallel +$results = IO::parSequence([ + fetchUserData($userId), + fetchOrderHistory($userId), + fetchPreferences($userId) +]); +``` + +## When to Use Phunkie Effect + +### Perfect For: + +**Complex Applications with Many Side Effects** +- Web applications with database access, external APIs, file I/O +- CLI tools that interact with the filesystem and network +- Background workers and job processors + +**Applications Requiring High Reliability** +- Financial systems where correctness is critical +- Healthcare applications with strict error handling requirements +- Any system where bugs have serious consequences + +**Concurrent and Parallel Processing** +- Data processing pipelines +- Microservices that need to coordinate multiple operations +- Applications that benefit from parallel execution + +**Testable Code** +- When you need comprehensive test coverage +- When you want to test business logic without hitting real databases/APIs +- When you need to verify error handling paths + +### Consider Alternatives When: + +**Simple Scripts** +- One-off scripts with minimal side effects +- Quick prototypes where correctness isn't critical + +**Performance-Critical Hot Paths** +- While Phunkie Effect is reasonably performant, the abstraction overhead may not be suitable for extremely performance-sensitive code +- Consider using it for the application structure and dropping down to imperative code for hot paths + +**Team Unfamiliar with Functional Programming** +- There's a learning curve to functional programming concepts +- Ensure your team is willing to invest in learning these patterns + +## Getting Started + +Ready to dive in? Check out the [Getting Started](getting-started.md) guide to install Phunkie Effect and write your first effectful program. + +## Learn More + +- [IO App and Console](io-app.md) - Build command-line applications +- [Combinators](combinators.md) - Core operations for working with IO +- [Concurrency](concurrency.md) - Parallel and concurrent execution +- [Resources](resources.md) - Safe resource management From 15509dfd3820c062f5b7ed6ebc1648fb2007672a Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Sat, 6 Dec 2025 18:57:24 +0000 Subject: [PATCH 04/23] Fix Effect introduction to use correct API: io() and bracket() --- docs/introduction.md | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/docs/introduction.md b/docs/introduction.md index e7604e6..f9a126a 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -10,9 +10,9 @@ At its core, Phunkie Effect introduces the `IO` monad - a data structure that re use Phunkie\Effect\IO; // This doesn't execute anything - it just describes what should happen -$program = IO::of(fn() => file_get_contents('config.json')) +$program = io(fn() => file_get_contents('config.json')) ->map(fn($json) => json_decode($json, true)) - ->flatMap(fn($config) => IO::of(fn() => connectToDatabase($config['db']))); + ->flatMap(fn($config) => io(fn() => connectToDatabase($config['db']))); // The program only runs when you explicitly call unsafeRun() $result = $program->unsafeRun(); @@ -30,7 +30,7 @@ $data = file_get_contents('data.txt'); // Executes now! $result = processData($data); // With IO - side effects are described, not executed -$program = IO::of(fn() => file_get_contents('data.txt')) +$program = io(fn() => file_get_contents('data.txt')) ->map(fn($data) => processData($data)); // Nothing has executed yet - we can compose, test, and reason about it ``` @@ -40,9 +40,9 @@ $program = IO::of(fn() => file_get_contents('data.txt')) Effects should compose just like pure functions. Phunkie Effect provides a rich set of combinators that allow you to build complex programs from simple building blocks. ```php -$readConfig = IO::of(fn() => file_get_contents('config.json')); -$parseJson = fn($json) => IO::of(fn() => json_decode($json, true)); -$validateConfig = fn($config) => IO::of(fn() => validate($config)); +$readConfig = io(fn() => file_get_contents('config.json')); +$parseJson = fn($json) => io(fn() => json_decode($json, true)); +$validateConfig = fn($config) => io(fn() => validate($config)); // Compose them together $program = $readConfig @@ -52,16 +52,18 @@ $program = $readConfig ### Resource Safety -Managing resources (files, database connections, network sockets) is error-prone. Phunkie Effect provides automatic resource management through brackets and the Resource type, ensuring cleanup happens even when errors occur. +Managing resources (files, database connections, network sockets) is error-prone. Phunkie Effect provides automatic resource management through brackets, ensuring cleanup happens even when errors occur. ```php -use Phunkie\Effect\Resource; - -$program = Resource::make( - IO::of(fn() => fopen('file.txt', 'r')), - fn($handle) => IO::of(fn() => fclose($handle)) -)->use(fn($handle) => - IO::of(fn() => fread($handle, 1024)) +use function Phunkie\Effect\Functions\blocking\blocking; + +$result = bracket( + // Acquire + blocking(fn() => fopen('file.txt', 'r')), + // Use + fn($handle) => blocking(fn() => fread($handle, 1024)), + // Release + fn($handle) => blocking(fn() => fclose($handle)) ); ``` @@ -90,7 +92,7 @@ Phunkie Effect provides safe, composable concurrency primitives that make it eas use Phunkie\Effect\IO; // Run multiple effects in parallel -$results = IO::parSequence([ +$results = parSequence([ fetchUserData($userId), fetchOrderHistory($userId), fetchPreferences($userId) From 0a06dccbe4a9d0dc12cbe4c026d318ada6256efd Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Sat, 6 Dec 2025 18:59:32 +0000 Subject: [PATCH 05/23] Fix parallel execution example to use parMap2 instead of non-existent parSequence --- docs/introduction.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/introduction.md b/docs/introduction.md index f9a126a..b8c4d0f 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -89,14 +89,12 @@ $io = processOrder($testOrder); Phunkie Effect provides safe, composable concurrency primitives that make it easy to write concurrent programs without the typical pitfalls of shared mutable state. ```php -use Phunkie\Effect\IO; +use function Phunkie\Effect\Functions\io\io; // Run multiple effects in parallel -$results = parSequence([ - fetchUserData($userId), - fetchOrderHistory($userId), - fetchPreferences($userId) -]); +$result = $fetchUserData + ->parMap2($fetchOrderHistory, fn($user, $orders) => [$user, $orders]) + ->unsafeRun(); ``` ## When to Use Phunkie Effect From 3d1a38ff377194db076d31fa3ea3bfb963eade70 Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Sat, 6 Dec 2025 19:02:13 +0000 Subject: [PATCH 06/23] Improve Effect index with better section headers for TOC sidebar --- docs/index.md | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/docs/index.md b/docs/index.md index ffd24f9..f097db8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,30 +2,50 @@ A functional effects library for PHP inspired by Scala's cats-effect. Phunkie Effects provides a way to manage side effects in a purely functional way, making your code more predictable, testable, and maintainable. -## Table of Contents +## Overview -1. [Introduction](introduction.md) +Phunkie Effect brings the power of purely functional programming to PHP, allowing you to describe side effects as values that can be composed, tested, and reasoned about before execution. + +## Getting Started + +New to Phunkie Effect? Start here: + +- [Introduction](introduction.md) - Learn what Phunkie Effect is and when to use it +- [Getting Started](getting-started.md) - Installation and your first effect +- [IO App and Console](io-app.md) - Build command-line applications + +## Core Concepts + +Master the fundamental building blocks: + +- [Combinators](combinators.md) - Core IO operations and composition patterns +- [Resources](resources.md) - Safe resource management with brackets +- [Concurrency](concurrency.md) - Parallel execution and concurrent programming + +## Documentation Index + +### 1. [Introduction](introduction.md) - What is Phunkie Effect? - Philosophy and design goals - When to use Phunkie Effect -2. [Getting Started](getting-started.md) +### 2. [Getting Started](getting-started.md) - Installation - Basic Usage - Your First Effect -3. [IO App and IO Console](io-app.md) +### 3. [IO App and IO Console](io-app.md) - Console Functions - Creating an IO App - Exit Codes and Error Handling - Best Practices -4. [Combinators](combinators.md) +### 4. [Combinators](combinators.md) - Core IO Operations - Error Handling - Composition Patterns -5. [Concurrency](concurrency.md) +### 5. [Concurrency](concurrency.md) - Blockers - Execution Context - Parallel Execution @@ -35,7 +55,7 @@ A functional effects library for PHP inspired by Scala's cats-effect. Phunkie Ef - Delay - Channels -6. [Resources](resources.md) +### 6. [Resources](resources.md) - Brackets - Resource Combinators - Files @@ -48,7 +68,7 @@ Contributions are welcome! Please feel free to submit a Pull Request. ## License -This project is licensed under the MIT License - see the LICENSE file for details. +This project is licensed under the MIT Licence - see the LICENSE file for details. ## Acknowledgments From 9f96e152a4f41cf28500fc0adf7a2fb13e20330b Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Sun, 7 Dec 2025 08:07:38 +0000 Subject: [PATCH 07/23] Update License to Licence (British English) --- docs/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index f097db8..3eaa174 100644 --- a/docs/index.md +++ b/docs/index.md @@ -66,9 +66,9 @@ Master the fundamental building blocks: Contributions are welcome! Please feel free to submit a Pull Request. -## License +## Licence -This project is licensed under the MIT Licence - see the LICENSE file for details. +This project is licensed under the MIT Licence - see the LICENCE file for details. ## Acknowledgments From f77b4ffd1087da390c58aed4c058a05881caf4e9 Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Sun, 7 Dec 2025 17:19:13 +0000 Subject: [PATCH 08/23] Add lint, test-all, check scripts and pre-commit hook --- composer.json | 11 ++++++++++- scripts/pre-commit | 30 ++++++++++++++++++++++++++++++ scripts/setup.sh | 32 ++++++++++++++++++++++++++++++++ scripts/test-all-versions.sh | 36 ++++++++++++++++++++++++++++++++++++ 4 files changed, 108 insertions(+), 1 deletion(-) create mode 100755 scripts/pre-commit create mode 100755 scripts/setup.sh create mode 100755 scripts/test-all-versions.sh diff --git a/composer.json b/composer.json index b070656..5267d70 100644 --- a/composer.json +++ b/composer.json @@ -39,7 +39,16 @@ "test": "phpunit", "phpstan": "phpstan analyse", "cs-fix": "php-cs-fixer fix --allow-risky=yes", - "cs-check": "php-cs-fixer fix --dry-run --diff --allow-risky=yes" + "cs-check": "php-cs-fixer fix --dry-run --diff --allow-risky=yes", + "lint": [ + "@cs-check", + "@phpstan" + ], + "test-all": "scripts/test-all-versions.sh", + "check": [ + "@lint", + "@test" + ] }, "minimum-stability": "dev", "prefer-stable": true, diff --git a/scripts/pre-commit b/scripts/pre-commit new file mode 100755 index 0000000..3f8a882 --- /dev/null +++ b/scripts/pre-commit @@ -0,0 +1,30 @@ +#!/bin/bash + +# Pre-commit hook for Phunkie projects +# Runs lint and tests before allowing commit + +set -e + +echo "๐Ÿ” Running pre-commit checks..." + +# Run lint (cs-check + phpstan) +echo "" +echo "๐Ÿ“‹ Running lint checks..." +if ! composer lint; then + echo "" + echo "โŒ Lint checks failed. Please fix the issues before committing." + echo " Run 'composer cs-fix' to auto-fix code style issues." + exit 1 +fi + +# Run tests +echo "" +echo "๐Ÿงช Running tests..." +if ! composer test; then + echo "" + echo "โŒ Tests failed. Please fix the issues before committing." + exit 1 +fi + +echo "" +echo "โœ… All pre-commit checks passed!" diff --git a/scripts/setup.sh b/scripts/setup.sh new file mode 100755 index 0000000..298d99c --- /dev/null +++ b/scripts/setup.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +# Setup script for development environment +# Installs git hooks and dependencies + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" + +echo "๐Ÿ”ง Setting up development environment..." + +# Install composer dependencies +echo "" +echo "๐Ÿ“ฆ Installing dependencies..." +composer install + +# Install pre-commit hook +echo "" +echo "๐Ÿช Installing git hooks..." +cp "$SCRIPT_DIR/pre-commit" "$PROJECT_ROOT/.git/hooks/pre-commit" +chmod +x "$PROJECT_ROOT/.git/hooks/pre-commit" + +echo "" +echo "โœ… Development environment setup complete!" +echo "" +echo "Available commands:" +echo " composer test - Run tests" +echo " composer lint - Run code style and static analysis" +echo " composer cs-fix - Auto-fix code style issues" +echo " composer check - Run lint + tests" +echo " composer test-all - Run tests on all PHP versions (requires Docker)" diff --git a/scripts/test-all-versions.sh b/scripts/test-all-versions.sh new file mode 100755 index 0000000..eca53c9 --- /dev/null +++ b/scripts/test-all-versions.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Test against all supported PHP versions using Docker +# Requires Docker to be installed and running + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" + +PHP_VERSIONS=("8.2" "8.3" "8.4") + +echo "==========================================" +echo "Testing against PHP versions: ${PHP_VERSIONS[*]}" +echo "==========================================" + +for version in "${PHP_VERSIONS[@]}"; do + echo "" + echo "==========================================" + echo "Testing PHP $version" + echo "==========================================" + + docker run --rm -v "$PROJECT_ROOT:/app" -w /app \ + "php:${version}-cli" \ + sh -c "apt-get update && apt-get install -y git unzip && \ + curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer && \ + composer install --no-interaction --prefer-dist && \ + composer test" + + echo "โœ… PHP $version tests passed" +done + +echo "" +echo "==========================================" +echo "โœ… All PHP versions passed!" +echo "==========================================" From 16f5a5c3df3c117b733feddb134e16ab4de6876d Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Sun, 7 Dec 2025 17:28:23 +0000 Subject: [PATCH 09/23] Update test-all to include lint + static analysis --- scripts/test-all-versions.sh | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/scripts/test-all-versions.sh b/scripts/test-all-versions.sh index eca53c9..e7faf68 100755 --- a/scripts/test-all-versions.sh +++ b/scripts/test-all-versions.sh @@ -11,26 +11,30 @@ PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" PHP_VERSIONS=("8.2" "8.3" "8.4") echo "==========================================" -echo "Testing against PHP versions: ${PHP_VERSIONS[*]}" +echo "Running lint + tests on PHP versions: ${PHP_VERSIONS[*]}" echo "==========================================" for version in "${PHP_VERSIONS[@]}"; do echo "" echo "==========================================" - echo "Testing PHP $version" + echo "PHP $version - Installing dependencies..." echo "==========================================" docker run --rm -v "$PROJECT_ROOT:/app" -w /app \ "php:${version}-cli" \ - sh -c "apt-get update && apt-get install -y git unzip && \ - curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer && \ - composer install --no-interaction --prefer-dist && \ + sh -c "apt-get update -qq && apt-get install -y -qq git unzip > /dev/null && \ + curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer > /dev/null && \ + composer install --no-interaction --prefer-dist --quiet && \ + echo '--- Running lint (cs-check + phpstan) ---' && \ + composer lint && \ + echo '--- Running tests ---' && \ composer test" - echo "โœ… PHP $version tests passed" + echo "โœ… PHP $version passed (lint + tests)" done echo "" echo "==========================================" echo "โœ… All PHP versions passed!" echo "==========================================" + From 0d78197950b77139d9a721bb13ef994b92594bdd Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Sun, 7 Dec 2025 17:31:31 +0000 Subject: [PATCH 10/23] Increase PHPStan memory limit to 512M --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 5267d70..c4d2416 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,7 @@ }, "scripts": { "test": "phpunit", - "phpstan": "phpstan analyse", + "phpstan": "phpstan analyse --memory-limit=512M", "cs-fix": "php-cs-fixer fix --allow-risky=yes", "cs-check": "php-cs-fixer fix --dry-run --diff --allow-risky=yes", "lint": [ From 18fb08f1a65c0b0bdbbbb13e428c4684b2de92eb Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Sun, 7 Dec 2025 17:42:16 +0000 Subject: [PATCH 11/23] Upgrade php-cs-fixer to ^3.90 for PHP 8.4 support --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index c4d2416..87a27c5 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "phpunit/phpunit": "^10.5", "phunkie/console": "^1.0.0", "phpstan/phpstan": "^2.1", - "friendsofphp/php-cs-fixer": "^3.75" + "friendsofphp/php-cs-fixer": "^3.90" }, "suggest": { "ext-parallel": "Required for parallel execution using threads. PHP must be compiled with ZTS support." From f664f617a5b5f91f3230385369558d3b23953e89 Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Sun, 7 Dec 2025 17:59:47 +0000 Subject: [PATCH 12/23] Upgrade php-cs-fixer to ^3.90, fix path repos and version aliases --- composer.json | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 87a27c5..50b28cc 100644 --- a/composer.json +++ b/composer.json @@ -9,13 +9,22 @@ "email": "marcello.duarte@gmail.com" } ], + "repositories": [ + { + "type": "path", + "url": "../phunkie" + }, + { + "type": "path", + "url": "../console" + } + ], "require": { "php": "^8.2 || ^8.3 || ^8.4", - "phunkie/phunkie": "^1.0.0" + "phunkie/phunkie": "dev-developing-1.0.0 as 1.0.0" }, "require-dev": { "phpunit/phpunit": "^10.5", - "phunkie/console": "^1.0.0", "phpstan/phpstan": "^2.1", "friendsofphp/php-cs-fixer": "^3.90" }, From 2a3da9b15a8c92a3c070d4bc6b1f1838f0239a55 Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Mon, 8 Dec 2025 10:56:24 +0000 Subject: [PATCH 13/23] =?UTF-8?q?Fix=20ParallelOps=20logic=20bug=20(&&?= =?UTF-8?q?=E2=86=92||),=20add=20getUnsafeRun=20to=20Parallel=20interface,?= =?UTF-8?q?=20clean=20up=20PHPStan=20ignores?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- phpstan.neon | 28 ++++++++-------------------- src/Cats/Parallel.php | 7 +++++++ src/IO/IO.php | 19 +++++++++++++++++-- src/Ops/IO/FunctorOps.php | 9 ++++++--- src/Ops/IO/ParallelOps.php | 35 ++++++++++++++++++++++------------- 5 files changed, 60 insertions(+), 38 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index fbe0db0..e6543d9 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -5,24 +5,12 @@ parameters: excludePaths: - src/Functions/common.php ignoreErrors: - # Allow mixed types for now in Ops traits (will be fixed as we add type classes) - - '#Method .+Ops::.+ has parameter \$.+ with no value type specified in iterable type array#' - - '#Method .+Ops::.+ return type has no value type specified in iterable type array#' - - '#Function .+ has parameter \$.+ with no value type specified in iterable type array#' - - '#Function .+ return type with generic class .+ does not specify its types#' - # Allow new static() in traits - will be refactored in Phase 1 - - '#Unsafe usage of new static\(\)#' - # Allow undefined property access in traits that access parent class properties - - '#Access to an undefined property .+::\$unsafeRun#' - # Generic type issues - will be fixed with proper type classes - - '#PHPDoc tag @return contains generic type .+ but (class|interface) .+ is not generic#' - - '#PHPDoc tag @param .+ contains generic type .+ but (class|interface) .+ is not generic#' - - '#PHPDoc tag @return with type .+ is not subtype of native type#' - - '#PHPDoc tag @param .+ with type .+ is not subtype of native type#' - - '#Method .+ has invalid return type .+\\Ops\\B#' - - '#PHPDoc tag @template .+ is not referenced in a parameter#' - - '#has parameter \$.+ with no type specified#' - - '#Method .+ should return .+ but returns#' - - '#Call to an undefined method .+::apply\(\)#' - - '#Template type B of method .+::flatMap\(\) is not referenced in a parameter#' + # HKT Limitation: PHPStan can't express that Kind is a higher-kinded type marker + # Kind is used to simulate HKTs in PHP - it's intentionally not generic + - '#PHPDoc tag @param .+ contains generic type Phunkie\\Types\\Kind\<.+\> but interface Phunkie\\Types\\Kind is not generic#' + + # Generic closure inference: PHPStan can't infer generic types through closures/lambdas + # These methods return correctly typed values but PHPStan loses the type info + - '#Method .+::zipWith\(\) should return .+ but returns .+Pair\>#' + - '#Method .+::attempt\(\) should return .+ but returns .+Validation\ The result of combined computation */ public function parMap2(Parallel $fb, callable $f): Parallel; + + /** + * Returns the underlying unsafe runner. + * + * @return callable(): A + */ + public function getUnsafeRun(): callable; } diff --git a/src/IO/IO.php b/src/IO/IO.php index a4e24d6..4d9bb9f 100644 --- a/src/IO/IO.php +++ b/src/IO/IO.php @@ -69,6 +69,16 @@ public function unsafeRun(): mixed return ($this->unsafeRun)(); } + /** + * Returns the underlying unsafe runner callable. + * + * @return callable(): A + */ + public function getUnsafeRun(): callable + { + return $this->unsafeRun; + } + /** * Executes the effect synchronously, awaiting any async handles. * @@ -110,11 +120,16 @@ public function handleError(callable $handler): IO * This mirrors the `attempt` behavior in Scala/Cats, where the result * is lifted into an Either (here Validation) to handle errors as values. * - * @return IO> + * @return IO> IO containing either Success(A) or Failure(Throwable) */ public function attempt(): IO { - return new IO(fn () => Attempt($this->unsafeRun)); + $run = $this->unsafeRun; + + return new IO( + /** @return Validation<\Throwable, A> */ + fn () => Attempt($run) + ); } /** diff --git a/src/Ops/IO/FunctorOps.php b/src/Ops/IO/FunctorOps.php index 8047778..d1ea8b7 100644 --- a/src/Ops/IO/FunctorOps.php +++ b/src/Ops/IO/FunctorOps.php @@ -83,9 +83,12 @@ public function void(): IO */ public function zipWith($f): IO { - return $this->map(function ($a) use ($f): Pair { - return Pair($a, $f($a)); - }); + return $this->map( + /** @return Pair */ + function ($a) use ($f): Pair { + return Pair($a, $f($a)); + } + ); } /** diff --git a/src/Ops/IO/ParallelOps.php b/src/Ops/IO/ParallelOps.php index 1d9fca2..c1e5c53 100644 --- a/src/Ops/IO/ParallelOps.php +++ b/src/Ops/IO/ParallelOps.php @@ -38,16 +38,18 @@ trait ParallelOps public function parMap2(Parallel $fb, callable $f): IO { return io(function () use ($fb, $f) { - if (! $this->unsafeRun instanceof Blocker && ! $this->unsafeRun->blockingContext() instanceof ParallelExecutionContext) { + $runA = $this->unsafeRun; + if (! $runA instanceof Blocker || ! $runA->blockingContext() instanceof ParallelExecutionContext) { throw new \Exception("First effect Blocker isn't in a parallel context"); } - if (! $fb->unsafeRun instanceof Blocker && ! $fb->unsafeRun->blockingContext() instanceof ParallelExecutionContext) { + $runB = $fb->getUnsafeRun(); + if (! $runB instanceof Blocker || ! $runB->blockingContext() instanceof ParallelExecutionContext) { throw new \Exception("Second effect Blocker isn't in a parallel context"); } - $handle1 = ($this->unsafeRun)(); - $handle2 = ($fb->unsafeRun)(); + $handle1 = $runA(); + $handle2 = $runB(); $a = $handle1->await(); $b = $handle2->await(); @@ -69,21 +71,24 @@ public function parMap2(Parallel $fb, callable $f): IO public function parMap3(Parallel $fb, Parallel $fc, callable $f): IO { return io(function () use ($fb, $fc, $f) { - if (! $this->unsafeRun instanceof Blocker && ! $this->unsafeRun->blockingContext() instanceof ParallelExecutionContext) { + $runA = $this->unsafeRun; + if (! $runA instanceof Blocker || ! $runA->blockingContext() instanceof ParallelExecutionContext) { throw new \Exception("First effect Blocker isn't in a parallel context"); } - if (! $fb->unsafeRun instanceof Blocker && ! $fb->unsafeRun->blockingContext() instanceof ParallelExecutionContext) { + $runB = $fb->getUnsafeRun(); + if (! $runB instanceof Blocker || ! $runB->blockingContext() instanceof ParallelExecutionContext) { throw new \Exception("Second effect Blocker isn't in a parallel context"); } - if (! $fc->unsafeRun instanceof Blocker && ! $fc->unsafeRun->blockingContext() instanceof ParallelExecutionContext) { + $runC = $fc->getUnsafeRun(); + if (! $runC instanceof Blocker || ! $runC->blockingContext() instanceof ParallelExecutionContext) { throw new \Exception("Third effect Blocker isn't in a parallel context"); } - $handle1 = ($this->unsafeRun)(); - $handle2 = ($fb->unsafeRun)(); - $handle3 = ($fc->unsafeRun)(); + $handle1 = $runA(); + $handle2 = $runB(); + $handle3 = $runC(); $a = $handle1->await(); $b = $handle2->await(); @@ -105,17 +110,21 @@ public function parMap3(Parallel $fb, Parallel $fc, callable $f): IO public function parMapN(array $fbs, callable $f): IO { return io(function () use ($fbs, $f) { - if (! $this->unsafeRun instanceof Blocker && ! $this->unsafeRun->blockingContext() instanceof ParallelExecutionContext) { + $runA = $this->unsafeRun; + if (! $runA instanceof Blocker || ! $runA->blockingContext() instanceof ParallelExecutionContext) { throw new \Exception("First effect Blocker isn't in a parallel context"); } + $runners = []; foreach ($fbs as $fb) { - if (! $fb->unsafeRun instanceof Blocker && ! $fb->unsafeRun->blockingContext() instanceof ParallelExecutionContext) { + $runner = $fb->getUnsafeRun(); + if (! $runner instanceof Blocker || ! $runner->blockingContext() instanceof ParallelExecutionContext) { throw new \Exception("Effect Blocker isn't in a parallel context"); } + $runners[] = $runner; } - $handles = array_map(fn ($fb) => ($fb->unsafeRun)(), $fbs); + $handles = array_map(fn (Blocker $runner) => $runner(), $runners); $results = array_map(fn ($handle) => $handle->await(), $handles); From ea55dd81d9af4a202774e628168c698e10841d91 Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Mon, 8 Dec 2025 11:09:16 +0000 Subject: [PATCH 14/23] Add phunkie/phpstan extension for call-site type inference --- composer.json | 3 ++- phpstan.neon | 13 +++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index 50b28cc..5a38b99 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ }, { "type": "path", - "url": "../console" + "url": "../phpstan" } ], "require": { @@ -26,6 +26,7 @@ "require-dev": { "phpunit/phpunit": "^10.5", "phpstan/phpstan": "^2.1", + "phunkie/phpstan": "@dev", "friendsofphp/php-cs-fixer": "^3.90" }, "suggest": { diff --git a/phpstan.neon b/phpstan.neon index e6543d9..d955c7b 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,3 +1,6 @@ +includes: + - vendor/phunkie/phpstan/extension.neon + parameters: level: 5 paths: @@ -9,8 +12,10 @@ parameters: # Kind is used to simulate HKTs in PHP - it's intentionally not generic - '#PHPDoc tag @param .+ contains generic type Phunkie\\Types\\Kind\<.+\> but interface Phunkie\\Types\\Kind is not generic#' - # Generic closure inference: PHPStan can't infer generic types through closures/lambdas - # These methods return correctly typed values but PHPStan loses the type info - - '#Method .+::zipWith\(\) should return .+ but returns .+Pair\>#' - - '#Method .+::attempt\(\) should return .+ but returns .+Validation\ Date: Mon, 8 Dec 2025 15:18:41 +0000 Subject: [PATCH 15/23] Make example of print return IO of Unit instead of void --- src/Functions/console.php | 18 +++++++++++++----- src/IO/IO.php | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/Functions/console.php b/src/Functions/console.php index 2500414..3b93aa2 100644 --- a/src/Functions/console.php +++ b/src/Functions/console.php @@ -13,6 +13,7 @@ use Phunkie\Effect\IO\IO; use Phunkie\Types\ImmList; +use Phunkie\Types\Unit; const printLn = '\Phunkie\Effect\Functions\console\printLn'; @@ -20,11 +21,15 @@ * Prints a message to stdout followed by a newline. * * @param string $message The message to print - * @return IO + * @return IO */ function printLn(string $message): IO { - return new IO(fn () => print($message . PHP_EOL)); + return new IO(function () use ($message): Unit { + print($message . PHP_EOL); + + return Unit(); + }); } const printLines = '\Phunkie\Effect\Functions\console\printLines'; @@ -33,12 +38,15 @@ function printLn(string $message): IO * Prints a list of messages to stdout, each followed by a newline. * * @param ImmList $lines The lines to print - * @return IO + * @return IO */ function printLines(ImmList $lines): IO { - return new IO(fn () => - $lines->withEach(fn ($message) => print($message . PHP_EOL))); + return new IO(function () use ($lines): Unit { + $lines->withEach(fn ($message) => print($message . PHP_EOL)); + + return Unit(); + }); } const readLine = '\Phunkie\Effect\Functions\console\readLine'; diff --git a/src/IO/IO.php b/src/IO/IO.php index 4d9bb9f..e8aaf4a 100644 --- a/src/IO/IO.php +++ b/src/IO/IO.php @@ -36,7 +36,7 @@ * - Parallel execution (via Parallel type class) * - Error handling * - * @template A + * @template-covariant A */ class IO implements Functor, Applicative, Monad, Parallel, Kind { From c32bfd04e7690833747d89bbeeabad41a7598a2a Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Mon, 8 Dec 2025 15:24:26 +0000 Subject: [PATCH 16/23] Make actions work with all versions --- .github/workflows/ci.yml | 42 ++++++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8e8596..2bdca65 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,15 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Checkout phunkie + uses: actions/checkout@v4 + with: + repository: phunkie/phunkie + path: phunkie + + - name: Configure path repository + run: composer config repositories.0.url phunkie + - name: Setup PHP uses: shivammathur/setup-php@v2 with: @@ -45,9 +54,13 @@ jobs: run: composer test lint: - name: Code Style (PHP-CS-Fixer) + name: PHP ${{ matrix.php }} Code Style (PHP-CS-Fixer) runs-on: ubuntu-latest - + strategy: + fail-fast: false + matrix: + php: ['8.2', '8.3', '8.4'] + steps: - name: Checkout code uses: actions/checkout@v4 @@ -55,26 +68,39 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.3 + php-version: ${{ matrix.php }} extensions: mbstring, json coverage: none tools: php-cs-fixer - name: Run PHP-CS-Fixer - run: php-cs-fixer fix --dry-run --diff --verbose + run: php-cs-fixer fix --dry-run --diff --verbose --allow-risky=yes static-analysis: - name: Static Analysis (PHPStan) + name: PHP ${{ matrix.php }} Static Analysis (PHPStan) runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['8.2', '8.3', '8.4'] steps: - name: Checkout code uses: actions/checkout@v4 + - name: Checkout phunkie + uses: actions/checkout@v4 + with: + repository: phunkie/phunkie + path: phunkie + + - name: Configure path repository + run: composer config repositories.0.url phunkie + - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.3 + php-version: ${{ matrix.php }} extensions: mbstring, json, pcntl, posix, zlib, sockets coverage: none @@ -83,9 +109,9 @@ jobs: uses: actions/cache@v4 with: path: vendor - key: ${{ runner.os }}-php-8.3-${{ hashFiles('**/composer.lock') }} + key: ${{ runner.os }}-php-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }} restore-keys: | - ${{ runner.os }}-php-8.3- + ${{ runner.os }}-php-${{ matrix.php }}- - name: Install dependencies run: composer install --prefer-dist --no-progress From cdf4842e93474079a8cf8698602c0e81173efc31 Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Mon, 8 Dec 2025 15:33:11 +0000 Subject: [PATCH 17/23] Fix CI --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2bdca65..dab149a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: path: phunkie - name: Configure path repository - run: composer config repositories.0.url phunkie + run: sed -i "s|url\": \"../phunkie\"|url\": \"phunkie\"|g" composer.json - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -95,7 +95,7 @@ jobs: path: phunkie - name: Configure path repository - run: composer config repositories.0.url phunkie + run: sed -i "s|url\": \"../phunkie\"|url\": \"phunkie\"|g" composer.json - name: Setup PHP uses: shivammathur/setup-php@v2 From 79e177a3770bde46f3478074efc6fecc8b81b2b6 Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Mon, 8 Dec 2025 16:22:46 +0000 Subject: [PATCH 18/23] Fix CI --- .github/workflows/ci.yml | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dab149a..c994917 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,14 +19,10 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Checkout phunkie - uses: actions/checkout@v4 - with: - repository: phunkie/phunkie - path: phunkie - - - name: Configure path repository - run: sed -i "s|url\": \"../phunkie\"|url\": \"phunkie\"|g" composer.json + - name: Unset local path repositories + run: | + composer config --unset repositories.0 + composer config --unset repositories.1 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -88,14 +84,10 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Checkout phunkie - uses: actions/checkout@v4 - with: - repository: phunkie/phunkie - path: phunkie - - - name: Configure path repository - run: sed -i "s|url\": \"../phunkie\"|url\": \"phunkie\"|g" composer.json + - name: Unset local path repositories + run: | + composer config --unset repositories.0 + composer config --unset repositories.1 - name: Setup PHP uses: shivammathur/setup-php@v2 From fd61904f2046e71e32535cc8c579ee015259dab3 Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Mon, 8 Dec 2025 16:25:21 +0000 Subject: [PATCH 19/23] Fix CI --- .github/workflows/ci.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c994917..928282a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,9 +20,7 @@ jobs: uses: actions/checkout@v4 - name: Unset local path repositories - run: | - composer config --unset repositories.0 - composer config --unset repositories.1 + run: composer config --unset repositories - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -85,9 +83,7 @@ jobs: uses: actions/checkout@v4 - name: Unset local path repositories - run: | - composer config --unset repositories.0 - composer config --unset repositories.1 + run: composer config --unset repositories - name: Setup PHP uses: shivammathur/setup-php@v2 From 7a357030252dfe938f8d4f08c9f81a01461745ed Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Mon, 8 Dec 2025 16:28:37 +0000 Subject: [PATCH 20/23] Fix Stan --- src/IO/IO.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/IO/IO.php b/src/IO/IO.php index e8aaf4a..4d9bb9f 100644 --- a/src/IO/IO.php +++ b/src/IO/IO.php @@ -36,7 +36,7 @@ * - Parallel execution (via Parallel type class) * - Error handling * - * @template-covariant A + * @template A */ class IO implements Functor, Applicative, Monad, Parallel, Kind { From aeb112f585c7f75ddf4c585a7fd53c8486cdb953 Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Mon, 8 Dec 2025 17:11:28 +0000 Subject: [PATCH 21/23] Add some cool badges --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d8a7bad..c21367b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Phunkie Effect -[![CI](https://github.com/phunkie/effect/workflows/CI/badge.svg)](https://github.com/phunkie/effect/actions) +[![CI](https://github.com/phunkie/effect/actions/workflows/ci.yml/badge.svg)](https://github.com/phunkie/effect/actions) [![Latest Version](https://img.shields.io/packagist/v/phunkie/effect.svg?style=flat-square)](https://packagist.org/packages/phunkie/effect) [![Total Downloads](https://poser.pugx.org/phunkie/effect/downloads)](https://packagist.org/packages/phunkie/effect) [![License](https://img.shields.io/packagist/l/phunkie/effect.svg?style=flat-square)](https://github.com/phunkie/effect/blob/main/LICENSE) From 690ef14faad984740ef8d6bc2a8f1bded54dbd12 Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Mon, 8 Dec 2025 18:26:19 +0000 Subject: [PATCH 22/23] Fix IO phpdoc --- src/IO/IO.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/IO/IO.php b/src/IO/IO.php index 4d9bb9f..e8aaf4a 100644 --- a/src/IO/IO.php +++ b/src/IO/IO.php @@ -36,7 +36,7 @@ * - Parallel execution (via Parallel type class) * - Error handling * - * @template A + * @template-covariant A */ class IO implements Functor, Applicative, Monad, Parallel, Kind { From 885c1255959491c1817d95d06262ea42b0c2f72b Mon Sep 17 00:00:00 2001 From: Marcello Duarte Date: Mon, 8 Dec 2025 19:01:33 +0000 Subject: [PATCH 23/23] Getting ready for release --- composer.json | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/composer.json b/composer.json index 5a38b99..867407c 100644 --- a/composer.json +++ b/composer.json @@ -9,24 +9,14 @@ "email": "marcello.duarte@gmail.com" } ], - "repositories": [ - { - "type": "path", - "url": "../phunkie" - }, - { - "type": "path", - "url": "../phpstan" - } - ], "require": { "php": "^8.2 || ^8.3 || ^8.4", - "phunkie/phunkie": "dev-developing-1.0.0 as 1.0.0" + "phunkie/phunkie": "^1.0" }, "require-dev": { "phpunit/phpunit": "^10.5", "phpstan/phpstan": "^2.1", - "phunkie/phpstan": "@dev", + "phunkie/phpstan": "^1.0", "friendsofphp/php-cs-fixer": "^3.90" }, "suggest": { @@ -65,4 +55,4 @@ "config": { "bin-dir": "bin" } -} \ No newline at end of file +}