108 lines
8.4 KiB
Markdown
108 lines
8.4 KiB
Markdown
# 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.
|
|
|
|
### `Option`s and `Result`s
|
|
|
|
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>`<br>Replaces `null` checks | `Result<TOK, TError>`<br>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**<br> | `->value: T` | `->ok: TOK` |
|
|
| _all throw if called on missing value_ | | `->error: TError` |
|
|
| **Transforming**<br> | `->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**<br>_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 be `void` or `never`.
|
|
- `->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 returns `true`, will remain Some; if it returns `false`, the value will become None.
|
|
- `->unwrap()` will return `null` for None options and the value for Some options.
|
|
|
|
> We would be remiss to not acknowledge some excellent prior art in this area - the [PhpOption](https://github.com/schmittjoh/php-option) project. `Option::of` recognizes their options and converts them properly, and `Option<T>` instances have a `->toPhpOption()` method that will convert these back into PhpOption's `Some<T>` and `None` instances. There is also a [ResultType](https://github.com/GrahamCampbell/Result-Type) 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.
|
|
|
|
```php
|
|
// 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`.
|
|
|
|
```php
|
|
// Use this
|
|
$theKeys = $array |> Arr::keys();
|
|
// ...or this
|
|
$theKeys = $array |> array_keys(...);
|
|
|
|
// Not this
|
|
$theKeys = $array |> Arr::keys(...);
|
|
```
|
|
|
|
## The Inspiration
|
|
|
|
[F#](https://fsharp.org/) 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...
|
|
|
|
```fsharp
|
|
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...
|
|
|
|
```php
|
|
$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.
|
|
|
|
```php
|
|
$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.
|