A collection of useful monads for PHP 8.3+. Inspired by Rust's powerful type system and functional programming patterns.
- 🦀 Rust-inspired API - Familiar methods for those coming from Rust
- 🔒 Type-safe - Full PHPStan level 9 support with generics
- 🧪 Well-tested - Comprehensive test suite
- 📦 Zero dependencies - Lightweight and focused
- 🎯 Four core monads:
Option<T>- Represent optional values without nullResult<T, E>- Handle errors without exceptionsLazy<T>- Defer computation until neededWriter<W, T>- Carry a value alongside an accumulated log
You can install the package via Composer:
composer require gosuperscript/monads- PHP 8.3 or higher
The Option type represents an optional value: every Option is either Some and contains a value, or None, and does not. This is a safer alternative to using null.
usefunctionSuperscript\Monads\Option\{Some, None};
// Create an Option$some = Some(42);
$none = None();
// Check if value exists$some->isSome(); // true$none->isNone(); // true// Transform the value$doubled = Some(21)->map(fn($x) => $x * 2); // Some(42)$empty = None()->map(fn($x) => $x * 2); // None// Provide default valuesSome(42)->unwrapOr(0); // 42None()->unwrapOr(0); // 0// Chain operationsSome(10)
->filter(fn($x) => $x > 5)
->map(fn($x) => $x * 2)
->unwrapOr(0); // 20// Convert to ResultSome(42)->okOr("error"); // Ok(42)None()->okOr("error"); // Err("error")isSome()/isNone()- Check if the option contains a valueisSomeAnd(callable $predicate)- Check if Some and matches predicatemap(callable $f)- Transform the contained valuefilter(callable $f)- Filter based on a predicateand(Option $other)/or(Option $other)- Combine optionsandThen(callable $f)- Chain operations (flatMap)unwrap()- Get the value (throws if None)unwrapOr($default)- Get the value or a defaultunwrapOrElse(callable $f)- Get the value or compute a defaultexpect(string|Throwable $message)- Unwrap with custom error message
Result<T, E> is the type used for returning and propagating errors. It is either Ok(T), representing success and containing a value, or Err(E), representing error and containing an error value.
usefunctionSuperscript\Monads\Result\{Ok, Err, attempt};
// Create Results$ok = Ok(42);
$err = Err("something went wrong");
// Check the result$ok->isOk(); // true$err->isErr(); // true// Transform success values$doubled = Ok(21)->map(fn($x) => $x * 2); // Ok(42)$stillErr = Err("error")->map(fn($x) => $x * 2); // Err("error")// Transform error values$recovered = Err("error")->mapErr(fn($e) => "recovered"); // Err("recovered")// Handle both cases$result = Ok(10)->match(
err: fn($e) => "Error: $e",
ok: fn($x) => "Success: $x"
); // "Success: 10"// Chain operationsOk(10)
->map(fn($x) => $x * 2)
->andThen(fn($x) => $x > 15 ? Ok($x) : Err("too small"))
->unwrapOr(0); // 20// Convert to OptionOk(42)->ok(); // Some(42)Err("e")->ok(); // None()// Safely execute code that might throw$result = attempt(fn() => json_decode($json, flags: JSON_THROW_ON_ERROR));
// Returns: Result<mixed, Throwable>isOk()/isErr()- Check if the result is success or errormap(callable $f)- Transform the success valuemapErr(callable $f)- Transform the error valuemapOr($default, callable $f)- Transform or provide defaultmapOrElse(callable $default, callable $f)- Transform or compute defaultmatch(callable $err, callable $ok)- Handle both casesand(Result $other)/or(Result $other)- Combine resultsandThen(callable $f)- Chain operations (flatMap)unwrap()- Get the success value (throws if Err)unwrapErr()- Get the error value (throws if Ok)unwrapOr($default)- Get the value or a defaultunwrapOrElse(callable $f)- Get the value or compute a defaultexpect(string|Throwable $message)- Unwrap with custom error message
The Lazy type allows you to defer the execution of a computation until its result is actually needed.
useSuperscript\Monads\Lazy\Lazy;
// Create a lazy computation$lazy = Lazy::of(fn() => expensiveComputation());
// The computation hasn't run yet...// Evaluate when needed (memoized)$result = $lazy->evaluate(); // Runs the computation$cached = $lazy->evaluate(); // Returns cached result// Practical example: lazy database query$users = Lazy::of(fn() => DB::query("SELECT * FROM users"));
if ($needUsers) {
$data = $users->evaluate(); // Query runs only if needed
}The Writer monad lets you carry a value alongside an accumulated log through a chain of computations. This is useful for logging, auditing, or building up auxiliary data without side effects.
useSuperscript\Monads\Writer\Writer;
usefunctionSuperscript\Monads\Writer\Writer;
// Create a Writer with an array-based log$writer = Writer(42, ['initial value']);
// Access value and log$writer->value(); // 42$writer->log(); // ['initial value']$writer->run(); // [42, ['initial value']]// Transform the value (log unchanged)Writer(21)->map(fn($x) => $x * 2); // Writer(42, [])// Chain computations that produce their own logsWriter(10, ['start'])
->andThen(fn($x) => Writer($x * 2, ['doubled']))
->andThen(fn($x) => Writer($x + 1, ['incremented']));
// value: 21, log: ['start', 'doubled', 'incremented']// Append to the log without changing the valueWriter(42)->tell(['something happened']);
// value: 42, log: ['something happened']// Use a custom log type with Writer::of()$writer = Writer::of('hello', '', fn(string$a, string$b): string => $a . $b)
->tell(' world')
->map(fn($v) => strtoupper($v));
// value: 'HELLO', log: ' world'value()- Get the contained valuelog()- Get the accumulated logrun()- Get both as a[$value, $log]tuplemap(callable $f)- Transform the value, leaving the log unchangedandThen(callable $f)- Chain a computation that returns a Writer, combining logs (flatMap)tell($entry)- Append to the log without changing the valuemapLog(callable $f)- Transform the loginspect(callable $f)- Execute a side effect with the valuelisten(callable $f)- Access both value and log to produce a new valuereset($log)- Reset the log to a given value
Both Option and Result support collecting arrays of values:
usefunctionSuperscript\Monads\Option\{Some, None};
usefunctionSuperscript\Monads\Result\{Ok, Err};
// Collect Options - returns first None or Some(array)
Option::collect([Some(1), Some(2), Some(3)]); // Some([1, 2, 3])
Option::collect([Some(1), None(), Some(3)]); // None()// Collect Results - returns first Err or Ok(array)
Result::collect([Ok(1), Ok(2), Ok(3)]); // Ok([1, 2, 3])
Result::collect([Ok(1), Err("e"), Ok(3)]); // Err("e")useSuperscript\Monads\Option\Option;
functiongetUser(int$id): Option {
$user = DB::find('users', $id);
return Option::from($user); // Returns None if null
}
$username = getUser(123)
->map(fn($user) => $user->name)
->unwrapOr('Guest');usefunctionSuperscript\Monads\Result\{Ok, Err, attempt};
functiondivide(int$a, int$b): Result {
return$b === 0 ? Err("Division by zero")
: Ok($a / $b);
}
$result = divide(10, 2)
->map(fn($x) => $x * 2)
->unwrapOr(0); // 10$error = divide(10, 0)
->map(fn($x) => $x * 2)
->unwrapOr(0); // 0usefunctionSuperscript\Monads\Writer\Writer;
functionaddTax(float$price): Writer {
$taxed = $price * 1.2;
returnWriter($taxed, [sprintf('Tax: %.2f -> %.2f', $price, $taxed)]);
}
functionapplyDiscount(float$price): Writer {
$discounted = $price * 0.9;
returnWriter($discounted, [sprintf('Discount: %.2f -> %.2f', $price, $discounted)]);
}
$result = Writer(100.0, ['Starting price: 100.00'])
->andThen(fn($p) => addTax($p))
->andThen(fn($p) => applyDiscount($p));
$result->value(); // 108.0$result->log(); // ['Starting price: 100.00', 'Tax: 100.00 -> 120.00', 'Discount: 120.00 -> 108.00']usefunctionSuperscript\Monads\Result\{Ok, Err};
functionprocessData(array$data): Result {
returnOk($data)
->andThen(fn($d) => validateData($d))
->andThen(fn($d) => transformData($d))
->andThen(fn($d) => saveData($d));
}
$result = processData($input)->match(
err: fn($e) => response()->json(['error' => $e], 400),
ok: fn($d) => response()->json(['data' => $d], 200)
);The package uses Pest for testing:
# Run tests
vendor/bin/pest
# Run type checking
vendor/bin/phpstan
# Run code style fixer
vendor/bin/pintThis library provides full PHPStan support with generic types. The testing utilities include:
useSuperscript\Monads\Result\Testing\ComparesResults;
useSuperscript\Monads\Option\Testing\ComparesOptions;
class MyTest extends TestCase {
use ComparesResults;
use ComparesOptions;
publicfunctiontest_example() {
// Custom assertions$this->assertOk(Ok(42));
$this->assertErr(Err("error"));
$this->assertSome(Some(42));
$this->assertNone(None());
// PHPUnit constraints$this->assertThat(Ok(42), $this->isOk());
$this->assertThat(Err("e"), $this->isErr());
}
}Monads help you write more predictable and maintainable code by:
- Making errors explicit - No hidden nulls or uncaught exceptions
- Enabling composition - Chain operations cleanly with
mapandandThen - Improving type safety - Let PHPStan catch errors at analysis time
- Reducing boilerplate - Less null checking and try-catch blocks
This library is heavily inspired by Rust's Option and Result types, bringing similar patterns to PHP.
Please see CHANGELOG for more information on what has changed recently.
Contributions are welcome! Please feel free to submit a Pull Request.
The MIT License (MIT). Please see License File for more information.