ascetik/hypothetik

Home-made Maybe monad

v0.3.0 2024-01-16 17:30 UTC

This package is auto-updated.

Last update: 2024-06-16 18:27:20 UTC


README

Hypothetik

[EN]

Home made OOP "Monad", for an easier management of hypothetical values.

Release notes

Version 0.3.0 : still a draft version.

Php version : 8.2.14

  • New Hypothetik interface, describing the behavior of a monad included in this package.
  • Maybe class implements Hypothetik interface
  • New When, Hypothetik implementation to handle booleans.

Descriptions

Interfaces

OptionnalValue is a general interface shared by both Hypothetik an Option instances.

  • OptionnalValue::isValid(): bool : check validity of a value (!null & !false)
  • OptionnalValue::value(): mixed : return Option raw value

The Hypothetik interface describes the way to handle a value which may be null or false using callables.

  • Hypothetik::apply(callable, ...mixed): mixed : return the result of given callable using the Option value
  • Hypothetik::either(callable, ...mixed): Either : return an Either instance according to an Option.
  • Hypothetik::then(callable, ...mixed): Hypothetik : return a new Hypothetic instance with the result of given callable.
  • Hypothetik::otherwise(mixed): Hypothetik : choose an alternative to return if the Option value is invalid.

The Option interface describes the behavior of an instance containing the exepcted value :

  • Option::apply(callable, ?array): mixed : return the result of given function with Option value as first parameter
  • Option::equals(Option): bool : Check equality with another Option
  • Option::isValid(): bool : see OptionnalValue interface
  • Option::value(): mixed : see OptionnalValue interface

This package includes 2 Option implementations : final class None and final class Some. Anyone can build another implementation of Option to replace Some class. That's why this interface is exposed here.

An Option is a simple ValueObject with simple behaviors, unuseful outside of an Hypothetik instance.

Available Implementations

final class Maybe : The Maybe class is the main tool of this package. It handles an Option which may contain a value, or may not, and drives different operations on this value, or not...

  • Maybe::equals(Maybe): bool : check equality with another Maybe instance.
  • Maybe::apply(callable, ...mixed): mixed : see Hypothetik interface
  • Maybe::either(callable): Either : see Hypothetik interface
  • Maybe::isValid(): bool : see OptionnalValue interface
  • Maybe::otherwise(mixed): Hypothetik : see Hypothetik interface
  • Maybe::then(callable, ...mixed): Hypothetik : see Hypothetik interface
  • Maybe::value(): mixed : see OptionnalValue interface
  • static Maybe::not(): Maybe : return a Maybe instance with a None option
  • static Maybe::of(Option): Maybe : return a Maybe instance with given Option instance.
  • static Maybe::some(mixed): Hypothetik : return a Hypothetik instance with given value

Maybe contructor is private. See examples below for instanciation.

final class When : (v.0.3.0) This implementation works almost like Maybe. The difference is that When contains an Option with a bool value and a falsy Option is considered as invalid.

  • When::apply(callable, ...mixed): mixed : see Hypothetik interface
  • When::either(callable): Either : see Hypothetik interface
  • When::isValid(): bool : see OptionnalValue interface
  • When::otherwise(mixed): Hypothetik : see Hypothetik interface
  • When::then(callable, ...mixed): Hypothetik : see Hypothetik interface
  • When::value(): mixed : see OptionnalValue interface
  • static When::ever(bool): When : return a Maybe instance with given value

Private constructor. Use When::ever(bool) or Maybe::some(bool) methods to build an instance.

final class Either :

The Either class handles a function to execute according to a Maybe Option value.

  • Either::or(callable, ...mixed): Either : return an new Either instance if maybe's value is null.
  • Either::try(): Maybe : return a new Maybe instance with the result of current Either function.
  • Either::value(): mixed : retourne la valeur contenue par le Maybe
  • Either::static use(Maybe, callable, ...mixed): Either : récupération d'une instance de Either, constructeur privé

An Either instance is exposed by a Hypothetik implementation for usage, unuseful in any other context.

final class None is a "null value" Option. final class Some is a not null value Option.

Usage

Construction

As Maybe constructor access is not available, 3 factory methods are provided :

$not = Maybe::not(); // Maybe<null>

$some = Maybe::some('my value'); // Maybe<string>
$someobj = Maybe::some(new MyOwnInstance()); // Maybe<MyOwnInstance>
$nullAnyway = Maybe::some(null); // Maybe<null>

$any = Maybe::of(new MyOwnStringOption('any string value')); // Maybe<string>
$anyobj = Maybe::of(new MyOwnOption(new MyOwnInstance())); // Maybe<MyOwnInstance>
$anyNullObj = Maybe::of(new MyOwnNullOption()); // Maybe<null>

// version 0.3.0
$truthy = Maybe::some(true); // this is a truthy "When" instance
$falsy = Maybe::some(false); // this is a falsy "When" instance

Valid value : mixed value not null

To retrieve raw optionnal value from the "$some" Maybe instance of previous example :

echo $some->value(); // "my value"

Pass an optionnal value through a function an get the result :

echo $some->apply(strtoupper(...)); // "MY VALUE"

The Option value is always passed to the function as first parameter.

It is possible to add arguments, separated by comas. The order of the arguments is important.

Another example with some added arguments :

$pathToAboutPage = Maybe::some('/about');
echo $pathToAboutPage->apply(trim(...), '/'); // "about", without forward slash

$function = fn(string $value, string $separator, string $add)=> trim($value, $separator) . '-' . $add
echo $pathToAboutPage->apply($function, '/','page' ); //"about-page"

