Add toPhpOption; convert both to mostly non-static

This commit is contained in:
Daniel J. Summers 2024-07-28 14:15:24 -04:00
parent 7d25b9ea28
commit 5a8a41a660
5 changed files with 581 additions and 567 deletions

View File

@ -17,7 +17,7 @@
"rss": "https://git.bitbadger.solutions/bit-badger/inspired-by-fsharp.rss" "rss": "https://git.bitbadger.solutions/bit-badger/inspired-by-fsharp.rss"
}, },
"require": { "require": {
"php": "^8" "php": "^8.2"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^11", "phpunit/phpunit": "^11",

View File

@ -8,6 +8,7 @@ declare(strict_types=1);
namespace BitBadger\InspiredByFSharp; namespace BitBadger\InspiredByFSharp;
use Error;
use InvalidArgumentException; use InvalidArgumentException;
/** /**
@ -25,7 +26,7 @@ use InvalidArgumentException;
*/ */
readonly class Option readonly class Option
{ {
/** @var ?T $value The value for this option */ /** @var T|null $value The value for this option */
private mixed $value; private mixed $value;
/** /**
@ -49,6 +50,138 @@ readonly class Option
}; };
} }
/**
* 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);
}
/**
* 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();
}
/**
* Get the value, or a default value, from an option
*
* @param T $default The default value to return if the option is `None`
* @return T The `Some` value, or the default value if the option is `None`
*/
public function getOrDefault(mixed $default): mixed
{
return $this->value ?? $default;
}
/**
* Get the value, or return the value of a callable function
*
* @template U The return type of the callable provided
* @param callable(): U $f The callable function to use for `None` options
* @return T|mixed The value if `Some`, the result of the callable if `None`
*/
public function getOrCall(callable $f): mixed
{
return $this->value ?? $f();
}
/**
* Map this optional value to another value
*
* @template U The type of the mapping function
* @param callable(T): U $f The mapping function
* @return Option<U> A `Some` instance with the transformed value if `Some`, `None` otherwise
*/
public function map(callable $f): self
{
return $this->isSome() ? self::Some($f($this->get())) : $this;
}
/**
* Execute a function on the value (if it exists)
*
* @param callable(T): void $f The function to call
*/
public function iter(callable $f): void
{
if ($this->isSome()) {
$f($this->value);
}
}
/**
* Transform an option into `None` if it does not match the given function
*
* @param callable(T): bool $f The filter function to run
* @return Option<T> The option, if it was `Some` and the function returned true; `None` otherwise
*/
public function filter(callable $f): self
{
return match (true) {
$this->isNone() => $this,
default => $f($this->value) ? $this : self::None(),
};
}
/**
* Does the option have the given value?
*
* @param T $value The value to be checked
* @param bool $strict True for strict equality (`===`), false for loose equality (`==`)
* @return bool True if the value matches, false if not; `None` always returns false
*/
public function is(mixed $value, bool $strict = true): bool
{
return match (true) {
$this->isNone() => false,
default => $strict ? $this->value === $value : $this->value == $value,
};
}
/**
* Safely retrieve the optional value as a nullable value
*
* @return T|null The value for `Some` instances, `null` for `None` instances
*/
public function unwrap(): mixed
{
return $this->value;
}
/**
* Tap into the `Result` for a secondary action, returning the result
*
* @param callable(Option<T>): mixed $f The function to run (return value is ignored)
* @return Option<T> The same option provided
*/
public function tap(callable $f): Option
{
$f($this);
return $this;
}
/**
* Convert this to a PhpOption option
*
* @return mixed An option from the PhpOption library
*/
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),
default => throw new Error('PhpOption types could not be found'),
};
}
/** /**
* Create a `Some` option with the given value * Create a `Some` option with the given value
* *
@ -87,132 +220,4 @@ readonly class Option
default => new self($value), default => new self($value),
}; };
} }
/**
* Does this option have a `None` value?
*
* @param Option<T> $it The option in question
* @return bool True if the option is `None`, false if it is `Some`
*/
public static function isNone(Option $it): bool
{
return is_null($it->value);
}
/**
* Does this option have a `Some` value?
*
* @param Option<T> $it The option in question
* @return bool True if the option is `Some`, false if it is `None`
*/
public static function isSome(Option $it): bool
{
return !self::isNone($it);
}
/**
* Get the value, or a default value, from an option
*
* @param T $default The default value to return if the option is `None`
* @param Option<T> $it The option in question
* @return T The `Some` value, or the default value if the option is `None`
*/
public static function defaultValue(mixed $default, Option $it): mixed
{
return self::isSome($it) ? $it->get() : $default;
}
/**
* Get the value, or return the value of a callable function
*
* @template U The return type of the callable provided
* @param callable(): U $f The callable function to use for `None` options
* @param Option<T> $it The option in question
* @return T|mixed The value if `Some`, the result of the callable if `None`
*/
public static function getOrCall(callable $f, Option $it): mixed
{
return self::isSome($it) ? $it->get() : $f();
}
/**
* Map this optional value to another value
*
* @template U The type of the mapping function
* @param callable(T): U $f The mapping function
* @param Option<T> $it The option in question
* @return Option<U> A `Some` instance with the transformed value if `Some`, `None` otherwise
*/
public static function map(callable $f, Option $it): self
{
return self::isSome($it) ? self::Some($f($it->get())) : self::None();
}
/**
* Execute a function on the value (if it exists)
*
* @param callable(T): void $f The function to call
* @param Option<T> $it The option in question
*/
public static function iter(callable $f, Option $it): void
{
if (self::isSome($it)) {
$f($it->get());
}
}
/**
* Transform an option into `None` if it does not match the given function
*
* @param callable(T): bool $f The filter function to run
* @param Option<T> $it The option in question
* @return Option<T> The option, if it was `Some` and the function returned true; `None` otherwise
*/
public static function filter(callable $f, Option $it): self
{
return match (true) {
self::isNone($it) => self::None(),
default => $f($it->get()) ? self::Some($it->get()) : self::None(),
};
}
/**
* Does the option have the given value?
*
* @param T $value The value to be checked
* @param Option<T> $it The option in question
* @param bool $strict True for strict equality (`===`), false for loose equality (`==`)
* @return bool True if the value matches, false if not; `None` always returns false
*/
public static function is(mixed $value, Option $it, bool $strict = true): bool
{
return match (true) {
self::isNone($it) => false,
default => $strict ? $it->value === $value : $it->value == $value,
};
}
/**
* Safely retrieve the optional value as a nullable value
*
* @param Option<T> $it The option in question
* @return ?T The value for `Some` instances, `null` for `None` instances
*/
public static function unwrap(Option $it): mixed
{
return self::isSome($it) ? $it->get() : null;
}
/**
* Tap into the `Result` for a secondary action, returning the result
*
* @param callable(Option<T>): mixed $f The function to run (return value is ignored)
* @param Option<T> $it The option against which the function should be run
* @return Option<T> The same option provided
*/
public static function tap(callable $f, Option $it): Option
{
$f($it);
return $it;
}
} }

