Inspired by F#
This project contains PHP utility classes whose functionality is inspired by their F# counterparts.
The v3 series requires at least PHP 8.5, and includes pipeable array_* functions. The v2 series requires at least PHP 8.4. A similar v2 API exists for PHP 8.2 - 8.3 in version 1 of this project; see its README for specifics.
What It Provides
This library currently provides three classes.
Options and Results
Two of them are designed to wrap values and indicate the state of the action that produced them. Option<T> represents a variable that may or may not have a value. Result<TOK, TError> represents the result of an action; the "ok" and "error" states both provide a value.
Option<T>Replaces null checks |
Result<TOK, TError>Replaces exception-based error handling |
|
|---|---|---|
| Creating | ::Some(T) for Some |
::OK(TOK) for OK |
::None() for None |
::Error(TError) for Error |
|
::of($value) None if null |
||
| Querying | ->isSome: bool |
->isOK: bool |
->isNone: bool |
->isError: bool |
|
->contains(T, $strict = true): bool |
->contains(TOK, $strict = true): bool |
|
->exists(callable(T): bool): bool |
->exists(callable(TOK): bool): bool |
|
| Reading |
->value: T |
->ok: TOK |
| all throw if called on missing value | ->error: TError |
|
| Transforming |
->map(callable(T): TMapped): Option<TMapped> |
->map(callable(TOK): TMapped): Result<TMapped, TError> |
all still Option or Result |
->mapError(callable(TError): TMapped): Result<TOK, TMapped> |
|
| Iterating | ->iter(callable(T): void): void |
->iter(callable(TOK): void): void |
| Inspecting returns the original instance |
->tap(callable(Option<T>): void): Option<T> |
->tap(callable(Result<TOK, TError>): void): Result<TOK, TError> |
| Continued Processing | ->bind(callable(T): Option<TBound>): Option<TBound> |
->bind(callable(TOK): Result<TBoundOK, TError>): Result<TBoundOK, TError> |
| Changing Types | ->toArray(): T[] |
->toArray(): TOK[] |
->toOption(): Option<TOK> |
In addition to this, Option<T> provides:
->getOrDefault(T)will return the Some value if it exists or the given default if the option is None.->getOrCall(callable(): mixed)will call the given function if the option is None. That function may return a value, or may bevoidornever.->getOrThrow(callable(): Exception)will return the Some value if it exists, or throw the exception returned by the function if the option is None.->filter(callable(T): bool)will compare a Some value against the callable, and if it returnstrue, will remain Some; if it returnsfalse, the value will become None.->unwrap()will returnnullfor None options and the value for Some options.
We would be remiss to not acknowledge some excellent prior art in this area - the PhpOption project.
Option::ofrecognizes their options and converts them properly, andOption<T>instances have a->toPhpOption()method that will convert these back into PhpOption'sSome<T>andNoneinstances. There is also a ResultType project from the same team, though this project's result does not (yet) have any conversion methods for it.
Pipeable array_* Functions
The final class, exclusive to v3, is Arr, which wraps the array_* functions with versions that return a single-arity (AKA "one parameter") function which takes the array and returns the result.
The array_map example is easy to understand. This function requires the array first, and the transformation callback second. This does not play nicely with PHP 8.5's pipeline operator (|>); to pipe to array_map, one has to create an anonymous function to reverse the parameters. Arr::map() does this for you.
// The result of both calls is [2, 4, 6]
// Without this library
$withoutLibrary =
[1, 2, 3]
|> fn ($arr) => array_map($arr, fn ($x) => x * 2);
// With this library
$withLibrary =
[1, 2, 3]
|> Arr::map(fn ($x) => $x * 2);
All array_* functions which expect the target array as the first parameter have a matching Arr::* function which provides a version of the function with the array last. array_* functions which only take the array (such as array_count_values or array_keys) have corresponding functions in Arr for completeness; when piping to these, call the Arr function vs. using it as a callable.
// Use this
$theKeys = $array |> Arr::keys();
// ...or this
$theKeys = $array |> array_keys(...);
// Not this
$theKeys = $array |> Arr::keys(...);
The Inspiration
F# is an ML-style language that runs under .NET. It has most of the functional programming paradigms, but as it runs on what was designed as an object-oriented runtime - and can use and interoperate with all the .NET libraries - it is a pragmatic approach to functional programming. (Many of its decade+ old features have been implemented into recent versions of C#.)
This library, too, makes some pragmatic choices about structure. In F#, for example, an optional value could be obtained like...
let value =
Option.ofObj myVar
|> Option.map (fun it -> it.Replace("howd", "part"))
|> Option.defaultValue "There was no string"
If myVar were null, this value would have "There was no string"; if myVar had "howdy", value would have "party". Each Option call takes the option as its last parameter, and |> is the pipeline operator; it provides the previous value as the last parameter to the next operation. A prior version of this library had static functions to mimic this, which resulted in something like...
$value = Option::defaultValue('There was no string',
Option::map(fn($it) => str_replace('howd', 'part', $it),
Option::of($myVar)));
...which reads right-to-left (or bottom-to-top, the way it is formatted there). By implementing these as instance methods, the PHP code looks much cleaner.
$value = Option::of($myVar)
->map(fn($it) => str_replace('howd', 'part', $it))
->getOrDefault('There was no string');
If PHP gets a pipeline operator (TODO: !!!!), we'll revisit lots of stuff here (in a non-breaking way, of course).
Ideas
This library currently has the features which its author needs. To suggest others, reach out to Daniel on the Fediverse at @daniel@fedi.summershome.org or on Twitter at @Bit_Badger.