It is possible to get a new Hypothetik instance containing the result of a function. Once again, the Option value is always passed to the function as first parameter and arguments can be added :

$maybeThen = $some->then(strtoupper(...)); // retourne un nouveau Maybe contenant "MY VALUE"
echo $maybeThen->value(); // affiche "MY VALUE"

As a new Maybe instance is returned, we can chain calls of this method :

echo $some->then(strtoupper(...)) // return a new Maybe containing "MY VALUE"
    ->then(fn(string $value) => $value.' is not null')
    ->value(); // "MY VALUE is not null"

Invalid value : null value

With a null value, things are slightly different. Both apply() and value() methods will return null again. The then() method returns a Maybe with a null Option value.

Take a look at the "$not" instance from the first example :

echo $not->value(); // prints nothing because null
echo $not->apply(strtoupper(...)); // null too, function is not applied
echo $not->then(strtoupper(...))->value(); // still null

Maybe provides a way to substitute an "invalid" instance to a valid one by using otherwise method :

$otherwise = $not->otherwise('nothing');
echo $otherwise->value(); // prints "nothing"
echo $otherwise->apply(strtoupper(...)); // prints "NOTHING"
echo $otherwise->then(strtoupper(...)) // // Maybe<'NOTHING'>
               ->value(); // "NOTHING" again.

Some other examples chaining methods :

echo $not->then(strtoupper(...)) // run strtoupper with a Maybe<null> won't work
    ->otherwise('i replace null') // new Maybe<string> available after first then() call
    ->then(fn(string $value) => $value . ' for demonstration') // run the function with the new instance
    ->value(); // prints "i replace null for demonstration"

echo $not->otherwise('i replace null') // new Maybe<string> available
    ->then(strtoupper(...)) // now transform initial string to upper case
    ->then(fn(string $value) => $value . ' for demonstration') // and append another string to the previous value
    ->value(); // prints "I REPLACE NULL for demonstration"

The otherwise method is only applied when the value is null. So :

echo $some->otherwise('my other value') // initial $some instance returned
    ->then(strtoupper(...))
    ->then(fn(string $value) => $value . ' for demonstration')
    ->value(); // prints "MY VALUE for demonstration"

Of course, we already know the content of the instances of the examples above. During runtime, we just can suppose that our value could be null. Sometimes, then() and otherwise() are not enough to make the job we want to. Another possibility is to use either() :

// with Some value
echo $some->either(toUpperCase(...))
    ->or(fn() => 'late value')
    ->value(); // prints "MY VALUE"

// with None value
echo $not->either(toUpperCase(...))
    ->or(fn() => 'late value')
    ->value(); // prints "late value"

And to retrieve a new Hypothetik instance from Either :

// with Some value
echo $some->either(toUpperCase(...))
    ->or(fn() => 'late value')
    ->try()    // returns a Maybe<string> from "$some" value
    ->then(fn(string $value) => $value . ' for demonstration')
    ->value(); // prints "MY VALUE for demonstration"

// with None value
echo $not->either(toUpperCase(...)) // won't run this function
    ->or(fn() => 'late value') // returns a new Either instance holding this new function
    ->try() // returns a Maybe<string> with "late value'
    ->then(fn(string $value) => $value . ' for demonstration') // append a string and return another Maybe with new complete string
    ->value(); // prints "late value for demonstration"

Boolean value :

An hypothetik boolean value works a different way. It always holds an Option with a boolean value where false is the invalid one. The Hypothetik interface ensures a fully substitutionnable instance, providing the hability to chain methods from a Maybe to a When or reverse.

Here's a simple example :

$phrase = 'this is just a test';

$when = Maybe::some(str_contains($phrase, 'just')); // truthy When
echo $when->value() ? 'valid' : 'invalid'; // 'valid'
$whenNot = Maybe::some(str_contains($phrase, 'only')); // falsy When
echo $whenNot->value() ? 'valid' : 'invalid'; // 'invalid'

When class has its own static factory method :

$when = When::ever(true); // or false...

Methods apply() and then won't use the boolean value as first function parameter, this time. Additionnal rguments are allowed, separated by comas, just like Maybe.

Combining Maybe and When instance calls :

$truthyWhen = Maybe::some('/about') // instance of Maybe</about>
    ->then(fn (string $value) => str_starts_with($value, '/')) // instance of When<false>
    ->either(fn() => 'truthy result') // will be executed
    ->or(fn() => 'falsy result') // won't be executed
    ->try(); // Maybe<'truthy result'>
echo $when->value(); // 'truthy result'

$falsyWhen = Maybe::some('/about') // instance of Maybe</about>
    ->then(fn (string $value) => trim($value, '/')) // instance of Maybe<about>
    ->then(fn (string $value) => str_starts_with($value, '/')) // instance of When<false>
    ->either(fn() => 'truthy result') // won't be executed
    ->or(fn() => 'falsy result') // will be executed
    ->value(); // raw value
echo $when; // 'falsy result'

Notes

No dependency injection. User has to provide required instances if needed. A Maybe cannot carry another Hypothetik, an Option cannot carry another Option. Trying to do so will return the given instance as is.

Issues

I'm still not able to use Php Documentation properly in order to provide autocompletion from any IDE. Problems on generic types handling.

I still don't need any Hypothetik container to handle multiple Hypothetik instances. I'll think about this kind of implementation only if necessary...

Maybe some tests are still missing. I'm not sure to cover all possible use cases.