View File

@ -66,6 +66,84 @@ readonly class Result
return $this->errorValue->get(); return $this->errorValue->get();
} }
/**
* 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();
}
/**
* 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();
}
/**
* Map an `OK` result to another, leaving an `Error` result unmodified
*
* @template U The type of the mapping function
* @param callable(TOK): U $f The mapping function
* @return Result<U, TError> A transformed `OK` instance or the original `Error` instance
*/
public function map(callable $f): self
{
return $this->isOK() ? self::OK($f($this->getOK())) : $this;
}
/**
* Map an `Error` result to another, leaving an `OK` result unmodified
*
* @template U The type of the mapping function
* @param callable(TError): U $f The mapping function
* @return Result<TOK, U> A transformed `Error` instance or the original `OK` instance
*/
public function mapError(callable $f): self
{
return $this->isError() ? self::Error($f($this->getError())) : $this;
}
/**
* Execute a function on an `OK` value (if it exists)
*
* @param callable(TOK): void $f The function to call
*/
public function iter(callable $f): void
{
if ($this->isOK()) {
$f($this->getOK());
}
}
/**
* Transform a `Result`'s `OK` value to an `Option`
*
* @return Option<TOK> A `Some` option with the OK value if `OK`, `None` if `Error`
*/
public function toOption(): Option
{
return $this->isOK() ? Option::Some($this->getOK()) : Option::None();
}
/**
* Tap into the `Result` for a secondary action, returning the result
*
* @param callable(Result<TOK, TError>): mixed $f The function to run (return value is ignored)
* @return Result<TOK, TError> The same result provided
*/
public function tap(callable $f): Result
{
$f($this);
return $this;
}
/** /**
* Create an `OK` result * Create an `OK` result
* *
@ -93,89 +171,4 @@ readonly class Result
} }
return new self(errorValue: $value); return new self(errorValue: $value);
} }
/**
* Is the given result `OK`?
*
* @param Result $it The result in question
* @return bool True if the result is `OK`, false if it is `Error`
*/
public static function isOK(Result $it): bool
{
return Option::isSome($it->okValue);
}
/**
* Is the given result `Error`?
*
* @param Result $it The result in question
* @return bool True if the result is `Error`, false if it is `OK`
*/
public static function isError(Result $it): bool
{
return Option::isSome($it->errorValue);
}
/**
* Map an `OK` result to another, leaving an `Error` result unmodified
*
* @template U The type of the mapping function
* @param callable(TOK): U $f The mapping function
* @param Result<TOK, TError> $it The result in question
* @return Result<U, TError> A transformed `OK` instance, or an `Error` instance with the same value
*/
public static function map(callable $f, Result $it): self
{
return self::isOK($it) ? self::OK($f($it->getOK())) : self::Error($it->getError());
}
/**
* Map an `Error` result to another, leaving an `OK` result unmodified
*
* @template U The type of the mapping function
* @param callable(TError): U $f The mapping function
* @param Result<TOK, TError> $it The result in question
* @return Result<TOK, U> A transformed `Error` instance, or an `OK` instance with the same value
*/
public static function mapError(callable $f, Result $it): self
{
return self::isError($it) ? self::Error($f($it->getError())) : self::OK($it->getOK());
}
/**
* Execute a function on an `OK` value (if it exists)
*
* @param callable(TOK): void $f The function to call
* @param Result<TOK, TError> $it The result in question
*/
public static function iter(callable $f, Result $it): void
{
if (self::isOK($it)) {
$f($it->getOK());
}
}
/**
* Transform a `Result`'s `OK` value to an `Option`
*
* @param Result<TOK, TError> $it The result in question
* @return Option<TOK> A `Some` option with the OK value if `OK`, `None` if `Error`
*/
public static function toOption(Result $it): Option
{
return Result::isOK($it) ? Option::Some($it->getOK()) : Option::None();
}
/**
* Tap into the `Result` for a secondary action, returning the result
*
* @param callable(Result<TOK, TError>): mixed $f The function to run (return value is ignored)
* @param Result<TOK, TError> $it The result against which the function should be run
* @return Result<TOK, TError> The same result provided
*/
public static function tap(callable $f, Result $it): Result
{
$f($it);
return $it;
}
} }

