PHP utility classes whose functionality is inspired by their F# counterparts
Go to file
2024-07-29 13:58:33 -04:00
src Add contains, exists, toArray 2024-07-29 13:58:33 -04:00
tests Add contains, exists, toArray 2024-07-29 13:58:33 -04:00
.gitignore Add Option and tests 2024-07-26 18:10:02 -04:00
composer.json Add toPhpOption; convert both to mostly non-static 2024-07-28 14:15:24 -04:00
composer.lock Add contains, exists, toArray 2024-07-29 13:58:33 -04:00
LICENSE Initial commit 2024-07-26 18:13:27 +00:00
README.md Add contains, exists, toArray 2024-07-29 13:58:33 -04:00

Inspired by F#

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<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
->get(): T ->getOK(): TOK
all throw if called on missing value ->getError(): 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 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.

Finally, we would be remiss to not acknowledge some really cool prior art in this area - the PhpOption 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 project from the same team, though this project's result does not (yet) have any conversion methods for it.

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, 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.