From efb3a4461edcb23e0dd82068adeb0591240870b0 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sun, 28 Jul 2024 17:35:11 -0400 Subject: [PATCH] Add details to README --- README.md | 64 ++++++++++++++++++++++++++++++++++++++++++++++++-- src/Option.php | 8 +++---- src/Result.php | 1 + 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e84ae90..afc8a3f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,63 @@ -# inspired-by-fsharp +# Inspired by F# -PHP utility classes whose functionality is inspired by their F# counterparts \ No newline at end of file +This project contains PHP utility classes whose functionality is inspired by their F# counterparts. + +## 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` represents a variable that may or may not have a value. `Result` represents the result of an action; the "ok" and "error" states both provide a value. + +| | `Option`
Replaces `null` checks | `Result`
Replaces exception-based error handling | +|----------------------------------------------------|--------------------------------------------------------------------------------|------------------------------------------------------------------| +| **Creating** | `::Some(T)` for Some
`::None()` for None
`::of($value)` _None if `null`_ | `::OK(TOK)` for OK
`::Error(TError)` for Error | +| **Querying** | `->isSome()`
`->isNone()` | `->isOK()`
`->isError()` | +| **Reading**
_throws if called on missing value_ | `->get()` | `->getOK()`
`->getError()` | +| **Transforming**
_still `Option` or `Result`_ | `->map(callable(T): U)` | `->map(callable(TOK): U)`
`->mapError(callable(TError): U)` | +| **Iterating** | `->iter(callable(T): void)` | `->iter(callable(TOK): void)` | +| **Inspecting**
_returns the original instance_ | `->tap(callable(Option): void)` | `->tap(callable(Result): void)` | + +In addition to this, `Option` 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`. +- `->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. +- `->is(T, $strict = true)` will return `true` if the option is Some and the value matches. Strict equality (the default) uses `===` for the comparison; if strict is set to `false`, the comparison will use `==` instead. +- `->unwrap()` will return `null` for None options and the value for Some options. + +`Result` also provides: +- `toOption()` will transform an OK result to a Some option, and an Error result to a None option. + +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` instances have a `->toPhpOption()` method that will convert these back into PhpOption's `Some` 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. + +## 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, 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. diff --git a/src/Option.php b/src/Option.php index 52448e2..094fd7d 100644 --- a/src/Option.php +++ b/src/Option.php @@ -2,6 +2,7 @@ /** * @author Daniel J. Summers * @license MIT + * @since 1.0.0 */ declare(strict_types=1); @@ -19,8 +20,7 @@ use InvalidArgumentException; * `map` and `iter` become de facto conditional operators). * * `Option::Some(T)` and `Option::None()` create instances. `get()` is available on options, but will throw an exception - * if called on a `None` option. The remaining functions are statically available, and should be provided an `Option` - * instance as their final parameter. + * if called on a `None` option. * * @template T The type of value represented by this option */ @@ -45,8 +45,8 @@ readonly class Option public function get(): mixed { return match (true) { - self::isSome($this) => $this->value, - default => throw new InvalidArgumentException('Cannot get the value of a None option'), + $this->isSome() => $this->value, + default => throw new InvalidArgumentException('Cannot get the value of a None option'), }; } diff --git a/src/Result.php b/src/Result.php index e626c23..b9dab26 100644 --- a/src/Result.php +++ b/src/Result.php @@ -2,6 +2,7 @@ /** * @author Daniel J. Summers * @license MIT + * @since 1.0.0 */ declare(strict_types=1);