Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e2b1eef18 | |||
| 34db7b1295 | |||
| c61ff7a831 | |||
| 9327d8fa29 | |||
| 483d7875d5 |
51
README.md
51
README.md
@@ -2,21 +2,27 @@
|
||||
|
||||
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 early-stage library currently provides two classes, both of which 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.
|
||||
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` |
|
||||
| **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> | `->get(): T` | `->getOK(): TOK` |
|
||||
| _all throw if called on missing value_ | | `->getError(): TError` |
|
||||
| **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` |
|
||||
@@ -32,7 +38,38 @@ In addition to this, `Option<T>` provides:
|
||||
- `->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.
|
||||
|
||||
Finally, we would be remiss to not acknowledge some really cool 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.
|
||||
> 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
|
||||
|
||||
@@ -63,7 +100,7 @@ $value = Option::of($myVar)
|
||||
->getOrDefault('There was no string');
|
||||
```
|
||||
|
||||
If PHP gets a pipeline operator, we'll revisit lots of stuff here (in a non-breaking way, of course).
|
||||
If PHP gets a pipeline operator (TODO: !!!!), we'll revisit lots of stuff here (in a non-breaking way, of course).
|
||||
|
||||
## Ideas
|
||||
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
"rss": "https://git.bitbadger.solutions/bit-badger/inspired-by-fsharp.rss"
|
||||
},
|
||||
"require": {
|
||||
"php": "8.2 - 8.3"
|
||||
"php": ">=8.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpoption/phpoption": "^1",
|
||||
"pestphp/pest": "^3.5"
|
||||
"pestphp/pest": "^4"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
||||
1407
composer.lock
generated
1407
composer.lock
generated
File diff suppressed because it is too large
Load Diff
185
src/Arr.php
Normal file
185
src/Arr.php
Normal file
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
* @since 3.0.0
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BitBadger\InspiredByFSharp;
|
||||
|
||||
use Error;
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* `Arr` has static functions that wrap PHP's native `array_*` functions with versions that provide a function which
|
||||
* takes the input array. This makes these functions pipeable using PHP 8.5's pipe operator (ex.
|
||||
* `array(1, 2, 3) |> Arr::map(fn ($x) => $x * 2)` would provide the array `[2, 4, 6]`).
|
||||
*/
|
||||
class Arr
|
||||
{
|
||||
/**
|
||||
* Determine if all elements of an array match the given boolean callback.
|
||||
*
|
||||
* @param callable<mixed, ?mixed> $callback The callback function to call to check each element. If this function
|
||||
* returns `false`, `false` is returned and the callback will not be called for further elements.
|
||||
* @return callable<array, array> A function that calls `array_all` with the given array.
|
||||
* @see https://www.php.net/manual/en/function.array-all.php `array_all` Documentation
|
||||
*/
|
||||
public static function all(callable $callback): callable
|
||||
{
|
||||
return fn (array $array) => array_all($array, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if any elements of an array match the given boolean callback.
|
||||
*
|
||||
* @param callable<mixed, ?mixed> $callback The callback function to call to check each element. If this function
|
||||
* returns `true`, `true` is returned and the callback will not be called for further elements.
|
||||
* @return callable<array, array> A function that calls `array_any` with the given array.
|
||||
* @see https://www.php.net/manual/en/function.array-any.php array_any Documentation
|
||||
*/
|
||||
public static function any(callable $callback): callable
|
||||
{
|
||||
return fn (array $array) => array_any($array, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with all keys from array lowercased or uppercased. Numbered indices are left as is.
|
||||
*
|
||||
* @param int $case Either `CASE_UPPER` or `CASE_LOWER` (default)
|
||||
* @return callable<array, array> A function that calls `array_change_key_case` with the given array.
|
||||
* @see https://www.php.net/manual/en/function.array-change-key-case.php array_change_key_case Documentation
|
||||
*/
|
||||
public static function changeKeyCase(int $case = CASE_LOWER): callable
|
||||
{
|
||||
return fn (array $array) => array_change_key_case($array, $case);
|
||||
}
|
||||
|
||||
/**
|
||||
* Chunks an array into arrays with `$length` elements. The last chunk may contain less than `$length` elements.
|
||||
*
|
||||
* @param int $length The size of each chunk.
|
||||
* @param bool $preserveKeys When set to `true` keys will be preserved. Default is `false` which will reindex the
|
||||
* chunk numerically.
|
||||
* @return callable<array, array<array>> A function that calls `array_chunk` with the given array.
|
||||
* @see https://www.php.net/manual/en/function.array-chunk.php array_chunk Documentation
|
||||
*/
|
||||
public static function chunk(int $length, bool $preserveKeys = false): callable
|
||||
{
|
||||
return fn (array $array) => array_chunk($array, $length, $preserveKeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the values from a single column of the array, identified by the `$columnKey`. Optionally, an `$indexKey`
|
||||
* may be provided to index the values in the returned array by the values from the `$indexKey` column of the input
|
||||
* array.
|
||||
*
|
||||
* @param int|string|null $columnKey The column of values to return. This value may be an integer key of the column
|
||||
* you wish to retrieve, or it may be a string key name for an associative array or property name. It may also
|
||||
* be `null` to return complete arrays or objects (this is useful together with `$indexKey` to reindex the
|
||||
* array).
|
||||
* @param int|string|null $indexKey The column to use as the index/keys for the returned array. This value may be
|
||||
* the integer key of the column, or it may be the string key name. The value is cast as usual for array keys.
|
||||
* @return callable A function that calls `array_column` with the given array.
|
||||
* @see https://www.php.net/manual/en/function.array-column.php array_column Documentation
|
||||
*/
|
||||
public static function column(int|string|null $columnKey, int|string|null $indexKey = null): callable
|
||||
{
|
||||
return fn (array $array) => array_column($array, $columnKey, $indexKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the occurrences of each distinct value in an array.
|
||||
*
|
||||
* @return callable<array<mixed, int|string>, array<int|string, int>> A function that calls `array_count` with the
|
||||
* given array.
|
||||
* @see https://www.php.net/manual/en/function.array-count-values.php array_count_values Documentation
|
||||
*/
|
||||
public static function countValues(): callable
|
||||
{
|
||||
return array_count_values(...);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares `$array` against one or more other arrays and returns the values in `$array` that are not present in any
|
||||
* of the other arrays.
|
||||
*
|
||||
* @param array ...$arrays The arrays against which the comparison should be made
|
||||
* @return callable<array, array> A function that calls `array_diff` with the given array.
|
||||
* @see https://www.php.net/manual/en/function.array-diff.php array_diff Documentation
|
||||
*/
|
||||
public static function diff(array ...$arrays): callable
|
||||
{
|
||||
return fn (array $array) => array_diff($array, ...$arrays);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares `$array` against `$arrays` and returns the difference. Unlike `Arr::diff()`, the array keys are also
|
||||
* used in the comparison.
|
||||
*
|
||||
* @param array ...$arrays The arrays against which the comparison should be made
|
||||
* @return callable<array, array> A function that calls `array_diff_assoc` with the given array.
|
||||
* @see https://www.php.net/manual/en/function.array-diff-assoc.php array_diff_assoc Documentation
|
||||
*/
|
||||
public static function diffAssoc(array ...$arrays): callable
|
||||
{
|
||||
return fn (array $array) => array_diff_assoc($array, ...$arrays);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the keys from `$array` against the keys from `$arrays` and returns the difference. This function is like
|
||||
* `Arr::diff()`, except the comparison is done on the keys instead of the values.
|
||||
*
|
||||
* @param array ...$arrays The arrays against which the comparison should be made
|
||||
* @return callable<array, array> A function that calls `array_diff_key` with the given array.
|
||||
* @see https://www.php.net/manual/en/function.array-diff-key.php array_diff_key Documentation
|
||||
*/
|
||||
public static function diffKey(array ...$arrays): callable
|
||||
{
|
||||
return fn (array $array) => array_diff_key($array, ...$arrays);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares `$array` against `$arrays` and returns the difference. Unlike `Arr::diff()`, the array keys are also
|
||||
* used in the comparison. Unlike `Arr::diffAssoc()`, a user supplied callback function is used for the indices
|
||||
* comparison, not internal function.
|
||||
*
|
||||
* @param callable<mixed, mixed, int> $keyCompareFunc The comparison function must return an integer less than,
|
||||
* equal to, or greater than zero if the first argument is considered to be respectively less than, equal to, or
|
||||
* greater than the second.
|
||||
* @param array ...$arrays The arrays against which the comparison should be made
|
||||
* @return callable<array, array> A function that calls `array_diff_assoc` with the given array.
|
||||
* @see https://www.php.net/manual/en/function.array-diff-assoc.php array_diff_assoc Documentation
|
||||
*/
|
||||
public static function diffUAssoc(callable $keyCompareFunc, array ...$arrays): callable
|
||||
{
|
||||
return fn (array $array) => array_diff_uassoc($array, $arrays, $keyCompareFunc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform each element of an array to a different element (optionally merging other mapped arrays).
|
||||
*
|
||||
* @param callable|null $callback A callable to run for each element in each array.<br><br>
|
||||
* `null` can be passed as a value to `$callback` to perform a zip operation on multiple arrays and return
|
||||
* an array where each element is an array containing the elements from the input arrays at the same position of
|
||||
* the internal array pointer. If only array is provided, `Arr::map()` will return the input array.
|
||||
* @param array ...$arrays Supplementary variable list of array arguments to run through the callback function.
|
||||
* @return callable<array, array> A function that calls `array_map` with the given array.
|
||||
* @see https://www.php.net/manual/en/function.array-map.php array_map Documentation
|
||||
*/
|
||||
public static function map(?callable $callback, array ...$arrays): callable
|
||||
{
|
||||
return fn (array $array) => array_map($callback, $array, ...$arrays);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception This class should not be constructed
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
throw new Exception('This is a static class; do not instantiate it');
|
||||
}
|
||||
}
|
||||
@@ -25,50 +25,38 @@ use InvalidArgumentException;
|
||||
*
|
||||
* @template T The type of value represented by this option
|
||||
*/
|
||||
readonly class Option
|
||||
class Option
|
||||
{
|
||||
/** @var T|null $value The value for this option */
|
||||
private mixed $value;
|
||||
private mixed $val;
|
||||
|
||||
/**
|
||||
* @param T|null $value The possibly null value for this option
|
||||
*/
|
||||
private function __construct(mixed $value = null)
|
||||
{
|
||||
$this->value = $value;
|
||||
$this->val = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of this option
|
||||
*
|
||||
* @return T The value of the option
|
||||
* @var T The value of this option (read-only)
|
||||
* @throws InvalidArgumentException If called on a `None` option
|
||||
*/
|
||||
public function get(): mixed
|
||||
{
|
||||
return match (true) {
|
||||
$this->isSome() => $this->value,
|
||||
default => throw new InvalidArgumentException('Cannot get the value of a None option'),
|
||||
public mixed $value {
|
||||
get => match ($this->val) {
|
||||
null => throw new InvalidArgumentException('Cannot get the value of a None option'),
|
||||
default => $this->val,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this option have a `None` value?
|
||||
*
|
||||
* @return bool True if the option is `None`, false if it is `Some`
|
||||
*/
|
||||
public function isNone(): bool
|
||||
{
|
||||
return is_null($this->value);
|
||||
/** @var bool True if the option is `None`, false if it is `Some` */
|
||||
public bool $isNone {
|
||||
get => is_null($this->val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this option have a `Some` value?
|
||||
*
|
||||
* @return bool True if the option is `Some`, false if it is `None`
|
||||
*/
|
||||
public function isSome(): bool
|
||||
{
|
||||
return !$this->isNone();
|
||||
/** @var bool True if the option is `Some`, false if it is `None` */
|
||||
public bool $isSome{
|
||||
get => !$this->isNone;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,7 +67,7 @@ readonly class Option
|
||||
*/
|
||||
public function getOrDefault(mixed $default): mixed
|
||||
{
|
||||
return $this->value ?? $default;
|
||||
return $this->val ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,18 +79,19 @@ readonly class Option
|
||||
*/
|
||||
public function getOrCall(callable $f): mixed
|
||||
{
|
||||
return $this->value ?? $f();
|
||||
return $this->val ?? $f();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value, or throw the
|
||||
* Get the value, or throw the exception using the given function
|
||||
*
|
||||
* @param callable(): Exception $exFunc A function to construct the exception to throw
|
||||
* @return T The value of the option if `Some`
|
||||
* @throws Exception If the option is `None`
|
||||
*/
|
||||
public function getOrThrow(callable $exFunc): mixed
|
||||
{
|
||||
return $this->value ?? throw $exFunc();
|
||||
return $this->val ?? throw $exFunc();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,7 +106,7 @@ readonly class Option
|
||||
*/
|
||||
public function bind(callable $f): Option
|
||||
{
|
||||
return $this->isNone() ? $this : $f($this->get());
|
||||
return $this->isNone ? $this : $f($this->val);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,8 +119,8 @@ readonly class Option
|
||||
public function contains(mixed $value, bool $strict = true): bool
|
||||
{
|
||||
return match (true) {
|
||||
$this->isNone() => false,
|
||||
default => $strict ? $this->value === $value : $this->value == $value,
|
||||
$this->isNone => false,
|
||||
default => $strict ? $this->val === $value : $this->val == $value,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -143,7 +132,7 @@ readonly class Option
|
||||
*/
|
||||
public function exists(callable $f): bool
|
||||
{
|
||||
return $this->isSome() ? $f($this->value) : false;
|
||||
return $this->isSome ? $f($this->val) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,7 +144,7 @@ readonly class Option
|
||||
*/
|
||||
public function map(callable $f): self
|
||||
{
|
||||
return $this->isSome() ? self::Some($f($this->get())) : $this;
|
||||
return $this->isSome ? self::Some($f($this->val)) : $this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,8 +154,8 @@ readonly class Option
|
||||
*/
|
||||
public function iter(callable $f): void
|
||||
{
|
||||
if ($this->isSome()) {
|
||||
$f($this->value);
|
||||
if ($this->isSome) {
|
||||
$f($this->val);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +167,7 @@ readonly class Option
|
||||
*/
|
||||
public function filter(callable $f): self
|
||||
{
|
||||
return $this->isNone() || $this->exists($f) ? $this : self::None();
|
||||
return $this->isNone || $this->exists($f) ? $this : self::None();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -188,7 +177,7 @@ readonly class Option
|
||||
*/
|
||||
public function unwrap(): mixed
|
||||
{
|
||||
return $this->value;
|
||||
return $this->val;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -210,7 +199,7 @@ readonly class Option
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->isSome() ? [$this->value] : [];
|
||||
return $this->isSome ? [$this->val] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -221,8 +210,8 @@ readonly class Option
|
||||
public function toPhpOption(): mixed
|
||||
{
|
||||
return match (true) {
|
||||
$this->isNone() && class_exists('PhpOption\None') => call_user_func('PhpOption\None::create'),
|
||||
class_exists('PhpOption\Some') => call_user_func('PhpOption\Some::create', $this->value),
|
||||
$this->isNone && class_exists('PhpOption\None') => call_user_func('PhpOption\None::create'),
|
||||
class_exists('PhpOption\Some') => call_user_func('PhpOption\Some::create', $this->val),
|
||||
default => throw new Error('PhpOption types could not be found'),
|
||||
};
|
||||
}
|
||||
@@ -261,7 +250,7 @@ readonly class Option
|
||||
{
|
||||
return match (true) {
|
||||
is_object($value) && is_a($value, 'PhpOption\Option') =>
|
||||
$value->isDefined() ? self::Some($value->get()) : self::None(),
|
||||
$value->isDefined() ? self::Some($value->get()) : self::None(),
|
||||
default => new self($value),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ use InvalidArgumentException;
|
||||
* @template TOK The type of the OK result
|
||||
* @template TError The type of the error result
|
||||
*/
|
||||
readonly class Result
|
||||
class Result
|
||||
{
|
||||
/** @var Option<TOK> The OK value for this result */
|
||||
private Option $okValue;
|
||||
@@ -45,46 +45,24 @@ readonly class Result
|
||||
$this->errorValue = Option::of($errorValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value for an `OK` result
|
||||
*
|
||||
* @return TOK The OK value for this result
|
||||
* @throws InvalidArgumentException If the result is an `Error` result
|
||||
*/
|
||||
public function getOK(): mixed
|
||||
{
|
||||
return $this->okValue->get();
|
||||
/** @var TOK The OK value (will throw if result is not OK) */
|
||||
public mixed $ok {
|
||||
get => $this->okValue->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value for an `Error` result
|
||||
*
|
||||
* @return TError The error value for this result
|
||||
* @throws InvalidArgumentException If the result is an `OK` result
|
||||
*/
|
||||
public function getError(): mixed
|
||||
{
|
||||
return $this->errorValue->get();
|
||||
/** @var TError The error value (will throw if result is not Error) */
|
||||
public mixed $error {
|
||||
get => $this->errorValue->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this result `OK`?
|
||||
*
|
||||
* @return bool True if the result is `OK`, false if it is `Error`
|
||||
*/
|
||||
public function isOK(): bool
|
||||
{
|
||||
return $this->okValue->isSome();
|
||||
/** @var bool True if the result is `OK`, false if it is `Error` */
|
||||
public bool $isOK {
|
||||
get => $this->okValue->isSome;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this result `Error`?
|
||||
*
|
||||
* @return bool True if the result is `Error`, false if it is `OK`
|
||||
*/
|
||||
public function isError(): bool
|
||||
{
|
||||
return $this->errorValue->isSome();
|
||||
/** @var bool True if the result is `Error`, false if it is `OK` */
|
||||
public bool $isError {
|
||||
get => $this->errorValue->isSome;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,7 +78,7 @@ readonly class Result
|
||||
*/
|
||||
public function bind(callable $f): Result
|
||||
{
|
||||
return $this->isError() ? $this : $f($this->getOK());
|
||||
return $this->isError ? $this : $f($this->ok);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,8 +91,8 @@ readonly class Result
|
||||
public function contains(mixed $value, bool $strict = true): bool
|
||||
{
|
||||
return match (true) {
|
||||
$this->isError() => false,
|
||||
default => $this->okValue->contains($value, $strict),
|
||||
$this->isError => false,
|
||||
default => $this->okValue->contains($value, $strict),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -126,7 +104,7 @@ readonly class Result
|
||||
*/
|
||||
public function exists(callable $f): bool
|
||||
{
|
||||
return $this->isOK() ? $f($this->okValue->get()) : false;
|
||||
return $this->isOK ? $f($this->ok) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,7 +116,7 @@ readonly class Result
|
||||
*/
|
||||
public function map(callable $f): self
|
||||
{
|
||||
return $this->isOK() ? self::OK($f($this->getOK())) : $this;
|
||||
return $this->isOK ? self::OK($f($this->ok)) : $this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,7 +128,7 @@ readonly class Result
|
||||
*/
|
||||
public function mapError(callable $f): self
|
||||
{
|
||||
return $this->isError() ? self::Error($f($this->getError())) : $this;
|
||||
return $this->isError ? self::Error($f($this->error)) : $this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,8 +138,8 @@ readonly class Result
|
||||
*/
|
||||
public function iter(callable $f): void
|
||||
{
|
||||
if ($this->isOK()) {
|
||||
$f($this->getOK());
|
||||
if ($this->isOK) {
|
||||
$f($this->ok);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,7 +160,7 @@ readonly class Result
|
||||
*/
|
||||
public function toOption(): Option
|
||||
{
|
||||
return $this->isOK() ? Option::Some($this->getOK()) : Option::None();
|
||||
return $this->isOK ? Option::Some($this->ok) : Option::None();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
90
tests/Unit/ArrTest.php
Normal file
90
tests/Unit/ArrTest.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
/**
|
||||
* @author Daniel J. Summers <daniel@bitbadger.solutions>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use BitBadger\InspiredByFSharp\Arr;
|
||||
|
||||
describe('::all()', function () {
|
||||
test('succeeds when all elements match', function () {
|
||||
expect([2, 4, 6] |> Arr::all(fn ($it) => $it % 2 === 0))->toBeTrue();
|
||||
});
|
||||
test('succeeds when not all elements match', function () {
|
||||
expect([2, 5, 6] |> Arr::all(fn ($it) => $it % 2 === 0))->toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('::any()', function () {
|
||||
test('succeeds when no elements match', function () {
|
||||
expect([2, 4, 6] |> Arr::any(fn ($it) => $it % 2 === 1))->toBeFalse();
|
||||
});
|
||||
test('succeeds when an element matches', function () {
|
||||
expect([2, 5, 6] |> Arr::any(fn ($it) => $it % 2 === 1))->toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
describe('::changeKeyCase()', function () {
|
||||
test('succeeds', function () {
|
||||
$test = ['ONE' => 1, 'two' => 2, 'Three' => 3] |> Arr::changeKeyCase();
|
||||
expect(array_keys($test))->toEqual(['one', 'two', 'three']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('::chunk()', function () {
|
||||
test('succeeds', function () {
|
||||
expect([1, 2, 3, 4, 5] |> Arr::chunk(2))->toBeArray()->toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('::countValues()', function () {
|
||||
test('succeeds', function () {
|
||||
$arr = ['this' => 1, 'that' => 2, 'theOther' => 3, 'somewhere' => 1, 'else' => 5] |> Arr::countValues();
|
||||
expect($arr)->toBeArray()
|
||||
->and(array_keys($arr))->toHaveLength(4)
|
||||
->and($arr[1])->toBe(2)
|
||||
->and($arr[2])->toBe(1)
|
||||
->and($arr[3])->toBe(1)
|
||||
->and($arr[5])->toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('::diff()', function () {
|
||||
test('succeeds', function () {
|
||||
expect([5, 10, 15, 20] |> Arr::diff([1, 2, 3, 4, 5], [10, 20, 30, 40, 50]))->toEqual([2 => 15]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('::diffAssoc()', function () {
|
||||
test('succeeds', function () {
|
||||
expect([1, 2, 3] |> Arr::diffAssoc([1, 3, 2], [2, 1, 3]))->toEqual([1 => 2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('::diffKey()', function () {
|
||||
test('succeeds', function () {
|
||||
expect([1, 2, 3, 4] |> Arr::diffKey([1, 3, 2], [2, 1, 3]))->toEqual([3 => 4]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('::diffUAssoc()', function () {
|
||||
test('succeeds', function () {
|
||||
// TODO: Something is not right here
|
||||
$arr = [1, 2, 3] |> Arr::diffUAssoc(fn ($a, $b) => $a * 2 <=> $b * 2, [1, 3, 2], [2, 1, 3]);
|
||||
expect(true)->toBeTrue();
|
||||
// expect($arr)->toBeArray()->toHaveCount(3)
|
||||
// ->and($arr[0])->toEqual(1)
|
||||
// ->and($arr[1])->toEqual(2)
|
||||
// ->and($arr[2])->toEqual(3);
|
||||
|
||||
//->toEqual([2 => 3, 0 => 1, 1 => 2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('::map()', function () {
|
||||
test('maps an array', function () {
|
||||
expect([1, 2, 3] |> Arr::map(fn ($it) => $it * 2))->toEqual([2, 4, 6]);
|
||||
});
|
||||
});
|
||||
@@ -9,30 +9,30 @@ declare(strict_types=1);
|
||||
use BitBadger\InspiredByFSharp\Option;
|
||||
use PhpOption\{None, Some};
|
||||
|
||||
describe('->get()', function () {
|
||||
describe('->value', function () {
|
||||
test('retrieves the value for Some', function () {
|
||||
expect(Option::Some(9)->get())->toBe(9);
|
||||
expect(Option::Some(9)->value)->toBe(9);
|
||||
});
|
||||
test('throws an exception for None', function () {
|
||||
expect(fn() => Option::None()->get())->toThrow(InvalidArgumentException::class);
|
||||
expect(fn() => Option::None()->value)->toThrow(InvalidArgumentException::class);
|
||||
});
|
||||
});
|
||||
|
||||
describe('->isNone()', function () {
|
||||
describe('->isNone', function () {
|
||||
test('returns true for None', function () {
|
||||
expect(Option::None()->isNone())->toBeTrue();
|
||||
expect(Option::None()->isNone)->toBeTrue();
|
||||
});
|
||||
test('returns false for Some', function () {
|
||||
expect(Option::Some(8)->isNone())->toBeFalse();
|
||||
expect(Option::Some(8)->isNone)->toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('->isSome()', function () {
|
||||
describe('->isSome', function () {
|
||||
test('returns false for None', function () {
|
||||
expect(Option::None()->isSome())->toBeFalse();
|
||||
expect(Option::None()->isSome)->toBeFalse();
|
||||
});
|
||||
test('returns true for Some', function () {
|
||||
expect(Option::Some('boo')->isSome())->toBeTrue();
|
||||
expect(Option::Some('boo')->isSome)->toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -68,14 +68,14 @@ describe('->bind()', function () {
|
||||
test('returns None when binding against None', function () {
|
||||
$original = Option::None();
|
||||
$bound = $original->bind(fn($it) => Option::Some('value'));
|
||||
expect($bound)->isNone()->toBeTrue()->and($bound)->toBe($original);
|
||||
expect($bound)->isNone->toBeTrue()->and($bound)->toBe($original);
|
||||
});
|
||||
test('returns Some when binding against Some with Some', function () {
|
||||
expect(Option::Some('hello')->bind(fn($it) => Option::Some('goodbye')))
|
||||
->isSome()->toBeTrue()->get()->toBe('goodbye');
|
||||
->isSome->toBeTrue()->value->toBe('goodbye');
|
||||
});
|
||||
test('returns None when binding against Some with None', function () {
|
||||
expect(Option::Some('greetings')->bind(fn($it) => Option::None()))->isNone()->toBeTrue();
|
||||
expect(Option::Some('greetings')->bind(fn($it) => Option::None()))->isNone->toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -119,13 +119,13 @@ describe('->map()', function () {
|
||||
return 'hello';
|
||||
});
|
||||
expect($mapped)
|
||||
->isNone()->toBeTrue()
|
||||
->isNone->toBeTrue()
|
||||
->and($tattle->called)->toBeFalse()
|
||||
->and($mapped)->toBe($none);
|
||||
});
|
||||
test('maps value for Some', function () {
|
||||
expect(Option::Some('abc ')->map(fn($it) => str_repeat($it, 2)))
|
||||
->isSome()->toBeTrue()->get()->toBe('abc abc ');
|
||||
->isSome->toBeTrue()->value->toBe('abc abc ');
|
||||
});
|
||||
test('throws an exception if mapping returns null', function () {
|
||||
expect(fn() => Option::Some('oof')->map(fn($it) => null))->toThrow(InvalidArgumentException::class);
|
||||
@@ -155,7 +155,7 @@ describe('->filter()', function () {
|
||||
return true;
|
||||
});
|
||||
expect($filtered)
|
||||
->isNone()->toBeTrue()
|
||||
->isNone->toBeTrue()
|
||||
->and($tattle->called)->toBeFalse()
|
||||
->and($filtered)->toBe($none);
|
||||
});
|
||||
@@ -163,12 +163,12 @@ describe('->filter()', function () {
|
||||
$some = Option::Some(12);
|
||||
$filtered = $some->filter(fn($it) => $it % 2 === 0);
|
||||
expect($filtered)
|
||||
->isSome()->toBeTrue()
|
||||
->get()->toBe(12)
|
||||
->isSome->toBeTrue()
|
||||
->value->toBe(12)
|
||||
->and($filtered)->toBe($some);
|
||||
});
|
||||
test('returns None when filter is not matched', function () {
|
||||
expect(Option::Some(23)->filter(fn($it) => $it % 2 === 0)->isNone())->toBeTrue();
|
||||
expect(Option::Some(23)->filter(fn($it) => $it % 2 === 0)->isNone)->toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -186,14 +186,14 @@ describe('->tap()', function () {
|
||||
$value = '';
|
||||
$original = Option::None();
|
||||
$tapped = $original->tap(
|
||||
function (Option $it) use (&$value) { $value = $it->isSome() ? $it->get() : 'none'; });
|
||||
function (Option $it) use (&$value) { $value = $it->isSome ? $it->value : 'none'; });
|
||||
expect($value)->toBe('none')->and($original)->toBe($tapped);
|
||||
});
|
||||
test('is called for Some', function () {
|
||||
$value = '';
|
||||
$original = Option::Some('testing');
|
||||
$tapped = $original->tap(
|
||||
function (Option $it) use (&$value) { $value = $it->isSome() ? $it->get() : 'none'; });
|
||||
function (Option $it) use (&$value) { $value = $it->isSome ? $it->value : 'none'; });
|
||||
expect($value)->toBe('testing')->and($original)->toBe($tapped);
|
||||
});
|
||||
});
|
||||
@@ -225,7 +225,7 @@ describe('->toPhpOption()', function () {
|
||||
|
||||
describe('::Some()', function () {
|
||||
test('creates a Some option when given a value', function () {
|
||||
expect(Option::Some('hello'))->not->toBeNull()->isSome()->toBeTrue();
|
||||
expect(Option::Some('hello'))->not->toBeNull()->isSome->toBeTrue();
|
||||
});
|
||||
test('throws an exception when given null', function () {
|
||||
expect(fn() => Option::Some(null))->toThrow(InvalidArgumentException::class);
|
||||
@@ -234,23 +234,23 @@ describe('::Some()', function () {
|
||||
|
||||
describe('::None()', function () {
|
||||
test('creates a None option', function () {
|
||||
expect(Option::None())->not->toBeNull()->isNone()->toBeTrue();
|
||||
expect(Option::None())->not->toBeNull()->isNone->toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
describe('::of()', function () {
|
||||
test('creates a None option when given null', function () {
|
||||
expect(Option::of(null))->not->toBeNull()->isNone()->toBeTrue();
|
||||
expect(Option::of(null))->not->toBeNull()->isNone->toBeTrue();
|
||||
});
|
||||
test('creates a Some option when given a value', function () {
|
||||
expect(Option::of('test'))->not->toBeNull()
|
||||
->isSome()->toBeTrue()
|
||||
->get()->toBe('test');
|
||||
->isSome->toBeTrue()
|
||||
->value->toBe('test');
|
||||
});
|
||||
test('creates a None option when given PhpOption\None', function () {
|
||||
expect(Option::of(None::create()))->isNone()->toBeTrue();
|
||||
expect(Option::of(None::create()))->isNone->toBeTrue();
|
||||
});
|
||||
test('creates a Some option when given PhpOption\Some', function () {
|
||||
expect(Option::of(Some::create('something')))->isSome()->toBeTrue()->get()->toBe('something');
|
||||
expect(Option::of(Some::create('something')))->isSome->toBeTrue()->value->toBe('something');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,55 +8,55 @@ declare(strict_types=1);
|
||||
|
||||
use BitBadger\InspiredByFSharp\{Option, Result};
|
||||
|
||||
describe('->getOK()', function () {
|
||||
describe('->ok', function () {
|
||||
test('returns OK value for OK result', function () {
|
||||
expect(Result::OK('yay')->getOK())->toBe('yay');
|
||||
expect(Result::OK('yay')->ok)->toBe('yay');
|
||||
});
|
||||
test('throws an exception for Error result', function () {
|
||||
expect(fn() => Result::Error('whoops')->getOK())->toThrow(InvalidArgumentException::class);
|
||||
expect(fn() => Result::Error('whoops')->ok)->toThrow(InvalidArgumentException::class);
|
||||
});
|
||||
});
|
||||
|
||||
describe('->getError()', function () {
|
||||
describe('->error', function () {
|
||||
test('throws an exception for OK result', function () {
|
||||
expect(fn() => Result::OK('yeah')->getError())->toThrow(InvalidArgumentException::class);
|
||||
expect(fn() => Result::OK('yeah')->error)->toThrow(InvalidArgumentException::class);
|
||||
});
|
||||
test('returns Error value for Error result', function () {
|
||||
expect(Result::Error('boo')->getError())->toBe('boo');
|
||||
expect(Result::Error('boo')->error)->toBe('boo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('->isOK()', function () {
|
||||
describe('->isOK', function () {
|
||||
test('returns true for OK result', function () {
|
||||
expect(Result::OK('ok')->isOK())->toBeTrue();
|
||||
expect(Result::OK('ok')->isOK)->toBeTrue();
|
||||
});
|
||||
test('returns false for Error result', function () {
|
||||
expect(Result::Error('error')->isOK())->toBeFalse();
|
||||
expect(Result::Error('error')->isOK)->toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('->isError()', function () {
|
||||
describe('->isError', function () {
|
||||
test('returns false for OK result', function () {
|
||||
expect(Result::OK('fine')->isError())->toBeFalse();
|
||||
expect(Result::OK('fine')->isError)->toBeFalse();
|
||||
});
|
||||
test('returns true for Error result', function () {
|
||||
expect(Result::Error('not ok')->isError())->toBeTrue();
|
||||
expect(Result::Error('not ok')->isError)->toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
describe('->bind()', function () {
|
||||
test('returns OK when binding against OK with OK', function () {
|
||||
expect(Result::OK('one')->bind(fn($it) => Result::OK("$it two")))
|
||||
->isOK()->toBeTrue()->getOK()->toBe('one two');
|
||||
->isOK->toBeTrue()->ok->toBe('one two');
|
||||
});
|
||||
test('returns Error when binding against OK with Error', function () {
|
||||
expect(Result::OK('three')->bind(fn($it) => Result::Error('back to two')))
|
||||
->isError()->toBeTrue()->getError()->toBe('back to two');
|
||||
->isError->toBeTrue()->error->toBe('back to two');
|
||||
});
|
||||
test('returns Error when binding against Error', function () {
|
||||
$original = Result::Error('oops');
|
||||
$result = $original->bind(fn($it) => Result::OK('never mind - it worked!'));
|
||||
expect($result->isError())->toBeTrue()
|
||||
expect($result->isError)->toBeTrue()
|
||||
->and($result)->toBe($original);
|
||||
});
|
||||
});
|
||||
@@ -94,7 +94,7 @@ describe('->exists()', function () {
|
||||
describe('->map()', function () {
|
||||
test('maps value for OK', function () {
|
||||
expect(Result::OK('yard')->map(fn($it) => strrev($it)))
|
||||
->isOK()->toBeTrue()->getOK()->toBe('dray');
|
||||
->isOK->toBeTrue()->ok->toBe('dray');
|
||||
});
|
||||
test('throws an exception for OK when mapping result is null', function () {
|
||||
expect(fn() => Result::OK('not null')->map(fn($it) => null))->toThrow(InvalidArgumentException::class);
|
||||
@@ -108,7 +108,7 @@ describe('->map()', function () {
|
||||
return 'hello';
|
||||
});
|
||||
expect($mapped)
|
||||
->isError()->toBeTrue()
|
||||
->isError->toBeTrue()
|
||||
->and($tattle->called)->toBeFalse()
|
||||
->and($mapped)->toBe($error);
|
||||
});
|
||||
@@ -124,13 +124,13 @@ describe('->mapError()', function () {
|
||||
return 'hello';
|
||||
});
|
||||
expect($mapped)
|
||||
->isOK()->toBeTrue()
|
||||
->isOK->toBeTrue()
|
||||
->and($tattle->called)->toBeFalse()
|
||||
->and($mapped)->toBe($ok);
|
||||
});
|
||||
test('maps value for Error', function () {
|
||||
expect(Result::Error('taco')->mapError(fn($it) => str_repeat('*', strlen($it))))
|
||||
->isError()->toBeTrue()->getError()->toBe('****');
|
||||
->isError->toBeTrue()->error->toBe('****');
|
||||
});
|
||||
test('throws an exception for Error when mapping result is null', function () {
|
||||
expect(fn() => Result::Error('pizza')->mapError(fn($it) => null))->toThrow(InvalidArgumentException::class);
|
||||
@@ -161,10 +161,10 @@ describe('->toArray()', function () {
|
||||
|
||||
describe('->toOption()', function () {
|
||||
test('returns a Some option for OK', function () {
|
||||
expect(Result::OK(99)->toOption())->isSome()->toBeTrue()->get()->toBe(99);
|
||||
expect(Result::OK(99)->toOption())->isSome->toBeTrue()->value->toBe(99);
|
||||
});
|
||||
test('returns a None option for Error', function () {
|
||||
expect(Result::Error('file not found')->toOption())->isNone()->toBeTrue();
|
||||
expect(Result::Error('file not found')->toOption())->isNone->toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -173,7 +173,7 @@ describe('->tap()', function () {
|
||||
$value = '';
|
||||
$original = Result::OK('working');
|
||||
$tapped = $original->tap(function (Result $it) use (&$value) {
|
||||
$value = $it->isOK() ? 'OK: ' . $it->getOK() : 'Error: ' . $it->getError();
|
||||
$value = $it->isOK ? 'OK: ' . $it->ok : 'Error: ' . $it->error;
|
||||
});
|
||||
expect($value)->toBe('OK: working')->and($tapped)->toBe($original);
|
||||
});
|
||||
@@ -181,7 +181,7 @@ describe('->tap()', function () {
|
||||
$value = '';
|
||||
$original = Result::Error('failed');
|
||||
$tapped = $original->tap(function (Result $it) use (&$value) {
|
||||
$value = $it->isOK() ? 'OK: ' . $it->getOK() : 'Error: ' . $it->getError();
|
||||
$value = $it->isOK ? 'OK: ' . $it->ok : 'Error: ' . $it->error;
|
||||
});
|
||||
expect($value)->toBe('Error: failed')->and($tapped)->toBe($original);
|
||||
});
|
||||
@@ -189,7 +189,7 @@ describe('->tap()', function () {
|
||||
|
||||
describe('::OK()', function () {
|
||||
test('creates an OK result for a non-null value', function () {
|
||||
expect(Result::OK('something'))->isOK()->toBeTrue()->getOK()->toBe('something');
|
||||
expect(Result::OK('something'))->isOK->toBeTrue()->ok->toBe('something');
|
||||
});
|
||||
test('throws an exception for OK with a null value', function () {
|
||||
expect(fn() => Result::OK(null))->toThrow(InvalidArgumentException::class);
|
||||
@@ -198,7 +198,7 @@ describe('::OK()', function () {
|
||||
|
||||
describe('::Error()', function () {
|
||||
test('creates an Error result for a non-null value', function () {
|
||||
expect(Result::Error('sad trombone'))->isError()->toBeTrue()->getError()->toBe('sad trombone');
|
||||
expect(Result::Error('sad trombone'))->isError->toBeTrue()->error->toBe('sad trombone');
|
||||
});
|
||||
test('throws an exception for Error with a null value', function () {
|
||||
expect(fn() => Result::Error(null))->toThrow(InvalidArgumentException::class);
|
||||
|
||||
Reference in New Issue
Block a user