Either in Phunkie represents a value of one of two possible types. An Either type can be Right(value) representing success, or Left(value) representing failure or error. This is commonly used for error handling and validation where you want to preserve error information.
There are several ways to create Eithers:
// Using explicit constructors
$success = Right(42); // Right(42)
$failure = Left("error"); // Left("error")
// From computations that might fail
function divide($a, $b) {
return $b === 0
? Left("Division by zero")
: Right($a / $b);
}
$result = divide(10, 2); // Right(5)
$error = divide(10, 0); // Left("Division by zero")
Eithers provide several basic operations:
$right = Right(42);
$left = Left("error");
// Check which side
$right->isRight(); // true
$right->isLeft(); // false
$left->isRight(); // false
$left->isLeft(); // true
// Get value with fallback
$right->getOrElse(0); // 42
$left->getOrElse(0); // 0
Either implements several type classes that provide powerful functional operations:
Map only transforms Right values - Left values are passed through unchanged:
$right = Right(2);
$result = $right->map(fn($x) => $x * 2); // Right(4)
$left = Left("error");
$result = $left->map(fn($x) => $x * 2); // Left("error") - unchanged
FlatMap allows chaining computations that might fail:
$safeDivide = fn($a, $b) => $b === 0 ? Left("div by zero") : Right($a / $b);
$safeSqrt = fn($x) => $x < 0 ? Left("negative sqrt") : Right(sqrt($x));
// Success case
$result = Right(16)
->flatMap(fn($x) => $safeDivide($x, 4)) // Right(4)
->flatMap($safeSqrt); // Right(2.0)
// Failure case - short-circuits on first error
$result = Right(16)
->flatMap(fn($x) => $safeDivide($x, 0)) // Left("div by zero")
->flatMap($safeSqrt); // Left("div by zero") - never executes sqrt
$value = Right(5);
$function = Right(fn($x) => $x * 2);
$result = $value->apply($function); // Right(10)
// With Left values
$error = Left("error");
$result = $error->apply($function); // Left("error")
Fold allows you to handle both cases and produce a single result:
$right = Right(42);
$result = ($right->fold(
fn($error) => "Error: $error"
))(fn($value) => "Success: $value");
// "Success: 42"
$left = Left("not found");
$result = ($left->fold(
fn($error) => "Error: $error"
))(fn($value) => "Success: $value");
// "Error: not found"
Swap exchanges Left and Right:
Right(42)->swap(); // Left(42)
Left("error")->swap(); // Right("error")
Transform both Left and Right values:
$either = Right(5);
$result = $either->bimap(
fn($e) => "Error: $e",
fn($v) => $v * 2
);
// Right(10)
$either = Left("fail");
$result = $either->bimap(
fn($e) => "Error: $e",
fn($v) => $v * 2
);
// Left("Error: fail")
Transform only Left values:
Right(42)->leftMap(fn($e) => "Error: $e"); // Right(42) - unchanged
Left("fail")->leftMap(fn($e) => "Error: $e"); // Left("Error: fail")
Filter Right values based on a predicate:
$isEven = fn($x) => $x % 2 === 0;
Right(42)->filterOrElse($isEven, "not even"); // Right(42)
Right(41)->filterOrElse($isEven, "not even"); // Left("not even")
Left("error")->filterOrElse($isEven, "not even"); // Left("error")
Convert Either to Option - Right becomes Some, Left becomes None:
Right(42)->toOption(); // Some(42)
Left("error")->toOption(); // None
Provide an alternative Either:
Right(42)->orElse(Right(0)); // Right(42)
Left("error")->orElse(Right(0)); // Right(0)
Left("error 1")->orElse(Left("error 2")); // Left("error 2")
Check if Right contains a specific value:
Right(42)->contains(42); // true
Right(42)->contains(0); // false
Left("error")->contains(42); // false
Check if Right value satisfies a predicate:
$isPositive = fn($x) => $x > 0;
Right(42)->exists($isPositive); // true
Right(-1)->exists($isPositive); // false
Left("error")->exists($isPositive); // false
Check if all Right values satisfy a predicate (vacuously true for Left):
$isPositive = fn($x) => $x > 0;
Right(42)->forall($isPositive); // true
Right(-1)->forall($isPositive); // false
Left("error")->forall($isPositive); // true (no value to check)
Either works with Phunkie’s pattern matching:
$result = match($either) {
Right($value) => "Success: $value",
Left($error) => "Error: $error"
};
function fetchUser($id) {
$user = database()->find($id);
return $user ? Right($user) : Left("User not found");
}
function validateAge($user) {
return $user['age'] >= 18
? Right($user)
: Left("User must be 18 or older");
}
$result = fetchUser(123)
->flatMap(fn($user) => validateAge($user))
->map(fn($user) => $user['name'])
->getOrElse("Anonymous");
function validateConfig($config) {
return isset($config['api_key'])
? Right($config)
: Left("Missing API key");
}
function validateTimeout($config) {
return $config['timeout'] > 0
? Right($config)
: Left("Timeout must be positive");
}
$result = Right($config)
->flatMap(fn($c) => validateConfig($c))
->flatMap(fn($c) => validateTimeout($c))
->fold(
fn($error) => throw new Exception($error),
fn($config) => $config
);
function readFile($path) {
return file_exists($path)
? Right(file_get_contents($path))
: Left("File not found: $path");
}
function parseJson($content) {
$data = json_decode($content, true);
return json_last_error() === JSON_ERROR_NONE
? Right($data)
: Left("Invalid JSON");
}
$result = readFile('config.json')
->flatMap(fn($content) => parseJson($content))
->map(fn($data) => $data['setting'])
->getOrElse('default_value');
While both Either and Validation can represent success/failure, they differ in how they handle errors:
// Either - stops at first error
$result = Right(5)
->flatMap(fn($x) => Left("error 1"))
->flatMap(fn($x) => Left("error 2"));
// Result: Left("error 1") - second error never seen
// Validation - accumulates errors
$v1 = Failure("error 1");
$v2 = Failure("error 2");
$result = $v1->combine($v2);
// Result: Failure(Nel("error 1", "error 2"))
Use Either when:
Use Validation when:
Either satisfies the laws for:
fa.map(id) == fafa.map(f).map(g) == fa.map(f ∘ g)pure(a).flatMap(f) == f(a)m.flatMap(pure) == mm.flatMap(f).flatMap(g) == m.flatMap(x => f(x).flatMap(g))pure(id).apply(v) == vpure(f).apply(pure(x)) == pure(f(x))u.apply(pure(y)) == pure(fn => fn(y)).apply(u)