tmciver / functional-php
一个将 Haskell 和 Scala Cats 类似功能引入 PHP 的函数式编程库。
Requires
- raphhh/trex-reflection: 1.0.*
Requires (Dev)
- phpunit/phpunit: 6.5.8
README
此存储库不再维护。现在托管在此处:[https://github.com/serenitylabz/phatcats](https://github.com/serenitylabz/phatcats)
函数式 PHP
内容
关于
此库旨在为 PHP 开发者提供类似于 Haskell 语言的 Category Theory 类似设施。
运行测试
本地
本项目使用 Composer 进行依赖管理,以及 PHPUnit 进行单元测试。首先,安装依赖项(包括开发依赖项)
$ composer install --dev
PHPUnit 需要访问自动加载,并且自动加载文件必须首先使用以下命令由 composer 生成
$ composer dump-autoload
然后,使用以下命令运行单元测试
$ ./vendor/bin/phpunit
使用 Docker
如果您已安装 docker,可以使用以下命令运行测试
$ make test
类型类
以下类型类受到支持
SemiGroup
Monoid
Functor
Monad
Applicative
Traversable
Foldable
请注意,并非所有类型都支持所有类型类。
类型
此库支持以下类型
链表
可能
要么
MaybeT
验证
关联数组
请注意,并非所有受支持的类型都是上述类型类的实例。
链表
LinkedList
类型是一个实现典型链表数据结构的 抽象数据类型。
创建
LinkedList
应使用 LinkedListFactory
类的实例来创建。例如,您可以创建一个空列表
$listFactory = new LinkedListFactory(); $emptyList = $listFactory->empty();
您可以从 PHP 数组创建一个 LinkedList
,如下所示
$arr = ['apples', 'oranges', 'bananas']; $l = $listFactory->fromNativeArray($arr); // $l = LinkedList('apples', 'oranges', 'bananas');
您还可以轻松地创建一系列值。这与标准库函数 range 完全相同。
$l = $listFactory->range('a', 'f', 2); // $l = LinkedList('a', 'c', 'e');
链表 Monoid
LinkedList
可以被 append
。
$l1 = $listFactory->fromNativeArray([1, 2, 3]); $l2 = $listFactory->fromNativeArray([4, 5, 6]); $l3 = $l1->append($l2); // $l3 = LinkedList(1, 2, 3, 4, 5, 6);
链表 Functor
正如预期的那样,LinkedList
是 Functors。只需将一个参数的函数传递给 map
方法即可
$l = $listFactory->fromNativeArray([1, 2, 3]); $linc = $l->map(function ($x) { return $x + 1; }); // $linc = LinkedList(2, 3, 4);
链表 Monad
它们也是 Monads
$l = $listFactory->fromNativeArray(["Hello", "world"]); $explodedStrs = $l->flatMap(function ($s) use ($listFactory) { return $listFactory->fromNativeArray(str_split($s)); }); // $explodedStrs = LinkedList('H', 'e', 'l', 'l', 'o', 'w', 'o', 'r', 'l', 'd');
链表 Applicative
如果您之前从未见过它,链表应用可能有点难以理解。它允许您将函数列表中的每个函数应用于参数列表中的每个参数。一个例子可能会使它更清晰。
$firstThree = function ($s) { return substr($s, 0, 3); }; $fs = $listFactory->fromNativeArray(['strtoupper', $firstThree]); $args = $listFactory->fromNativeArray(["Hello", "world"]); $result = $fs->apply($args); // $result = LinkedList("HELLO", "WORLD", "Hel", "wor");
可以使用 __invoke
魔法方法实现相同的结果
$result = $fs($args); // $result = LinkedList("HELLO", "WORLD", "Hel", "wor");
您甚至可以调用无参数函数的 LinkedList
$one = function () { return 1; }; $fs = $listFactory->fromNativeArray(['time', $one]); $vals = $fs(); // $vals = LinkedList(1527883005, 1);
链表 Traversable
《Traversable》类型类的《traverse》方法是另一个可能一开始看起来有点奇怪的,但实际上非常有用的方法。《traverse》的第一个参数是一个函数,它接受一个《LinkedList》元素并返回某种monad。作为第二个参数,它接受该monad的实例。《traverse》的返回值是一个包装着包含由传入函数返回的monad包装的值的《LinkedList》的monad实例。虽然听起来有些复杂,但通过示例来看会更加直观。首先,让我们定义一个返回《Maybe》的函数。
$divideTwelveBy = function ($denom) { return ($denom == 0) ? Maybe::nothing() : Maybe::fromValue(12 / $denom); };
然后我们将使用该函数遍历一个整数列表。
$l = $listFactory->fromNativeArray([1, 2, 3, 4]); $divisions = $l->traverse($divideTwelveBy); // $divisions = Just(LinkedList(12, 6, 4, 3));
当您想要映射一个《LinkedList》,但映射的结果会得到一个包含某些monad的《LinkedList》时,《traverse》非常有用。使用《traverse》可以反转《LinkedList》和monad。
但请注意在这个示例中,如果对《$divideTwelveBy》的调用返回《Nothing》会发生什么。
$l = $listFactory->fromNativeArray([1, 0, 3, 4]); $divisions = $l->traverse($divideTwelveBy); // $divisions = Nothing;
《Traversable》类型类还有一个名为《sequence》的方法,它在您已经有某些monad的《LinkedList》时非常有用。
$l = $listFactory->fromNativeArray([ Maybe::fromValue(1), Maybe::fromValue(2), Maybe::fromValue(3) ]); $m = $l->sequence(); // $m = Just(LinkedList(1, 2, 3));
《LinkedList》可折叠性
《LinkedList》也可以以各种方式折叠。这里是一个经典的例子,用于计算整数列表的总和。
$l = $listFactory->fromNativeArray([1, 2, 3, 4]); $add = function ($x, $y) { return $x + $y; }; $sum = $l->foldLeft(0, $add); // $sum = 10
或者乘以同一列表的元素。
$mult = function ($x, $y) { return $x * $y; }; $product = $l->foldLeft(1, $mult); // $product = 24
还有《foldRight》,它可以用来拼接两个《LinkedList》,以及其他一些功能。
$l1 = $listFactory->fromNativeArray([1, 2, 3]); $l2 = $listFactory->fromNativeArray([4, 5, 6]); $cons = function ($x, $l) { return $l->cons($x); }; $l1PlusL2 = $l1->foldRight($l2, $cons); // $l1PlusL2 = LinkedList(1, 2, 3, 4, 5, 6);
然后是《fold》,它将某个幺半群作为参数。使用《fold》的想法是假设元素都是某个幺半群,并通过将它们全部通过《append》结合在一起来组合它们。当《LinkedList》为空时需要幺半群参数。
$nothing = Maybe::nothing(); $monoid = $nothing; $list = $this->makeListFromArray([Maybe::fromValue("hello"), $nothing, Maybe::fromValue(" world!")]); $result = $list->fold($monoid); // $result = Just("hello world!")
《foldMap》与《fold》类似,但作为第二个参数接受一个函数,该函数将每个元素转换为幺半群,然后通过《append》将它们拼接起来。这里有一个字符串的《LinkedList》,而不是像上面那样的《Maybe》字符串《LinkedList》。在《append》之前,使用《$toMonoid》函数将它们转换。
$nothing = Maybe::nothing(); $monoid = $nothing; $list = $this->makeListFromArray(["hello", " world!"]); $toMonoid = function ($v) { return Maybe::fromValue($v); }; $result = $list->foldMap($monoid, $toMonoid); // $result = Just("hello world!")
《LinkedList》集合
《LinkedList》还实现了《Collection》特性。所有操作都按预期进行,我们不会详细描述它们,仅提供一个过滤《LinkedList》的示例。
$l = $listFactory->fromNativeArray([1, 2, 3, 4, 5]); $isOdd = function ($n) { return $n % 2 == 1; }; $lOdd = $l->filter($isOdd); // $lOdd = LinkedList(1, 3, 5)
可能
《Maybe》用于表示可能存在值缺失的情况。通常,您会在函数中返回null值时使用《Maybe》。有时,《Maybe》也用于表示错误条件。
《Maybe》作为抽象类实现,有两个具体的子类:《Just》和《Nothing》。但您不能直接实例化这些子类;您必须使用《Maybe》类中定义的静态创建方法。如果您想在一个《Maybe》上下文中放入一个普通值,使用如下所示的《fromValue()`》静态方法。
$maybeInt = Maybe::fromValue($myInt);
上述代码执行后,如果《$myInt》不为null,则《$maybeInt》将是《Just》的实例;否则,《$maybeInt》将是《Nothing》的实例。如果您想表示值的缺失,请使用《nothing()`》静态方法。
$maybeInt = Maybe::nothing();
访问包装的值
您可能想直接访问《Maybe》中包装的值。这只有在您有默认值可以在《Maybe》是《Nothing》的情况下使用时才有意义。在Haskell中,您可以使用《fromMaybe》函数来实现这一点。这里,您可以通过调用《getOrElse`方法来这样做。
// preferred $a = Maybe::fromValue(5); $b = Maybe::nothing(); $a->getOrElse(0); // yields 5 $b->getOrElse(0); // yields 0
可能作为 Functor
另一个常见的愿望是将一个普通函数应用到包裹在Maybe
中的值上,并将返回的值再次包裹在一个Maybe
中。这种用法的数据类型被称为Functor
。
以下代码展示了如何将字符串"apples"转换为大写,同时它被包含在一个Maybe
中
$a = Maybe::fromValue('apples'); $maybeUppercase = $a->map('strtoupper'); // $maybeUppercase = Just('APPLES');
但如果$a
是Nothing
的一个实例,那么结果将是Nothing()
,而strtoupper
函数将不会运行。您还可以链式调用map
$a = Maybe::fromValue('apples'); $maybeUppercaseOfFirstLetter = $a->map('strtoupper') ->map(function ($str) { return substr($str, 0, 1); }); // $maybeUppercaseOfFirstLetter = Just('A');
这里有几个需要注意的地方。首先,map
接受一个callable
。在PHP中,callable
有多种形式,但其中之一是字符串。在第一次调用map
时,我们传入了一个内置PHP函数的字符串形式。在第二种情况下,我们传入了一个匿名函数,它也是一个callable
。有关更多信息,请参阅PHP的callable
文档。如果使用字符串调用函数看起来很奇怪,那么它确实很奇怪! :)
其次,传入map
的callable
必须是单参数函数,并且该参数将是包裹在Maybe
中的值。第三,该函数返回的值将自动
包裹在一个Maybe
中。因此,调用map
的Maybe
的结果又是一个Maybe
,这允许我们这样链式调用map
。
可能作为 Monad
通常,您想应用到包裹在Maybe
中的值上的函数本身会返回一个Maybe
。在这种情况下,您不能直接使用map
方法。为了说明这一点,我们首先创建一个返回Maybe
的函数。一个经典的例子是head()
函数,它返回一个数组的第一元素。奇怪的是,PHP没有这样的函数,推荐的解决方案并不直接,如StackOverflow答案所示:http://stackoverflow.com/a/3771228。但即使您使用那里描述的复杂解决方案,您仍然必须处理空数组时可能返回的NULL
值。
以下函数隐藏了获取数组第一个元素的复杂性,并返回一个Maybe
类型,这样我们就不需要处理NULL
了
function head($array) { if (is_array($array)) { if (count($array) > 0) { $vals = array_values($array); $h = Maybe::fromValue($array[0]); } else { $h = Maybe::nothing(); } } else { $h = Maybe::nothing(); } return $h; }
当给定一个非空数组时,上述函数将返回Just($v)
,其中$v
是数组参数的第一个值。在其他所有情况下,它将返回Nothing
。有关使用此函数的示例,请参阅此文件。
为了说明为什么我们不能使用此函数与map()
,让我们扩展上面的示例,但这次不是从包裹在Maybe
中的字符串开始,而是从字符串数组开始
$a = Maybe::fromValue(['apples', 'oranges', 'bananas']); $b = $a->map('head'); // $b = Just(Just('apples'));
如您所见,我们最终在Just
中得到了一个Just
,这几乎肯定不是您通常想要的。为了解决这个问题,我们只需要使用flatMap
方法
$a = Maybe::fromValue(['apples', 'oranges', 'bananas']); $b = $a->flatMap('head'); // $b = Just('apples');
这种用法的数据类型称为Monad
。
可能作为 Monoid
您可能会遇到需要将几个Maybe
组合成一个Maybe
的情况。在Haskell中,您可以使用mappend
函数或(<>)
运算符来完成此操作。在这里,您可以使用append
方法来完成此操作。以下代码展示了它是如何工作的。
$just1 = Maybe::fromValue(1); $just2 = Maybe::fromValue(2); $nothing = Maybe::nothing(); $just1->append($nothing); // Just(1); $nothing->append($just1); // Just(1); $just1->append($just2); // Just([1, 2]); $just2->append($just1); // Just([2, 1]); $nothing->append($nothing); // Nothing();
转换
将您的 Maybe
转换为其他类型非常常见。在 Haskell 中,您可以使用 maybe
函数 来实现这一点。此库没有这样的函数,但您可以使用其他技术来实现相同的结果。例如,您可能想将一个 Maybe
转换为 HTTP 响应。您可以使用 PHP 的 instanceof
操作符如下
// Ugly, but gets the job done. if ($myMaybe instanceof Just) { $myVal = $myMaybe->get(); $response = response("<p>$myVal is: " . $myVal . ".</p>"); } else { $response = response("There was no value!", 400); }
一种稍好但等效的方法是使用提供的 isNothing()
方法
// A little better. if ($myMaybe->isNothing()) { $response = response("There was no value!", 400); } else { $myVal = $myMaybe->get(); $response = response("<p>$myVal is: " . $myVal . ".</p>"); }
将 Maybe
转换为其他类型推荐的方式是使用 访问者模式。您可以通过创建一个实现 MaybeVisitor
接口(如下所示)的类来创建一个 Maybe
访问者
class MaybeToHttpResponse implements MaybeVisitor { public function visitJust($just) { $myVal = $just->get(); return response("<p>$myVal is: " . $myVal . ".</p>"); } public function visitNothing($nothing) { return response("There was no value!", 400); } }
然后通过创建此访问者的实例并将其传递给 Maybe
的 accept
方法来完成转换
$response = $myMaybe->accept(new MaybeToHttpResponse());
就是这样!
要么
Either
数据类型用于表示两个可能值之一。Either
的功能与 Maybe
非常相似——实际上如此相似,以至于我不再详细介绍其使用方法,因为它几乎与 Maybe
中的介绍相同。但我会在此处指出差异。
与 Maybe
不同的是,它用于表示可能缺少值,而 Either
通常用于表示可能出现的错误(尽管它比这更通用)。Either
抽象类的两个子类是命名相当不直观的 Left
和 Right
。这是因为 Either
类型实际上比仅指示错误更通用;它可以用于返回任何两种可能性。我们在本库中使用 Either
的原因仅因为 Haskell 也是这样做的。
Right
子类用于表示成功的计算。您可以创建实例如下
$myEither = Either::fromValue($someVal);
Left
子类用于表示错误,您可以创建实例如下
$myEither = Either::left('Houston, we have a problem!');
请注意,我们通过传递一个包含错误消息的字符串来创建了一个 Left
。这是使用 Either
表示错误的一种常见方式,但 Left
可以包含任何类型,我们也可以同样正确(可能更清晰地)传递自定义错误或异常类。
与 Maybe
的区别仅此而已。
MaybeT
待定
关联数组
AssociativeArray
是围绕 PHP 本地数组的一个简单包装,以便可以向其中添加数组/列表处理方法。目前它仅包含 Traversable
特性的实现。
要创建一个 AssociativeArray
的实例,只需调用构造函数
$aa = new AssociativeArray([1,2,3]);
AssociativeArray Traversable
Traversable
特性中有两个方法:traverse()
和 sequence()
。sequence()
是这两个中较简单的一个,所以我们将从它开始。在 Maybe
的情况下,sequence()
最常用于将 Maybe
数组转换为 Maybe
数组,如下所示
$a = new AssociativeArray([Maybe::fromValue(1), Maybe::fromValue(2), Maybe::fromValue(3)]); $m = Maybe::nothing(); $b = $a->sequence($m); // $b = Just([1,2,3]);
请注意,必须传递一个 Applicative
实例给 sequence()
方法。这是动态类型的一个不幸后果,在这种情况下,空数组中包含的对象类型是未知的。
traverse()
方法与此类似,但它为您提供了对数组中每个值运行函数的机会。为了演示这一点,我们首先定义一个返回 Either
类型(见 Either)的函数
function divide($x, $y) { if ($y == 0) { $eitherResult = Either::left('Division by zero!'); } else { $eitherResult = Either::fromValue($x/$y); } return $eitherResult; }
然后我们使用该函数在 traverse()
调用中使用
$dividend = 12; $divisors = [2, 4, 6]; $intsArray = new AssociativeArray($divisors); $m = Either::left(''); $eitherResults = $intsArray->traverse(function ($i) use ($dividend) { return divide($dividend, $i); }, $m); // $eitherResults = Right([6,3,2]);
请注意,sequence()
和 traverse()
都具有以下特性:如果数组中的一个或多个元素为 Nothing
(在 sequence()
的情况下),或者传递给 traverse()
的函数返回 Nothing
(在 traverse()
的情况下),则结果也将是 Nothing
。让我们通过查看上述 sequence()
示例的略微修改版本来说明这一点。
$a = new AssociativeArray([Maybe::fromValue(1), Maybe::nothing(), Maybe::fromValue(3)]); $m = Maybe::nothing(); $b = $a->sequence($m); // $b = Nothing();