tmciver/functional-php

该软件包最新版本(v0.10.0)没有提供许可证信息。

一个将 Haskell 和 Scala Cats 类似功能引入 PHP 的函数式编程库。

v0.10.0 2019-01-16 02:59 UTC

This package is auto-updated.

Last update: 2024-09-16 19:39:18 UTC


README

CircleCI

此存储库不再维护。现在托管在此处:[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');

但如果$aNothing的一个实例,那么结果将是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文档。如果使用字符串调用函数看起来很奇怪,那么它确实很奇怪! :)

其次,传入mapcallable必须是单参数函数,并且该参数将是包裹在Maybe中的值。第三,该函数返回的值将自动包裹在一个Maybe中。因此,调用mapMaybe的结果又是一个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);
   }
}

然后通过创建此访问者的实例并将其传递给 Maybeaccept 方法来完成转换

$response = $myMaybe->accept(new MaybeToHttpResponse());

就是这样!

要么

Either 数据类型用于表示两个可能值之一。Either 的功能与 Maybe 非常相似——实际上如此相似,以至于我不再详细介绍其使用方法,因为它几乎与 Maybe 中的介绍相同。但我会在此处指出差异。

Maybe 不同的是,它用于表示可能缺少值,而 Either 通常用于表示可能出现的错误(尽管它比这更通用)。Either 抽象类的两个子类是命名相当不直观的 LeftRight。这是因为 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();