marcosh/lamphpda

函数式编程数据结构集合

v3.1.1 2024-08-29 08:13 UTC

README

类型安全的函数式数据结构集合

目标

该库的目标是在PHP生态系统内以最类型安全的方式提供一系列函数式数据结构,同时提供通用和一致的API。

主要思想

区别于PHP中其他函数式库的两个主要思想是

安装

composer require marcosh/lamphpda

工具

我们使用 Psalm 作为类型检查器。它基本上作为一个编译步骤,确保所有类型都一致。

要利用这个库,您的代码必须通过Psalm检查。

开发

该库包含一个 flake.nix 文件,使您能够访问一个配备PHP 8.1和Composer的开发环境。要在目录中利用它,只需执行 nix develop 即可。对于使用 direnv(带有 nix-direnv)的人来说,还提供了一个预配置的 .envrc 文件,它在进入库目录时会自动加载环境。

决策记录

关于项目的相关决策收集在 adr 文件夹中,遵循 架构决策记录 格式。

内容

该库提供了一些不可变的数据结构,这些结构对于用函数式风格编写应用程序非常有用。

目前实现的数据结构包括

  • Maybe,它允许对可能缺失的数据建模;
  • Either,它对备选方案进行建模;
  • Identity,它只是一个简单的包装器;
  • LinkedList,它对可能包含多个值的可能性进行建模;
  • Pair,它对同时存在两个事物进行建模;
  • Reader,它对依赖于上下文的价值进行建模;
  • State,它对可以与全局状态交互的价值进行建模;
  • IO,它对惰性值进行建模。

您可以在 docs/data-structures 文件夹 中找到有关每个数据结构的实现和背后的想法的更多详细信息。

如何与数据结构交互

该库被构建得非常抽象和通用,以允许极度的可组合性和可重用性。

您可以使用多种方式与提供的数据结构进行交互。

类型类

您可以将类型类视为可以附加到数据结构上的行为。由于数据结构原则上可以以多种方式实现特定的行为(例如,使用两个整数计算新整数的方法不止一种),我们不能直接使用接口来实现我们的数据结构。因此,类型类实例作为实现接口的独立对象实现,该接口描述了类型类本身。

例如,Semigroup 类型类,它描述了将同类型两个元素组合成相同类型的元素的行为,可以像下面这样实现:

/**
 * @template A
 */
interface Semigroup
{
    /**
     * @param A $a
     * @param A $b
     * @return A
     */
    public function append($a, $b);
}

现在我们可以为任何我们想要的类型实现 Semigroup 实例,甚至包括原生类型。例如,我们可以为整数之间的加法实现一个半群

/**
 * @implements Semigruop<int>
 */
final class IntAddition implements Semigroup
{
    /**
     * @param int $a
     * @param int $b
     * @return int
     */
    public function append($a, $b): int
    {
        return $a + $b;
    }
}

然后我们可以使用它来计算两个整数的和

(new IntAddition())->append(1, 2); // returns 3

这个特定的实例并不那么有趣,但您能够编写依赖于通用 Semigroup 的代码这一事实绝对值得注意!

我们目前公开的类型类有

  • Functor,允许将一元函数提升到给定上下文中;
  • Apply,允许将任何元数的函数提升到给定上下文中;
  • Applicative,允许将值提升到上下文中;
  • Alternative,模拟组合封装在上下文中的值的能力;
  • Monad,允许按顺序执行在上下文中返回值的函数;
  • MonadThrow,允许以纯方式管理异常;
  • Foldable,允许将数据结构缩减为单个值;
  • Traversable,允许在应用上下文中通过函数转换数据结构;
  • Semigroup,允许组合相同类型的两个值;
  • Monoid,允许创建一个单位元素;
  • Bifunctor,模拟依赖于两个协变类型变量的上下文;
  • Profunctor,模拟依赖于一个逆变和一个协变类型变量的上下文。

有关每个类型类的更多详细信息,请参阅docs/typeclasses 文件夹

类型类和数据结构

作为此库的设计原则,我们试图在我们的数据结构上仅公开来自类型类的方法。这意味着提供的结构具有标准通用 API,并使用类型类实例。

例如,Either 有两个 Apply 实例。要选择您想使用的实例,Either 提供了 iapply 方法,该方法将 EitherApply 类型类实例作为第一个参数。

/**
 * @template A
 * @template B
 */
final class Either
{
    /**
     * @template C
     * @param Apply<EitherBrand<A>> $apply
     * @param HK1<EitherBrand<A>, callable(B): C> $f
     * @return Either<A, C>
     */
    public function iapply(Apply $apply, HK1 $f): self
}

我们可以指定一个类型类实例指向特定的数据结构,使用所谓的Brands,这其实是在类型级别上的标签,它使我们能够模拟高阶类型。

默认类型类实例

通常情况下,一个数据结构只接受类型类的一个实例,或者存在一个在文献中被认为是标准的实例。在这种情况下,持续传递类型类实例的负担是非常不便的;为了减轻痛苦,我们还公开了已经提供了默认类型类实例的方法。

继续前一个示例,Either还公开了一个名为apply的方法,其中硬编码了EitherApply实例。

/**
 * @template A
 * @template B
 */
final class Either
{
    /**
     * @template C
     * @param HK1<EitherBrand<A>, callable(B): C> $f
     * @return Either<A, C>
     */
    public function apply(HK1 $f): self
    {
        return $this->iapply(new EitherApply(), $f);
    }
}

贡献

如果您想为该项目做出贡献,请阅读贡献指南