diff --git a/.gitignore b/.gitignore index c9dec9a..b4c96e1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ /vendor/ -.php_cs.cache +.cache/ composer.lock diff --git a/.php_cs b/.php-cs-fixer.dist.php similarity index 85% rename from .php_cs rename to .php-cs-fixer.dist.php index 7187377..13ce596 100644 --- a/.php_cs +++ b/.php-cs-fixer.dist.php @@ -1,9 +1,10 @@ exclude('vendor') ->in(__DIR__); -return PhpCsFixer\Config::create() +return (new PhpCsFixer\Config()) ->setUsingCache(true) + ->setCacheFile('.cache/php-cs-fixer/php-cs-fixer.cache') ->setRules([ '@PSR2' => true, 'encoding' => true, @@ -14,7 +15,7 @@ 'indentation_type' => true, 'blank_line_after_namespace' => true, 'line_ending' => true, - 'lowercase_constants' => true, + 'constant_case' => ['case' => 'lower'], 'lowercase_keywords' => true, 'no_closing_tag' => true, 'single_line_after_imports' => true, @@ -23,7 +24,7 @@ 'whitespace_after_comma_in_array' => true, 'blank_line_after_opening_tag' => true, 'no_empty_statement' => true, - 'no_extra_consecutive_blank_lines' => true, + 'no_extra_blank_lines' => true, 'function_typehint_space' => true, 'no_leading_namespace_whitespace' => true, 'no_blank_lines_after_class_opening' => true, @@ -31,8 +32,7 @@ 'phpdoc_scalar' => true, 'phpdoc_types' => true, 'no_leading_import_slash' => true, - 'no_extra_consecutive_blank_lines' => ['use'], - 'blank_line_before_return' => true, + 'blank_line_before_statement' => ['statements' => ['return']], 'self_accessor' => false, 'no_short_bool_cast' => true, 'no_trailing_comma_in_singleline_array' => true, diff --git a/composer.json b/composer.json index 4ca93a6..7e8cb5e 100644 --- a/composer.json +++ b/composer.json @@ -13,9 +13,11 @@ "php": ">=7.1" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^2", - "phpunit/phpunit": "^5", - "giorgiosironi/eris": "^0.9" + "friendsofphp/php-cs-fixer": "^3.65", + "phpunit/phpunit": "^10.0", + "giorgiosironi/eris": "^1.0", + "phpstan/phpstan": "^2.0", + "vimeo/psalm": "^5.26" }, "prefer-stable": true, "autoload": { @@ -29,6 +31,8 @@ "scripts": { "test": "phpunit --no-coverage", "fix-code": "php-cs-fixer fix --allow-risky=yes", - "check-code": "php-cs-fixer fix --verbose --diff --dry-run --allow-risky=yes" + "check-code": "php-cs-fixer fix --verbose --diff --dry-run --allow-risky=yes", + "phpstan": "phpstan analyse", + "psalm": "psalm" } } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..34a69d8 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,31 @@ +parameters: + ignoreErrors: + - + message: '#^Unable to resolve the template type b in call to function FunctionalPHP\\FantasyLand\\applicator$#' + identifier: argument.templateType + count: 1 + path: src/FantasyLand/Helpful/ApplicativeLaws.php + + - + message: '#^Variable \$ap in PHPDoc tag @var does not match assigned variable \$applicatorX\.$#' + identifier: varTag.differentVariable + count: 1 + path: src/FantasyLand/Helpful/ApplicativeLaws.php + + - + message: '#^Parameter \#1 \$f of function FunctionalPHP\\FantasyLand\\compose expects callable\(FunctionalPHP\\FantasyLand\\Functor\\)\: FunctionalPHP\\FantasyLand\\Functor\, \(callable\(FunctionalPHP\\FantasyLand\\Functor\\)\: FunctionalPHP\\FantasyLand\\Functor\\)\|FunctionalPHP\\FantasyLand\\Functor\ given\.$#' + identifier: argument.type + count: 1 + path: src/FantasyLand/Helpful/FunctorLaws.php + + - + message: '#^Parameter \#2 \$g of function FunctionalPHP\\FantasyLand\\compose expects callable\(FunctionalPHP\\FantasyLand\\Functor\\)\: FunctionalPHP\\FantasyLand\\Functor\, \(callable\(FunctionalPHP\\FantasyLand\\Functor\\)\: FunctionalPHP\\FantasyLand\\Functor\\)\|FunctionalPHP\\FantasyLand\\Functor\ given\.$#' + identifier: argument.type + count: 1 + path: src/FantasyLand/Helpful/FunctorLaws.php + + - + message: '#^Parameter \#4 \$f of static method FunctionalPHP\\FantasyLand\\Helpful\\MonadLaws\:\:test\(\) expects callable\(mixed\)\: FunctionalPHP\\FantasyLand\\Monad\, callable\(a\)\: FunctionalPHP\\FantasyLand\\Useful\\Identity\ given\.$#' + identifier: argument.type + count: 1 + path: src/FantasyLand/Helpful/Tests/MonadLawsTest.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..babf8c0 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,8 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: 10 + tmpDir: .cache/phpstan + paths: + - src diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f1ee651..3103287 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,14 +1,17 @@ - - - - ./src/FantasyLand/Helpful/Tests - - - - - ./src/ - - + + + + ./src/FantasyLand/Helpful/Tests + + + + + ./src/ + + diff --git a/psalm.baseline.xml b/psalm.baseline.xml new file mode 100644 index 0000000..874eb91 --- /dev/null +++ b/psalm.baseline.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..f930a34 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/FantasyLand/Applicative.php b/src/FantasyLand/Applicative.php index fdd2c1e..61ca86b 100644 --- a/src/FantasyLand/Applicative.php +++ b/src/FantasyLand/Applicative.php @@ -6,8 +6,8 @@ /** * @template a - * @template-extends Apply - * @template-extends Pointed + * @extends Apply + * @extends Pointed */ interface Applicative extends Apply, diff --git a/src/FantasyLand/Apply.php b/src/FantasyLand/Apply.php index 9469935..8e682b2 100644 --- a/src/FantasyLand/Apply.php +++ b/src/FantasyLand/Apply.php @@ -6,7 +6,7 @@ /** * @template a - * @template-extends Functor + * @extends Functor */ interface Apply extends Functor { diff --git a/src/FantasyLand/Chain.php b/src/FantasyLand/Chain.php index a3d3527..fa9c65d 100644 --- a/src/FantasyLand/Chain.php +++ b/src/FantasyLand/Chain.php @@ -6,7 +6,7 @@ /** * @template a - * @template-extends Apply + * @extends Apply */ interface Chain extends Apply { @@ -14,9 +14,8 @@ interface Chain extends Apply * bind :: Monad m => (a -> m b) -> m b * * @template b - * @template f of callable(a): Chain * - * @param f $function + * @param callable(a): Chain $function * * @return Chain */ diff --git a/src/FantasyLand/Foldable.php b/src/FantasyLand/Foldable.php index 323a457..891fad7 100644 --- a/src/FantasyLand/Foldable.php +++ b/src/FantasyLand/Foldable.php @@ -13,10 +13,9 @@ interface Foldable * reduce :: (b -> a -> b) -> b -> b * * @template b - * @template f of callable(b, a): b * - * @param f $function Binary function ($accumulator, $value) - * @param b $accumulator Value to witch reduce + * @param callable(b, a): b $function Binary function ($accumulator, $value) + * @param b $accumulator Value to witch reduce * * @return b Same type as $accumulator */ diff --git a/src/FantasyLand/Functor.php b/src/FantasyLand/Functor.php index a8efd6b..277f0c1 100644 --- a/src/FantasyLand/Functor.php +++ b/src/FantasyLand/Functor.php @@ -13,11 +13,10 @@ interface Functor * map :: Functor f => (a -> b) -> f b * * @template b - * @template f of callable(a): b * - * @param f $function + * @param callable(a): b $function * * @return Functor */ - public function map(callable $function): Functor; + public function map(callable $function); } diff --git a/src/FantasyLand/Helpful/ApplicativeLaws.php b/src/FantasyLand/Helpful/ApplicativeLaws.php index 23a3890..e8ef704 100644 --- a/src/FantasyLand/Helpful/ApplicativeLaws.php +++ b/src/FantasyLand/Helpful/ApplicativeLaws.php @@ -15,23 +15,28 @@ class ApplicativeLaws /** * Generic test to verify if a type obey the applicative laws. * - * @param callable $assertEqual Asserting function (Applicative $a1, Applicative $a2, $message) - * @param callable $pure Applicative "constructor" - * @param Applicative $u Applicative f => f (a -> b) - * @param Applicative $v Applicative f => f (a -> b) - * @param Applicative $w Applicative f => f (a -> b) - * @param callable $f (a -> b) - * @param mixed $x Value to put into a applicative + * @template a + * @template b + * @template c + * @template d + * + * @param callable $assertEqual Asserting function (Applicative $a1, Applicative $a2, $message) + * @param a $x Value to put into a applicative + * @param callable(mixed): Applicative $pure Applicative "constructor" + * @param Applicative $u Applicative f => f (a -> b) + * @param Applicative $v Applicative f => f (a -> b) + * @param Applicative $w Applicative f => f (a -> b) + * @param callable(a): b $f (a -> b) */ public static function test( callable $assertEqual, + $x, callable $pure, Applicative $u, Applicative $v, Applicative $w, - callable $f, - $x - ) { + callable $f + ): void { // identity: pure id <*> v = v $assertEqual( $pure(identity)->ap($v), @@ -47,9 +52,11 @@ public static function test( ); // interchange: u <*> pure x = pure ($ x) <*> u + /** @var callable(callable(a): b): b $ap */ + $applicatorX = applicator($x); $assertEqual( $u->ap($pure($x)), - $pure(applicator($x))->ap($u), + $pure($applicatorX)->ap($u), 'interchange' ); diff --git a/src/FantasyLand/Helpful/FunctorLaws.php b/src/FantasyLand/Helpful/FunctorLaws.php index ea6bcb8..27ff440 100644 --- a/src/FantasyLand/Helpful/FunctorLaws.php +++ b/src/FantasyLand/Helpful/FunctorLaws.php @@ -5,7 +5,6 @@ namespace FunctionalPHP\FantasyLand\Helpful; use FunctionalPHP\FantasyLand\Functor; -use const FunctionalPHP\FantasyLand\identity; use function FunctionalPHP\FantasyLand\compose; use function FunctionalPHP\FantasyLand\map; @@ -14,20 +13,33 @@ class FunctorLaws /** * Generic test to verify if a type obey the functor laws. * - * @param callable $assertEqual Asserting function (Functor $f1, Functor $f2, $message) - * @param callable $f (a -> b) - * @param callable $g (a -> b) - * @param Functor $x f a + * @template a + * @template b + * @template c + * + * @param callable $assertEqual Asserting function (Functor $f1, Functor $f2, $message) + * @param callable(b): c $f (a -> b) + * @param callable(a): b $g (a -> b) + * @param Functor $x f a */ public static function test( callable $assertEqual, callable $f, callable $g, Functor $x - ) { + ): void { + $identity = + /** + * @param a $x + * @return a + */ + static function ($x) { + return $x; + }; + // identity: fmap id == id $assertEqual( - map(identity, $x), + map($identity, $x), $x, 'identity' ); diff --git a/src/FantasyLand/Helpful/MonadLaws.php b/src/FantasyLand/Helpful/MonadLaws.php index 0fa4cce..86585b3 100644 --- a/src/FantasyLand/Helpful/MonadLaws.php +++ b/src/FantasyLand/Helpful/MonadLaws.php @@ -4,6 +4,8 @@ namespace FunctionalPHP\FantasyLand\Helpful; +use FunctionalPHP\FantasyLand\Monad; + use function FunctionalPHP\FantasyLand\bind; class MonadLaws @@ -11,34 +13,55 @@ class MonadLaws /** * Generic test to verify if a type obey the monad laws. * - * @param callable $assertEqual Asserting function (Monad $m1, Monad $m2, $message) - * @param callable $return Monad "constructor" - * @param callable $f Monadic function - * @param callable $g Monadic function - * @param mixed $x Value to put into a monad + * @template a + * @template b + * @template c + * + * @param callable $assertEqual Asserting function (Monad $m1, Monad $m2, $message) + * @param a $x Value to put into a monad + * @param callable(a): Monad $return Monad "constructor" + * @param callable(a): Monad $f Monadic function + * @param callable(b): Monad $g Monadic function */ public static function test( callable $assertEqual, + $x, callable $return, callable $f, - callable $g, - $x - ) { + callable $g + ): void { // Make reading bellow tests easier $m = $return($x); // left identity: (return x) >>= f ≡ f x - $assertEqual(bind($f, $return($x)), $f($x), 'left identity'); + $assertEqual( + bind($f, $return($x)), + $f($x), + 'left identity' + ); // right identity: m >>= return ≡ m $assertEqual(bind($return, $m), $m, 'right identity'); // associativity: (m >>= f) >>= g ≡ m >>= ( \x -> (f x >>= g) ) - $assertEqual( - bind($g, bind($f, $m)), - bind(function ($x) use ($f, $g) { + + /** @var Monad $boundF */ + $boundF = bind($f, $m); + /** @var Monad $boundG */ + $boundG = bind($g, $boundF); + $boundFoG = + /** + * @param a $x + * @return Monad + */ + function ($x) use ($f, $g) { + /** @var Monad */ return bind($g, $f($x)); - }, $m), + }; + + $assertEqual( + $boundG, + bind($boundFoG, $m), 'associativity' ); } diff --git a/src/FantasyLand/Helpful/MonoidLaws.php b/src/FantasyLand/Helpful/MonoidLaws.php index a6f140d..5ad09ea 100644 --- a/src/FantasyLand/Helpful/MonoidLaws.php +++ b/src/FantasyLand/Helpful/MonoidLaws.php @@ -13,17 +13,19 @@ class MonoidLaws /** * Generic test to verify if a type obey the monodic laws. * - * @param callable $assertEqual Asserting function (Monoid $m1, Monoid $m2, $message) - * @param Monoid $x - * @param Monoid $y - * @param Monoid $z + * @template a + * + * @param callable $assertEqual Asserting function (Monoid $m1, Monoid $m2, $message) + * @param Monoid $x + * @param Monoid $y + * @param Monoid $z */ public static function test( callable $assertEqual, Monoid $x, Monoid $y, Monoid $z - ) { + ): void { $assertEqual( concat($x, emptyy($x)), $x, diff --git a/src/FantasyLand/Helpful/SetoidLaws.php b/src/FantasyLand/Helpful/SetoidLaws.php index 29d1c73..bbe1dd4 100644 --- a/src/FantasyLand/Helpful/SetoidLaws.php +++ b/src/FantasyLand/Helpful/SetoidLaws.php @@ -10,17 +10,21 @@ class SetoidLaws { /** - * @param callable $assertEqual - * @param Setoid $a - * @param Setoid $b - * @param Setoid $c + * @template a + * @template b + * @template c + * + * @param callable $assertEqual + * @param Setoid $a + * @param Setoid $b + * @param Setoid $c */ public static function test( callable $assertEqual, Setoid $a, Setoid $b, Setoid $c - ) { + ): void { $assertEqual( equal($a, $a), true, diff --git a/src/FantasyLand/Helpful/Tests/ApplicativeLawsTest.php b/src/FantasyLand/Helpful/Tests/ApplicativeLawsTest.php index 82bc2cc..6768bde 100644 --- a/src/FantasyLand/Helpful/Tests/ApplicativeLawsTest.php +++ b/src/FantasyLand/Helpful/Tests/ApplicativeLawsTest.php @@ -8,46 +8,71 @@ use FunctionalPHP\FantasyLand\Helpful\ApplicativeLaws; use FunctionalPHP\FantasyLand\Useful\Identity; -class ApplicativeLawsTest extends \PHPUnit_Framework_TestCase +class ApplicativeLawsTest extends \PHPUnit\Framework\TestCase { /** * @dataProvider provideApplicativeTestData + * + * @template a + * @template b + * @template c + * @template d + * + * @param a $x + * @param Applicative $u + * @param Applicative $v + * @param Applicative $w + * @param callable(a): b $f */ public function test_it_should_obey_applicative_laws( + $x, Applicative $u, Applicative $v, Applicative $w, - callable $f, - $x - ) { + callable $f + ): void { + // This is a workaround to allow static analysis to infer the types of + // the `pure` function. + $pure = + /** + * @param a $x + * @return Identity + */ + function ($x) { + return Identity::of($x); + }; + ApplicativeLaws::test( [$this, 'assertEquals'], - Identity::of, + $x, + $pure, $u, $v, $w, - $f, - $x + $f ); } - public function provideApplicativeTestData() + /** + * @return array{default: array} + */ + public static function provideApplicativeTestData() { return [ 'default' => [ - '$u' => Identity::of(function () { + 'x' => 33, + 'u' => Identity::of(function () { return 1; }), - '$v' => Identity::of(function () { + 'v' => Identity::of(function () { return 5; }), - '$w' => Identity::of(function () { + 'w' => Identity::of(function () { return 7; }), - '$f' => function ($x) { + 'f' => function (int $x) { return $x + 400; - }, - '$x' => 33 + } ], ]; } diff --git a/src/FantasyLand/Helpful/Tests/FunctorLawsTest.php b/src/FantasyLand/Helpful/Tests/FunctorLawsTest.php index 5264526..be17649 100644 --- a/src/FantasyLand/Helpful/Tests/FunctorLawsTest.php +++ b/src/FantasyLand/Helpful/Tests/FunctorLawsTest.php @@ -8,16 +8,24 @@ use FunctionalPHP\FantasyLand\Helpful\FunctorLaws; use FunctionalPHP\FantasyLand\Useful\Identity; -class FunctorLawsTest extends \PHPUnit_Framework_TestCase +class FunctorLawsTest extends \PHPUnit\Framework\TestCase { /** * @dataProvider provideFunctorTestData + * + * @template a + * @template b + * @template c + * + * @param callable(b): c $f + * @param callable(a): b $g + * @param Functor $x */ public function test_it_should_obey_functor_laws( callable $f, callable $g, Functor $x - ) { + ): void { FunctorLaws::test( [$this, 'assertEquals'], $f, @@ -26,17 +34,20 @@ public function test_it_should_obey_functor_laws( ); } - public function provideFunctorTestData() + /** + * @return array{Identity: array} + */ + public static function provideFunctorTestData(): array { return [ 'Identity' => [ - '$f' => function ($x) { + 'f' => function (int $x) { return $x + 1; }, - '$g' => function ($x) { + 'g' => function (int $x) { return $x + 5; }, - '$x' => Identity::of(123), + 'x' => Identity::of(123), ], ]; } diff --git a/src/FantasyLand/Helpful/Tests/MonadLawsTest.php b/src/FantasyLand/Helpful/Tests/MonadLawsTest.php index 2137284..c4e13fc 100644 --- a/src/FantasyLand/Helpful/Tests/MonadLawsTest.php +++ b/src/FantasyLand/Helpful/Tests/MonadLawsTest.php @@ -7,36 +7,58 @@ use FunctionalPHP\FantasyLand\Helpful\MonadLaws; use FunctionalPHP\FantasyLand\Useful\Identity; -class MonadLawsTest extends \PHPUnit_Framework_TestCase +class MonadLawsTest extends \PHPUnit\Framework\TestCase { /** * @dataProvider provideData + * + * @template a + * @template b + * @template c + * + * @param a $x + * @param callable(a): Identity $f + * @param callable(b): Identity $g */ - public function test_if_identity_monad_obeys_the_laws($f, $g, $x) + public function test_if_identity_monad_obeys_the_laws($x, $f, $g): void { + // This is a workaround to allow static analysis to infer the types of + // the `pure` function. + $pure = + /** + * @param a $x + * @return Identity + */ + function ($x) { + return Identity::of($x); + }; + MonadLaws::test( [$this, 'assertEquals'], - Identity::of, + $x, + $pure, $f, - $g, - $x + $g ); } - public function provideData() + /** + * @return array{Identity: array} + */ + public static function provideData(): array { - $addOne = function ($x) { + $addOne = function (int $x): Identity { return Identity::of($x + 1); }; - $addTwo = function ($x) { + $addTwo = function (int $x): Identity { return Identity::of($x + 2); }; return [ 'Identity' => [ - '$f' => $addOne, - '$g' => $addTwo, - '$x' => 10, + 'x' => 10, + 'f' => $addOne, + 'g' => $addTwo, ], ]; } diff --git a/src/FantasyLand/Helpful/Tests/StringMonoidLawsTest.php b/src/FantasyLand/Helpful/Tests/StringMonoidLawsTest.php index 806fea0..e1c9d57 100644 --- a/src/FantasyLand/Helpful/Tests/StringMonoidLawsTest.php +++ b/src/FantasyLand/Helpful/Tests/StringMonoidLawsTest.php @@ -4,12 +4,19 @@ namespace FunctionalPHP\FantasyLand\Helpful\Tests; -use Eris\Generator; +use Eris\Generators; use Eris\TestTrait; use FunctionalPHP\FantasyLand\Helpful\MonoidLaws; use FunctionalPHP\FantasyLand\Monoid; use FunctionalPHP\FantasyLand\Semigroup; +use function FunctionalPHP\FantasyLand\concat; +use function FunctionalPHP\FantasyLand\emptyy; +/** + * This class is a monoid because it obeys the monoid laws. + * + * @implements Monoid + */ class StringMonoid implements Monoid { /** @@ -24,6 +31,7 @@ public function __construct(string $value) /** * @inheritdoc + * @return StringMonoid */ public static function mempty() { @@ -32,6 +40,8 @@ public static function mempty() /** * @inheritdoc + * @param StringMonoid $value + * @return StringMonoid */ public function concat(Semigroup $value): Semigroup { @@ -39,6 +49,14 @@ public function concat(Semigroup $value): Semigroup } } +/** + * This class is not a monoid because it does not obey the monoid laws, + * despite implementing the Monoid interface. In particular, it does not obay + * the right identity and left identity laws, due to the way the `mempty` method + * is implemented. + * + * @implements Monoid + */ class NotAStringMonoid implements Monoid { /** @@ -53,6 +71,7 @@ public function __construct(string $value) /** * @inheritdoc + * @return NotAStringMonoid */ public static function mempty() { @@ -61,6 +80,8 @@ public static function mempty() /** * @inheritdoc + * @param NotAStringMonoid $value + * @return NotAStringMonoid */ public function concat(Semigroup $value): Semigroup { @@ -68,16 +89,16 @@ public function concat(Semigroup $value): Semigroup } } -class StringMonoidLawsTest extends \PHPUnit_Framework_TestCase +class StringMonoidLawsTest extends \PHPUnit\Framework\TestCase { use TestTrait; - public function test_it_should_obay_monoid_laws() + public function test_it_should_obay_monoid_laws(): void { $this->forAll( - Generator\char(), - Generator\string(), - Generator\names() + Generators::char(), + Generators::string(), + Generators::names() )->then(function (string $a, string $b, string $c) { MonoidLaws::test( [$this, 'assertEquals'], @@ -88,21 +109,33 @@ public function test_it_should_obay_monoid_laws() }); } - /** - * @expectedException \DomainException - */ - public function test_it_should_fail_monoid_laws() + public function test_it_should_fail_monoid_laws(): void { $this->forAll( - Generator\char(), - Generator\string(), - Generator\names() + Generators::char(), + Generators::string(), + Generators::names() )->then(function (string $a, string $b, string $c) { - MonoidLaws::test( - [$this, 'assertEquals'], - new NotAStringMonoid($a), - new NotAStringMonoid($b), - new NotAStringMonoid($c) + $x = new NotAStringMonoid($a); + $y = new NotAStringMonoid($b); + $z = new NotAStringMonoid($c); + + $this->assertNotEquals( + concat($x, emptyy($x)), + $x, + 'Right identity' + ); + + $this->assertNotEquals( + concat(emptyy($x), $x), + $x, + 'Left identity' + ); + + $this->assertEquals( + concat($x, concat($y, $z)), + concat(concat($x, $y), $z), + 'Associativity' ); }); } diff --git a/src/FantasyLand/Helpful/Tests/StringSetoidLawsTest.php b/src/FantasyLand/Helpful/Tests/StringSetoidLawsTest.php index ff0ebc6..cb3ffc4 100644 --- a/src/FantasyLand/Helpful/Tests/StringSetoidLawsTest.php +++ b/src/FantasyLand/Helpful/Tests/StringSetoidLawsTest.php @@ -4,11 +4,14 @@ namespace FunctionalPHP\FantasyLand\Helpful\Tests; -use Eris\Generator; +use Eris\Generators; use Eris\TestTrait; use FunctionalPHP\FantasyLand\Helpful\SetoidLaws; use FunctionalPHP\FantasyLand\Setoid; +/** + * @implements Setoid + */ class StringSetoid implements Setoid { /** @@ -32,16 +35,16 @@ public function equals($other): bool } } -class StringSetoidLawsTest extends \PHPUnit_Framework_TestCase +class StringSetoidLawsTest extends \PHPUnit\Framework\TestCase { use TestTrait; - public function test_it_should_obay_setoid_laws() + public function test_it_should_obay_setoid_laws(): void { $this->forAll( - Generator\char(), - Generator\string(), - Generator\names() + Generators::char(), + Generators::string(), + Generators::names() )->then(function (string $a, string $b, string $c) { SetoidLaws::test( [$this, 'assertEquals'], diff --git a/src/FantasyLand/Monad.php b/src/FantasyLand/Monad.php index c245c35..b352345 100644 --- a/src/FantasyLand/Monad.php +++ b/src/FantasyLand/Monad.php @@ -6,8 +6,8 @@ /** * @template a - * @template-extends Applicative - * @template-extends Chain + * @extends Applicative + * @extends Chain */ interface Monad extends Applicative, diff --git a/src/FantasyLand/Monoid.php b/src/FantasyLand/Monoid.php index 43f76de..d281116 100644 --- a/src/FantasyLand/Monoid.php +++ b/src/FantasyLand/Monoid.php @@ -6,7 +6,7 @@ /** * @template a - * @template-extends Semigroup + * @extends Semigroup */ interface Monoid extends Semigroup { diff --git a/src/FantasyLand/Pointed.php b/src/FantasyLand/Pointed.php index 97b1032..a9f80e2 100644 --- a/src/FantasyLand/Pointed.php +++ b/src/FantasyLand/Pointed.php @@ -12,9 +12,11 @@ interface Pointed /** * Put $value in default minimal context. * - * @param a $value + * @template A * - * @return Pointed + * @param A $value + * + * @return Pointed */ public static function of($value); } diff --git a/src/FantasyLand/Setoid.php b/src/FantasyLand/Setoid.php index 4a37c72..7d87cb2 100644 --- a/src/FantasyLand/Setoid.php +++ b/src/FantasyLand/Setoid.php @@ -12,7 +12,7 @@ interface Setoid /** * @template b * - * @param Setoid|mixed $other + * @param Setoid $other * * @return bool */ diff --git a/src/FantasyLand/Traversable.php b/src/FantasyLand/Traversable.php index 76cd9c3..4e977eb 100644 --- a/src/FantasyLand/Traversable.php +++ b/src/FantasyLand/Traversable.php @@ -6,7 +6,7 @@ /** * @template a - * @template-extends Functor + * @extends Functor */ interface Traversable extends Functor { @@ -16,9 +16,8 @@ interface Traversable extends Functor * Where the `a` is value inside of container. * * @template b - * @template fn of callable(a): Applicative * - * @param fn $fn (a -> f b) + * @param callable(a): Applicative $fn (a -> f b) * * @return Applicative> f (t b) */ diff --git a/src/FantasyLand/Useful/Identity.php b/src/FantasyLand/Useful/Identity.php index 042cfab..5635389 100644 --- a/src/FantasyLand/Useful/Identity.php +++ b/src/FantasyLand/Useful/Identity.php @@ -8,25 +8,31 @@ /** * @template a - * @template-extends FantasyLand\Monad + * @implements FantasyLand\Monad */ class Identity implements FantasyLand\Monad { - const of = 'FunctionalPHP\FantasyLand\Useful\Identity::of'; + public const of = 'FunctionalPHP\FantasyLand\Useful\Identity::of'; /** - * @var mixed + * @var a */ private $value; /** * @inheritdoc + * @template A + * @param A $value + * @return Identity */ public static function of($value) { - return new self($value); + return new Identity($value); } + /** + * @param a $value + */ private function __construct($value) { $this->value = $value; @@ -34,18 +40,61 @@ private function __construct($value) /** * @inheritdoc + * @template b + * @param callable(a): b $transformation + * @return Identity */ - public function map(callable $transformation): FantasyLand\Functor + public function map(callable $transformation): Identity { - return static::of($this->bind($transformation)); + $fn = + /** + * This is a workaround so that static analysis tools can understand + * the type signature of the callables. + * + * @param a $x + * @return Identity + */ + static function ($x) use ($transformation): Identity { + return Identity::of($transformation($x)); + }; + + /** @var Identity */ + return $this->bind($fn); } /** * @inheritdoc + * + * TODO: Not sure how to write the type signature for this method; it seems + * to rely on the current `a` type being a subtype of `callable(b): c` + * which cannot be guaranteed by the type system as this may have been + * instantiated with any type. Calling `->ap(...)` on an instance of + * `Identity` with a non-callable value will result in a runtime error + * that cannot be caught by the type system. Additionally, if the + * `Identity` instance has a callable value, we still cannot guarentee + * that the types line up correctly, so really anything could happen + * here. + * + * @template b + * @param Identity $applicative + * @return Identity|never */ public function ap(FantasyLand\Apply $applicative): FantasyLand\Apply { - return $applicative->map($this->value); + $value = $this->value; + + if (!is_callable($value)) { + throw new \TypeError('Cannot call `ap` on an Identity instance with a non-callable value'); + } + + /** + * Being optimistic and hoping that _if_ $value is callable then the + * types should line up correctly (assuming the user of the function + * hasn't provided a callable of a different type). + * + * @var callable(b): mixed $value + */ + return $applicative->map($value); } /** diff --git a/src/FantasyLand/functions.php b/src/FantasyLand/functions.php index 41c2f59..c76408d 100644 --- a/src/FantasyLand/functions.php +++ b/src/FantasyLand/functions.php @@ -63,7 +63,6 @@ function emptyy(Monoid $a): Monoid return $a::mempty(); } - /** * @var callable */ @@ -74,19 +73,17 @@ function emptyy(Monoid $a): Monoid * * @template a * @template b - * @template f of callable(a): b - * @template next of callable(Functor): Functor * - * @param f $transformation + * @param callable(a): b $transformation * @param Functor|null $value * - * @return Functor|next If a functor was provided directly to map, returns - * the result of applying the transformation to the - * value. Otherwise, returns a curried function that - * expects a functor. + * @return Functor|(callable(Functor): Functor) If a functor was provided directly to map, returns the result + * of applying the transformation to the value. Otherwise, returns + * a curried function that expects a functor. */ function map(callable $transformation, ?Functor $value = null) { + /** @var Functor|(callable(Functor): Functor) */ return curryN(2, function (callable $transformation, Functor $value) { return $value->map($transformation); })(...func_get_args()); @@ -102,19 +99,22 @@ function map(callable $transformation, ?Functor $value = null) * * @template a * @template b - * @template f of callable(a): Monad - * @template next of callable(Monad): Monad * - * @param f $function - * @param Monad|null $value + * @param callable(a): Monad $function + * @param Monad|null $value * - * @return Monad|next If a monad was provided directly to bind, returns the - * result. Otherwise, returns a curried function that - * expects a monad. + * @return Monad|(callable(Monad): Monad) If a monad was provided directly to bind, returns the result. + * Otherwise, returns a curried function that expects a monad. */ function bind(callable $function, ?Monad $value = null) { - return curryN(2, function (callable $function, Monad $value) { + /** @var Monad|(callable(Monad): Monad) */ + return curryN(2, function (callable $function, Monad $value): Monad { + /** + * @var callable(a): Monad $function + * @var Monad $value + */ + /** @var Monad */ return $value->bind($function); })(...func_get_args()); } @@ -128,13 +128,10 @@ function bind(callable $function, ?Monad $value = null) * @template a * @template b * @template c - * @template f of callable(b): c - * @template g of callable(a): b - * @template composed of callable(a): c * - * @param f $f - * @param g $g - * @return composed + * @param callable(b): c $f + * @param callable(a): b $g + * @return callable(a): c */ function compose(callable $f, callable $g): callable { @@ -143,7 +140,6 @@ function compose(callable $f, callable $g): callable }; } - /** * @var callable */ @@ -154,42 +150,53 @@ function compose(callable $f, callable $g): callable * * @template a * @template b - * @template f of callable(a): b - * @template next of callable(f): b * - * @param a $x - * @param f|null $f + * @param a $x + * @param callable(a): b|null $f * - * @return b|next If a function was provided directly to applicator, returns the - * result of applying the function to the value. Otherwise, - * returns a curried function that expects said function. + * @return b|(callable(callable(a): b): b) If a function was provided directly to applicator, returns the result of + * applying the function to the value. Otherwise, returns a curried function + * that expects said function. */ function applicator($x, ?callable $f = null) { - return curryN(2, function ($x, callable $f) { - return $f($x); - })(...func_get_args()); + return curryN( + 2, + /** + * @param a $x + * @param callable(a): b $f + * @return b + */ + function ($x, callable $f) { + return $f($x); + } + )(...func_get_args()); } - /** * Curry function * * @param int $numberOfArguments * @param callable $function - * @param array $args + * @param mixed[] $args * * @return callable */ function curryN($numberOfArguments, callable $function, array $args = []) { - return function (...$argsNext) use ($numberOfArguments, $function, $args) { - $argsLeft = $numberOfArguments - func_num_args(); - - return $argsLeft <= 0 - ? $function(...push_($args, $argsNext)) - : curryN($argsLeft, $function, push_($args, $argsNext)); - }; + return + /** + * @param mixed ...$argsNext + * + * @return mixed|callable + */ + function (...$argsNext) use ($numberOfArguments, $function, $args) { + $argsLeft = $numberOfArguments - func_num_args(); + + return $argsLeft <= 0 + ? $function(...push_($args, $argsNext)) + : curryN($argsLeft, $function, push_($args, $argsNext)); + }; } /**