View File

@ -23,7 +23,7 @@ class OptionTest extends TestCase
public function testGetSucceedsForSome(): void public function testGetSucceedsForSome(): void
{ {
$it = Option::Some(9); $it = Option::Some(9);
$this->assertTrue(Option::isSome($it), 'The option should have been "Some"'); $this->assertTrue($it->isSome(), 'The option should have been "Some"');
$this->assertEquals(9, $it->get(), 'The value was incorrect'); $this->assertEquals(9, $it->get(), 'The value was incorrect');
} }
@ -34,10 +34,223 @@ class OptionTest extends TestCase
Option::None()->get(); Option::None()->get();
} }
#[TestDox('IsNone succeeds with None')]
public function testIsNoneSucceedsWithNone(): void
{
$this->assertTrue(Option::None()->isNone(), '"None" should return true');
}
#[TestDox('IsNone succeeds with Some')]
public function testIsNoneSucceedsWithSome(): void
{
$this->assertFalse(Option::Some(8)->isNone(), '"Some" should return false');
}
#[TestDox('IsSome succeeds with None')]
public function testIsSomeSucceedsWithNone(): void
{
$this->assertFalse(Option::None()->isSome(), '"None" should return false');
}
#[TestDox('IsSome succeeds with Some')]
public function testIsSomeSucceedsWithSome(): void
{
$this->assertTrue(Option::Some('boo')->isSome(), '"Some" should return true');
}
#[TestDox('GetOrDefault succeeds with None')]
public function testGetOrDefaultSucceedsWithNone(): void
{
$this->assertEquals('yes', Option::None()->getOrDefault('yes'), 'Value should have been default');
}
#[TestDox('GetOrDefault succeeds with Some')]
public function testGetOrDefaultSucceedsWithSome(): void
{
$this->assertEquals('no', Option::Some('no')->getOrDefault('yes'), 'Value should have been from option');
}
#[TestDox('GetOrCall succeeds with None')]
public function testGetOrCallSucceedsWithNone(): void
{
$value = Option::None()->getOrCall(new class { public function __invoke(): string { return 'called'; } });
$this->assertEquals('called', $value, 'The value should have been obtained from the callable');
}
#[TestDox('GetOrCall succeeds with Some')]
public function testGetOrCallSucceedsWithSome(): void
{
$value = Option::Some('passed')->getOrCall(
new class { public function __invoke(): string { return 'called'; } });
$this->assertEquals('passed', $value, 'The value should have been obtained from the option');
}
#[TestDox('Map succeeds with None')]
public function testMapSucceedsWithNone(): void
{
$tattle = new class { public bool $called = false; };
$none = Option::None();
$mapped = $none->map(function () use ($tattle)
{
$tattle->called = true;
return 'hello';
});
$this->assertTrue($mapped->isNone(), 'The mapped option should be "None"');
$this->assertFalse($tattle->called, 'The mapping function should not have been called');
$this->assertSame($none, $mapped, 'The same "None" instance should have been returned');
}
#[TestDox('Map succeeds with Some')]
public function testMapSucceedsWithSome(): void
{
$mapped = Option::Some('abc ')->map(fn($it) => str_repeat($it, 2));
$this->assertTrue($mapped->isSome(), 'The mapped option should be "Some"');
$this->assertEquals('abc abc ', $mapped->get(), 'The mapping function was not called correctly');
}
#[TestDox('Map fails with Some when mapping is null')]
public function testMapFailsWithSomeWhenMappingIsNull(): void
{
$this->expectException(InvalidArgumentException::class);
Option::Some('oof')->map(fn($it) => null);
}
#[TestDox('Iter succeeds with None')]
public function testIterSucceedsWithNone(): void
{
$target = new class { public mixed $called = null; };
Option::None()->iter(function () use ($target) { $target->called = 'uh oh'; });
$this->assertNull($target->called, 'The function should not have been called');
}
#[TestDox('Iter succeeds with Some')]
public function testIterSucceedsWithSome(): void
{
$target = new class { public mixed $called = null; };
Option::Some(33)->iter(function ($it) use ($target) { $target->called = $it; });
$this->assertEquals(33, $target->called, 'The function should have been called with the "Some" value');
}
#[TestDox('Filter succeeds with None')]
public function testFilterSucceedsWithNone(): void
{
$tattle = new class { public bool $called = false; };
$none = Option::None();
$filtered = $none->filter(function () use ($tattle)
{
$tattle->called = true;
return true;
});
$this->assertTrue($filtered->isNone(), 'The filtered option should have been "None"');
$this->assertFalse($tattle->called, 'The callable should not have been called');
$this->assertSame($none, $filtered, 'The "None" instance returned should have been the one passed');
}
#[TestDox('Filter succeeds with Some when true')]
public function testFilterSucceedsWithSomeWhenTrue(): void
{
$some = Option::Some(12);
$filtered = $some->filter(fn($it) => $it % 2 === 0);
$this->assertTrue($filtered->isSome(), 'The filtered option should have been "Some"');
$this->assertEquals(12, $filtered->get(), 'The filtered option value is incorrect');
$this->assertSame($some, $filtered, 'The same "Some" instance should have been returned');
}
#[TestDox('Filter succeeds with Some when false')]
public function testFilterSucceedsWithSomeWhenFalse(): void
{
$some = Option::Some(23);
$filtered = $some->filter(fn($it) => $it % 2 === 0);
$this->assertTrue($filtered->isNone(), 'The filtered option should have been "None"');
}
#[TestDox('Is succeeds with None')]
public function testIsSucceedsWithNone(): void
{
$this->assertFalse(Option::None()->is(null), '"None" should always return false');
}
#[TestDox('Is succeeds with Some when strictly equal')]
public function testIsSucceedsWithSomeWhenStrictlyEqual(): void
{
$this->assertTrue(Option::Some(3)->is(3), '"Some" with strict equality should be true');
}
#[TestDox('Is succeeds with Some when strictly unequal')]
public function testIsSucceedsWithSomeWhenStrictlyUnequal(): void
{
$this->assertFalse(Option::Some('3')->is(3), '"Some" with strict equality should be false');
}
#[TestDox('Is succeeds with Some when loosely equal')]
public function testIsSucceedsWithSomeWhenLooselyEqual(): void
{
$this->assertTrue(Option::Some('3')->is(3, strict: false), '"Some" with loose equality should be true');
}
#[TestDox('Is succeeds with Some when loosely unequal')]
public function testIsSucceedsWithSomeWhenLooselyUnequal(): void
{
$this->assertFalse(Option::Some('3')->is(4, strict: false), '"Some" with loose equality should be false');
}
#[TestDox('Unwrap succeeds with None')]
public function testUnwrapSucceedsWithNone(): void
{
$this->assertNull(Option::None()->unwrap(), '"None" should return null');
}
#[TestDox('Unwrap succeeds with Some')]
public function testUnwrapSucceedsWithSome(): void
{
$this->assertEquals('boy howdy', Option::Some('boy howdy')->unwrap(), '"Some" should return its value');
}
#[TestDox('Tap succeeds with Some')]
public function testTapSucceedsWithSome(): void
{
$value = '';
$original = Option::Some('testing');
$tapped = $original->tap(
function (Option $it) use (&$value) { $value = $it->isSome() ? $it->get() : 'none'; });
$this->assertEquals('testing', $value, 'The tapped function was not called');
$this->assertSame($original, $tapped, 'The same option should have been returned');
}
#[TestDox('Tap succeeds with None')]
public function testTapSucceedsWithNone(): void
{
$value = '';
$original = Option::None();
$tapped = $original->tap(
function (Option $it) use (&$value) { $value = $it->isSome() ? $it->get() : 'none'; });
$this->assertEquals('none', $value, 'The tapped function was not called');
$this->assertSame($original, $tapped, 'The same option should have been returned');
}
#[TestDox('ToPhpOption succeeds for Some')]
public function testToPhpOptionSucceedsForSome(): void
{
$opt = Option::Some('php')->toPhpOption();
$this->assertNotNull($opt, 'The PhpOption should not have been null');
$this->assertInstanceOf('PhpOption\Some', $opt, 'The PhpOption should have been "Some"');
$this->assertTrue($opt->isDefined(), 'There should have been a value for the PhpOption');
$this->assertEquals('php', $opt->get(), 'The value was not correct');
}
#[TestDox('ToPhpOption succeeds for None')]
public function testToPhpOptionSucceedsForNone(): void
{
$opt = Option::None()->toPhpOption();
$this->assertNotNull($opt, 'The PhpOption should not have been null');
$this->assertInstanceOf('PhpOption\None', $opt, 'The PhpOption should have been "None"');
$this->assertFalse($opt->isDefined(), 'There should not have been a value for the PhpOption');
}
public function testSomeSucceedsWithValue(): void public function testSomeSucceedsWithValue(): void
{ {
$it = Option::Some('hello'); $it = Option::Some('hello');
$this->assertTrue(Option::isSome($it), 'The option should have been "Some"'); $this->assertTrue($it->isSome(), 'The option should have been "Some"');
} }
public function testSomeFailsWithNull(): void public function testSomeFailsWithNull(): void
@ -49,19 +262,19 @@ class OptionTest extends TestCase
public function testNoneSucceeds(): void public function testNoneSucceeds(): void
{ {
$it = Option::None(); $it = Option::None();
$this->assertTrue(Option::isNone($it), 'The option should have been "None"'); $this->assertTrue($it->isNone(), 'The option should have been "None"');
} }
public function testOfSucceedsWithNull(): void public function testOfSucceedsWithNull(): void
{ {
$it = Option::of(null); $it = Option::of(null);
$this->assertTrue(Option::isNone($it), '"null" should have created a "None" option'); $this->assertTrue($it->isNone(), '"null" should have created a "None" option');
} }
public function testOfSucceedsWithNonNull(): void public function testOfSucceedsWithNonNull(): void
{ {
$it = Option::of('test'); $it = Option::of('test');
$this->assertTrue(Option::isSome($it), 'A non-null value should have created a "Some" option'); $this->assertTrue($it->isSome(), 'A non-null value should have created a "Some" option');
$this->assertEquals('test', $it->get(), 'The value was not assigned correctly'); $this->assertEquals('test', $it->get(), 'The value was not assigned correctly');
} }
@ -69,7 +282,7 @@ class OptionTest extends TestCase
public function testOfSucceedsWithPhpOptionSome(): void public function testOfSucceedsWithPhpOptionSome(): void
{ {
$it = Option::of(Some::create('something')); $it = Option::of(Some::create('something'));
$this->assertTrue(Option::isSome($it), 'A "Some" PhpOption should have created a "Some" option'); $this->assertTrue($it->isSome(), 'A "Some" PhpOption should have created a "Some" option');
$this->assertEquals('something', $it->get(), 'The value was not assigned correctly'); $this->assertEquals('something', $it->get(), 'The value was not assigned correctly');
} }
@ -77,202 +290,6 @@ class OptionTest extends TestCase
public function testOfSucceedsWithPhpOptionNone(): void public function testOfSucceedsWithPhpOptionNone(): void
{ {
$it = Option::of(None::create()); $it = Option::of(None::create());
$this->assertTrue(Option::isNone($it), 'A "None" PhpOption should have created a "None" option'); $this->assertTrue($it->isNone(), 'A "None" PhpOption should have created a "None" option');
}
#[TestDox('IsNone succeeds with None')]
public function testIsNoneSucceedsWithNone(): void
{
$this->assertTrue(Option::isNone(Option::None()), '"None" should return true');
}
#[TestDox('IsNone succeeds with Some')]
public function testIsNoneSucceedsWithSome(): void
{
$this->assertFalse(Option::isNone(Option::Some(8)), '"Some" should return false');
}
#[TestDox('IsSome succeeds with None')]
public function testIsSomeSucceedsWithNone(): void
{
$this->assertFalse(Option::isSome(Option::None()), '"None" should return false');
}
#[TestDox('IsSome succeeds with Some')]
public function testIsSomeSucceedsWithSome(): void
{
$this->assertTrue(Option::isSome(Option::Some('boo')), '"Some" should return true');
}
#[TestDox('DefaultValue succeeds with None')]
public function testDefaultValueSucceedsWithNone(): void
{
$this->assertEquals('yes', Option::defaultValue('yes', Option::None()), 'Value should have been default');
}
#[TestDox('DefaultValue succeeds with Some')]
public function testDefaultValueSucceedsWithSome(): void
{
$this->assertEquals('no', Option::defaultValue('yes', Option::Some('no')),
'Value should have been from option');
}
#[TestDox('GetOrCall succeeds with None')]
public function testGetOrCallSucceedsWithNone(): void
{
$value = Option::getOrCall(new class { public function __invoke(): string { return 'called'; } },
Option::None());
$this->assertEquals('called', $value, 'The value should have been obtained from the callable');
}
#[TestDox('GetOrCall succeeds with Some')]
public function testGetOrCallSucceedsWithSome(): void
{
$value = Option::getOrCall(new class { public function __invoke(): string { return 'called'; } },
Option::Some('passed'));
$this->assertEquals('passed', $value, 'The value should have been obtained from the option');
}
#[TestDox('Map succeeds with None')]
public function testMapSucceedsWithNone(): void
{
$tattle = new class { public bool $called = false; };
$none = Option::None();
$mapped = Option::map(function ($ignored) use ($tattle)
{
$tattle->called = true;
return 'hello';
}, $none);
$this->assertTrue(Option::isNone($mapped), 'The mapped option should be "None"');
$this->assertFalse($tattle->called, 'The mapping function should not have been called');
$this->assertNotSame($none, $mapped, 'There should have been a new "None" instance returned');
}
#[TestDox('Map succeeds with Some')]
public function testMapSucceedsWithSome(): void
{
$mapped = Option::map(fn($it) => str_repeat($it, 2), Option::Some('abc '));
$this->assertTrue(Option::isSome($mapped), 'The mapped option should be "Some"');
$this->assertEquals('abc abc ', $mapped->get(), 'The mapping function was not called correctly');
}
#[TestDox('Map fails with Some when mapping is null')]
public function testMapFailsWithSomeWhenMappingIsNull(): void
{
$this->expectException(InvalidArgumentException::class);
Option::map(fn($it) => null, Option::Some('oof'));
}
#[TestDox('Iter succeeds with None')]
public function testIterSucceedsWithNone(): void
{
$target = new class { public mixed $called = null; };
Option::iter(function ($ignored) use ($target) { $target->called = 'uh oh'; }, Option::None());
$this->assertNull($target->called, 'The function should not have been called');
}
#[TestDox('Iter succeeds with Some')]
public function testIterSucceedsWithSome(): void
{
$target = new class { public mixed $called = null; };
Option::iter(function ($it) use ($target) { $target->called = $it; }, Option::Some(33));
$this->assertEquals(33, $target->called, 'The function should have been called with the "Some" value');
}
#[TestDox('Filter succeeds with None')]
public function testFilterSucceedsWithNone(): void
{
$tattle = new class { public bool $called = false; };
$none = Option::None();
$filtered = Option::filter(function ($ignored) use ($tattle)
{
$tattle->called = true;
return true;
}, $none);
$this->assertTrue(Option::isNone($filtered), 'The filtered option should have been "None"');
$this->assertFalse($tattle->called, 'The callable should not have been called');
}
#[TestDox('Filter succeeds with Some when true')]
public function testFilterSucceedsWithSomeWhenTrue(): void
{
$some = Option::Some(12);
$filtered = Option::filter(fn($it) => $it % 2 === 0, $some);
$this->assertTrue(Option::isSome($filtered), 'The filtered option should have been "Some"');
$this->assertEquals(12, $filtered->get(), 'The filtered option value is incorrect');
$this->assertNotSame($some, $filtered, 'There should have been a new option instance returned');
}
#[TestDox('Filter succeeds with Some when false')]
public function testFilterSucceedsWithSomeWhenFalse(): void
{
$some = Option::Some(23);
$filtered = Option::filter(fn($it) => $it % 2 === 0, $some);
$this->assertTrue(Option::isNone($filtered), 'The filtered option should have been "None"');
}
#[TestDox('Is succeeds with None')]
public function testIsSucceedsWithNone(): void
{
$this->assertFalse(Option::is(null, Option::None()), '"None" should always return false');
}
#[TestDox('Is succeeds with Some when strictly equal')]
public function testIsSucceedsWithSomeWhenStrictlyEqual(): void
{
$this->assertTrue(Option::is(3, Option::Some(3)), '"Some" with strict equality should be true');
}
#[TestDox('Is succeeds with Some when strictly unequal')]
public function testIsSucceedsWithSomeWhenStrictlyUnequal(): void
{
$this->assertFalse(Option::is(3, Option::Some('3')), '"Some" with strict equality should be false');
}
#[TestDox('Is succeeds with Some when loosely equal')]
public function testIsSucceedsWithSomeWhenLooselyEqual(): void
{
$this->assertTrue(Option::is(3, Option::Some('3'), strict: false), '"Some" with loose equality should be true');
}
#[TestDox('Is succeeds with Some when loosely unequal')]
public function testIsSucceedsWithSomeWhenLooselyUnequal(): void
{
$this->assertFalse(Option::is(4, Option::Some('3'), strict: false),
'"Some" with loose equality should be false');
}
#[TestDox('Unwrap succeeds with None')]
public function testUnwrapSucceedsWithNone(): void
{
$this->assertNull(Option::unwrap(Option::None()), '"None" should return null');
}
#[TestDox('Unwrap succeeds with Some')]
public function testUnwrapSucceedsWithSome(): void
{
$this->assertEquals('boy howdy', Option::unwrap(Option::Some('boy howdy')), '"Some" should return its value');
}
#[TestDox('Tap succeeds with Some')]
public function testTapSucceedsWithSome(): void
{
$value = '';
$original = Option::Some('testing');
$tapped = Option::tap(
function (Option $it) use (&$value) { $value = Option::isSome($it) ? $it->get() : 'none'; }, $original);
$this->assertEquals('testing', $value, 'The tapped function was not called');
$this->assertSame($original, $tapped, 'The same option should have been returned');
}
#[TestDox('Tap succeeds with None')]
public function testTapSucceedsWithNone(): void
{
$value = '';
$original = Option::None();
$tapped = Option::tap(
function (Option $it) use (&$value) { $value = Option::isSome($it) ? $it->get() : 'none'; }, $original);
$this->assertEquals('none', $value, 'The tapped function was not called');
$this->assertSame($original, $tapped, 'The same option should have been returned');
} }
} }

