PHP utility classes whose functionality is inspired by their F# counterparts
Go to file
2024-07-28 17:35:11 -04:00
src Add details to README 2024-07-28 17:35:11 -04:00
tests Add toPhpOption; convert both to mostly non-static 2024-07-28 14:15:24 -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 PhpOption support to Option::of 2024-07-27 21:20:35 -04:00
LICENSE Initial commit 2024-07-26 18:13:27 +00:00
README.md Add details to README 2024-07-28 17:35:11 -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
::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<T>): void) ->tap(callable(Result<TOK, TError>): void)

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.
  • ->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<TOK, TError> 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 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.