Monad transformers allow you to combine multiple monads into a single monad. Phunkie provides several monad transformers to help you work with nested monadic structures.
Monad transformers wrap one monad inside another, allowing you to work with both contexts simultaneously. For example, OptionT wraps an Option inside another monad F.
Combines Either with another monad:
use Phunkie\Cats\EitherT;
// ImmList<Either<String, Int>>
$listOfEithers = ImmList(Right(1), Left("error"), Right(2));
$eitherT = EitherT($listOfEithers);
// Map over the Right values
$result = $eitherT->map(fn($x) => $x + 1);
// EitherT(ImmList(Right(2), Left("error"), Right(3)))
// FlatMap with another EitherT
$result = $eitherT->flatMap(
fn($x) => EitherT(ImmList(Right($x * 2)))
);
// EitherT(ImmList(Right(2), Left("error"), Right(4)))
// Error handling with IO
$safeDivide = fn($a, $b) => IO(fn() =>
$b === 0 ? Left("Division by zero") : Right($a / $b)
);
$result = EitherT($safeDivide(10, 2))
->map(fn($x) => $x * 2)
->getOrElse(0);
// IO that returns 10
Combines Option with another monad:
use Phunkie\Cats\OptionT;
// ImmList<Option<Int>>
$listOfOptions = ImmList(Some(1), None(), Some(2));
$optionT = OptionT($listOfOptions);
// Map over the inner values
$result = $optionT->map(fn($x) => $x + 1);
// OptionT(ImmList(Some(2), None(), Some(3)))
// FlatMap with another OptionT
$result = $optionT->flatMap(
fn($x) => OptionT(ImmList(Some($x + 1)))
);
// OptionT(ImmList(Some(2), None(), Some(3)))
Combines State with another monad F. The standard function signature is S => F[(S, A)].
use Phunkie\Cats\StateT;
// StateT<Option, Int, Int>
// Function takes state (Int) and returns Option<Pair<State, Value>>
$stateT = new StateT(fn($n) => Some(Pair($n + 1, $n)));
$result = $stateT->run(1);
// Some(Pair(2, 1))
Represents functions that return monadic values:
use Phunkie\Cats\Kleisli;
use function Phunkie\Functions\kleisli\kleisli;
$validateLength = kleisli(fn($s) =>
Option(strlen($s) > 3 ? $s : null)
);
$validateEmail = kleisli(fn($s) =>
Option(filter_var($s, FILTER_VALIDATE_EMAIL) ? $s : null)
);
// Compose validations
$validateInput = $validateLength->andThen($validateEmail);
$result = $validateInput->run("a@b"); // None
$result = $validateInput->run("user@example.com"); // Some("user@example.com")
Transform values within the nested structure:
$optionT->map(fn($x) => $x * 2);
$stateT->map(fn($x) => $x * 2);
Chain operations that return transformed values:
$optionT->flatMap(fn($x) => OptionT(Some($x * 2)));
$eitherT->isRight(); // F<Boolean>
$eitherT->isLeft(); // F<Boolean>
$eitherT->getOrElse(42); // F<A>
$eitherT->fold( // F<B>
fn($e) => "Error: $e",
fn($v) => "Success: $v"
);
$eitherT->swap(); // EitherT<F,R,L>
$eitherT->bimap( // EitherT<F,A,B>
fn($e) => "Error: $e",
fn($v) => $v * 2
);
$eitherT->leftMap( // EitherT<F,A,R>
fn($e) => "Error: $e"
);
$eitherT->toOptionT(); // OptionT<F,R>
// Static constructors
EitherT::pure($monad, 42); // EitherT(Right(42))
EitherT::left($monad, "error"); // EitherT(Left("error"))
EitherT::fromOptionT($optionT, "not found"); // Convert OptionT
$optionT->isDefined(); // F<Boolean>
$optionT->isEmpty(); // F<Boolean>
$optionT->getOrElse(42); // F<A>
use Phunkie\Cats\EitherT;
$readConfig = fn($path) => IO(fn() =>
file_exists($path)
? Right(file_get_contents($path))
: Left("File not found: $path")
);
$parseJson = fn($content) => IO(fn() => {
$data = json_decode($content, true);
return json_last_error() === JSON_ERROR_NONE
? Right($data)
: Left("Invalid JSON");
});
$config = EitherT($readConfig('config.json'))
->flatMap(fn($content) => EitherT($parseJson($content)))
->map(fn($data) => $data['setting'])
->getOrElse('default');
// Returns IO that produces either the setting or 'default'
$users = ImmList(
Some(['name' => 'Alice']),
None(),
Some(['name' => 'Bob'])
);
$names = OptionT($users)
->map(fn($user) => $user['name'])
->getOrElse('Unknown');
// ImmList('Alice', 'Unknown', 'Bob')
$validateAge = fn($user) =>
$user['age'] >= 18
? Right($user)
: Left("Must be 18 or older");
$validateEmail = fn($user) =>
filter_var($user['email'], FILTER_VALIDATE_EMAIL)
? Right($user)
: Left("Invalid email");
$users = ImmList(
Right(['name' => 'Alice', 'age' => 25, 'email' => 'alice@example.com']),
Right(['name' => 'Bob', 'age' => 16, 'email' => 'bob@example.com'])
);
$validated = EitherT($users)
->flatMap(fn($user) => EitherT(ImmList($validateAge($user))))
->flatMap(fn($user) => EitherT(ImmList($validateEmail($user))))
->getValue();
// ImmList(Right(['name' => 'Alice', ...]), Left("Must be 18 or older"))
$computation = new StateT(
fn($state) => Some(Pair($state + 1, $state))
);
$validate = kleisli(fn($input) =>
Option($input)
->filter(fn($x) => strlen($x) > 3)
->filter(fn($x) => is_numeric($x))
);