View File

@ -8,7 +8,6 @@ declare(strict_types=1);
namespace Test; namespace Test;
use BitBadger\InspiredByFSharp\Option;
use BitBadger\InspiredByFSharp\Result; use BitBadger\InspiredByFSharp\Result;
use InvalidArgumentException; use InvalidArgumentException;
use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\Attributes\TestDox;
@ -47,11 +46,156 @@ class ResultTest extends TestCase
Result::OK('yeah')->getError(); Result::OK('yeah')->getError();
} }
#[TestDox('IsOK succeeds for OK result')]
public function testIsOKSucceedsForOKResult(): void
{
$result = Result::OK('ok');
$this->assertTrue($result->isOK(), 'The check for "OK" should have returned true');
}
#[TestDox('IsOK succeeds for Error result')]
public function testIsOKSucceedsForErrorResult(): void
{
$result = Result::Error('error');
$this->assertFalse($result->isOK(), 'The check for "OK" should have returned false');
}
#[TestDox('IsError succeeds for Error result')]
public function testIsErrorSucceedsForErrorResult(): void
{
$result = Result::Error('not ok');
$this->assertTrue($result->isError(), 'The check for "Error" should have returned true');
}
#[TestDox('IsError succeeds for OK result')]
public function testIsErrorSucceedsForOKResult(): void
{
$result = Result::OK('fine');
$this->assertFalse($result->isError(), 'The check for "Error" should have returned false');
}
#[TestDox('Map succeeds for OK result')]
public function testMapSucceedsForOKResult(): void
{
$ok = Result::OK('yard');
$mapped = $ok->map(fn($it) => strrev($it));
$this->assertTrue($mapped->isOK(), 'The mapped result should be "OK"');
$this->assertEquals('dray', $mapped->getOK(), 'The mapping function was not called correctly');
}
#[TestDox('Map fails for OK result when mapping is null')]
public function testMapFailsForOKResultWhenMappingIsNull(): void
{
$this->expectException(InvalidArgumentException::class);
Result::OK('not null')->map(fn($it) => null);
}
#[TestDox('Map succeeds for Error result')]
public function testMapSucceedsForErrorResult(): void
{
$tattle = new class { public bool $called = false; };
$error = Result::Error('nope');
$mapped = $error->map(function () use ($tattle)
{
$tattle->called = true;
return 'hello';
});
$this->assertTrue($mapped->isError(), 'The mapped result should be "Error"');
$this->assertFalse($tattle->called, 'The mapping function should not have been called');
$this->assertSame($error, $mapped, 'The same "Error" instance should have been returned');
}
#[TestDox('MapError succeeds for OK result')]
public function testMapErrorSucceedsForOKResult(): void
{
$tattle = new class { public bool $called = false; };
$ok = Result::OK('sure');
$mapped = $ok->mapError(function () use ($tattle)
{
$tattle->called = true;
return 'hello';
});
$this->assertTrue($mapped->isOK(), 'The mapped result should be "OK"');
$this->assertFalse($tattle->called, 'The mapping function should not have been called');
$this->assertSame($ok, $mapped, 'The same "OK" instance should have been returned');
}
#[TestDox('MapError succeeds for Error result')]
public function testMapErrorSucceedsForErrorResult(): void
{
$error = Result::Error('taco');
$mapped = $error->mapError(fn($it) => str_repeat('*', strlen($it)));
$this->assertTrue($mapped->isError(), 'The mapped result should be "Error"');
$this->assertEquals('****', $mapped->getError(), 'The mapping function was not called correctly');
}
#[TestDox('MapError fails for Error result when mapping is null')]
public function testMapErrorFailsForErrorResultWhenMappingIsNull(): void
{
$this->expectException(InvalidArgumentException::class);
Result::Error('pizza')->mapError(fn($it) => null);
}
#[TestDox('Iter succeeds for OK result')]
public function testIterSucceedsForOKResult(): void
{
$target = new class { public mixed $called = null; };
Result::OK(77)->iter(function ($it) use ($target) { $target->called = $it; });
$this->assertEquals(77, $target->called, 'The function should have been called with the "OK" value');
}
#[TestDox('Iter succeeds for Error result')]
public function testIterSucceedsForErrorResult(): void
{
$target = new class { public mixed $called = null; };
Result::Error('')->iter(function () use ($target) { $target->called = 'uh oh'; });
$this->assertNull($target->called, 'The function should not have been called');
}
#[TestDox('ToOption succeeds for OK result')]
public function testToOptionSucceedsForOKResult()
{
$value = Result::OK(99)->toOption();
$this->assertTrue($value->isSome(), 'An "OK" result should map to a "Some" option');
$this->assertEquals(99, $value->get(), 'The value is not correct');
}
#[TestDox('ToOption succeeds for Error result')]
public function testToOptionSucceedsForErrorResult()
{
$value = Result::Error('file not found')->toOption();
$this->assertTrue($value->isNone(), 'An "Error" result should map to a "None" option');
}
#[TestDox('Tap succeeds for OK result')]
public function testTapSucceedsForOKResult(): void
{
$value = '';
$original = Result::OK('working');
$tapped = $original->tap(function (Result $it) use (&$value) {
$value = $it->isOK() ? 'OK: ' . $it->getOK() : 'Error: ' . $it->getError();
});
$this->assertEquals('OK: working', $value, 'The tapped function was not called');
$this->assertSame($original, $tapped, 'The same result should have been returned');
}
#[TestDox('Tap succeeds for Error result')]
public function testTapSucceedsForErrorResult(): void
{
$value = '';
$original = Result::Error('failed');
$tapped = $original->tap(function (Result $it) use (&$value) {
$value = $it->isOK() ? 'OK: ' . $it->getOK() : 'Error: ' . $it->getError();
});
$this->assertEquals('Error: failed', $value, 'The tapped function was not called');
$this->assertSame($original, $tapped, 'The same result should have been returned');
}
#[TestDox('OK succeeds for non null result')] #[TestDox('OK succeeds for non null result')]
public function testOKSucceedsForNonNullResult(): void public function testOKSucceedsForNonNullResult(): void
{ {
$result = Result::OK('something'); $result = Result::OK('something');
$this->assertTrue(Result::isOK($result), 'The result should have been "OK"'); $this->assertTrue($result->isOK(), 'The result should have been "OK"');
$this->assertEquals('something', $result->getOK(), 'The "OK" value was incorrect'); $this->assertEquals('something', $result->getOK(), 'The "OK" value was incorrect');
} }
@ -66,7 +210,7 @@ class ResultTest extends TestCase
public function testErrorSucceedsForNonNullResult(): void public function testErrorSucceedsForNonNullResult(): void
{ {
$result = Result::Error('sad trombone'); $result = Result::Error('sad trombone');
$this->assertTrue(Result::isError($result), 'The result should have been "Error"'); $this->assertTrue($result->isError(), 'The result should have been "Error"');
$this->assertEquals('sad trombone', $result->getError(), 'The "Error" value was incorrect'); $this->assertEquals('sad trombone', $result->getError(), 'The "Error" value was incorrect');
} }
@ -76,149 +220,4 @@ class ResultTest extends TestCase
$this->expectException(InvalidArgumentException::class); $this->expectException(InvalidArgumentException::class);
Result::Error(null); Result::Error(null);
} }
#[TestDox('IsOK succeeds for OK result')]
public function testIsOKSucceedsForOKResult(): void
{
$result = Result::OK('ok');
$this->assertTrue(Result::isOK($result), 'The check for "OK" should have returned true');
}
#[TestDox('IsOK succeeds for Error result')]
public function testIsOKSucceedsForErrorResult(): void
{
$result = Result::Error('error');
$this->assertFalse(Result::isOK($result), 'The check for "OK" should have returned false');
}
#[TestDox('IsError succeeds for Error result')]
public function testIsErrorSucceedsForErrorResult(): void
{
$result = Result::Error('not ok');
$this->assertTrue(Result::isError($result), 'The check for "Error" should have returned true');
}
#[TestDox('IsError succeeds for OK result')]
public function testIsErrorSucceedsForOKResult(): void
{
$result = Result::OK('fine');
$this->assertFalse(Result::isError($result), 'The check for "Error" should have returned false');
}
#[TestDox('Map succeeds for OK result')]
public function testMapSucceedsForOKResult(): void
{
$ok = Result::OK('yard');
$mapped = Result::map(fn($it) => strrev($it), $ok);
$this->assertTrue(Result::isOK($mapped), 'The mapped result should be "OK"');
$this->assertEquals('dray', $mapped->getOK(), 'The mapping function was not called correctly');
}
#[TestDox('Map fails for OK result when mapping is null')]
public function testMapFailsForOKResultWhenMappingIsNull(): void
{
$this->expectException(InvalidArgumentException::class);
Result::map(fn($it) => null, Result::OK('not null'));
}
#[TestDox('Map succeeds for Error result')]
public function testMapSucceedsForErrorResult(): void
{
$tattle = new class { public bool $called = false; };
$error = Result::Error('nope');
$mapped = Result::map(function ($ignored) use ($tattle)
{
$tattle->called = true;
return 'hello';
}, $error);
$this->assertTrue(Result::isError($mapped), 'The mapped result should be "Error"');
$this->assertFalse($tattle->called, 'The mapping function should not have been called');
$this->assertNotSame($error, $mapped, 'There should have been a new "Error" instance returned');
}
#[TestDox('MapError succeeds for OK result')]
public function testMapErrorSucceedsForOKResult(): void
{
$tattle = new class { public bool $called = false; };
$ok = Result::OK('sure');
$mapped = Result::mapError(function ($ignored) use ($tattle)
{
$tattle->called = true;
return 'hello';
}, $ok);
$this->assertTrue(Result::isOK($mapped), 'The mapped result should be "OK"');
$this->assertFalse($tattle->called, 'The mapping function should not have been called');
$this->assertNotSame($ok, $mapped, 'There should have been a new "OK" instance returned');
}
#[TestDox('MapError succeeds for Error result')]
public function testMapErrorSucceedsForErrorResult(): void
{
$error = Result::Error('taco');
$mapped = Result::mapError(fn($it) => str_repeat('*', strlen($it)), $error);
$this->assertTrue(Result::isError($mapped), 'The mapped result should be "Error"');
$this->assertEquals('****', $mapped->getError(), 'The mapping function was not called correctly');
}
#[TestDox('MapError fails for Error result when mapping is null')]
public function testMapErrorFailsForErrorResultWhenMappingIsNull(): void
{
$this->expectException(InvalidArgumentException::class);
Result::mapError(fn($it) => null, Result::Error('pizza'));
}
#[TestDox('Iter succeeds for OK result')]
public function testIterSucceedsForOKResult(): void
{
$target = new class { public mixed $called = null; };
Result::iter(function ($it) use ($target) { $target->called = $it; }, Result::OK(77));
$this->assertEquals(77, $target->called, 'The function should have been called with the "OK" value');
}
#[TestDox('Iter succeeds for Error result')]
public function testIterSucceedsForErrorResult(): void
{
$target = new class { public mixed $called = null; };
Result::iter(function ($ignored) use ($target) { $target->called = 'uh oh'; }, Result::Error(''));
$this->assertNull($target->called, 'The function should not have been called');
}
#[TestDox('ToOption succeeds for OK result')]
public function testToOptionSucceedsForOKResult()
{
$value = Result::toOption(Result::OK(99));
$this->assertTrue(Option::isSome($value), 'An "OK" result should map to a "Some" option');
$this->assertEquals(99, $value->get(), 'The value is not correct');
}
#[TestDox('ToOption succeeds for Error result')]
public function testToOptionSucceedsForErrorResult()
{
$value = Result::toOption(Result::Error('file not found'));
$this->assertTrue(Option::isNone($value), 'An "Error" result should map to a "None" option');
}
#[TestDox('Tap succeeds for OK result')]
public function testTapSucceedsForOKResult(): void
{
$value = '';
$original = Result::OK('working');
$tapped = Result::tap(function (Result $it) use (&$value) {
$value = Result::isOK($it) ? 'OK: ' . $it->getOK() : 'Error: ' . $it->getError();
}, $original);
$this->assertEquals('OK: working', $value, 'The tapped function was not called');
$this->assertSame($original, $tapped, 'The same result should have been returned');
}
#[TestDox('Tap succeeds for Error result')]
public function testTapSucceedsForErrorResult(): void
{
$value = '';
$original = Result::Error('failed');
$tapped = Result::tap(function (Result $it) use (&$value) {
$value = Result::isOK($it) ? 'OK: ' . $it->getOK() : 'Error: ' . $it->getError();
}, $original);
$this->assertEquals('Error: failed', $value, 'The tapped function was not called');
$this->assertSame($original, $tapped, 'The same result should have been returned');
}
} }