crell/fp

适用于 PHP 8 及以上版本的函数工具

维护者

详细信息

github.com/Crell/fp

主页

源码

问题

资助包维护!
Crell

1.0.0 2023-10-28 20:06 UTC

README

Latest Version on Packagist Software License Total Downloads

此库包含用于 PHP 8.1 及以上版本的功能工具。其主要工具是 pipe() 函数,该函数接受一个起始参数,然后是一系列可调用的函数来“管道”该参数。其他大多数函数是生成闭包的实用工具,该闭包以之前 pipe() 步骤的返回值为唯一参数。

这为构建多步功能管道和组合提供了相当好的体验,至少直到 PHP 本身获得合适的管道操作符。:-) 它还提供了一个方便的无参数样式体验。

安装

通过 Composer

$ composer require crell/fp

使用方法

管道和组合

此库中最重要的函数是 pipe()。它接受任意数量的参数。第一个是您想要通过一系列函数发送的起始值。其余的是任何单参数可调用函数(单个参数的可调用函数),它返回一个值。pipe() 将第一个值传递给第一个可调用函数,然后将该结果传递给第二个可调用函数,然后将其结果传递给第三个可调用函数,依此类推,直到管道结束。然后返回最终结果。

例如,一个简单的例子

use function Crell\fp\pipe;

$result = pipe(5,
  static fn ($in) => $in ** 4,     // Returns 625
  static fn ($in) => $in / 4,     // Returns 156.25
  static fn ($in) => (string)$in,  // Coerces the number to a string
  strlen(...),                    // Returns the length of the string
);
// $result is now 6, because "156.25" has 6 characters in it.

还有一个类似的方法 compose(),它只接受任意数量的可调用函数,并生成一个函数,该函数将接受一个参数并将它传递给所有这些函数。区别在于 compose() 返回结果可调用函数,而 pipe() 立即执行。技术上,根据另一个实现它们是微不足道的,但出于性能原因,它们是分开的。

可管道函数

如上所述,pipe() 仅与单参数函数一起工作。然而,PHP 有许多函数不是单参数的,包括许多最有用的数组和字符串函数。因此,此库提供了大多数常见操作的替代、管道友好的版本。所有这些都将接受一些参数并返回一个闭包,该闭包具有部分应用的参数;也就是说,提供的参数被“保存”并在返回函数被调用时使用。通常这将在 pipe() 链中完成,但也可以直接调用,如果需要的话。

例如,explode() 函数(已命名空间化以避免与全局函数冲突),接受一个参数,分隔符。它的返回值是一个可调用函数,当调用时,将使用提供的字符串和保存的分隔符调用内置的 \explode() 函数。

use function Crell\fp\explode;

$result = pipe("Hello world",
  explode(' '),  // Produces ['Hello', 'world']
  count(...),    // Returns tne number of array elements, which is 2
);
// $result is now 2

// or

$words = explode(' ')("Hello World");
// $words is now ['Hello', 'world']

这种方法的优点是,几乎所有针/干草问题都消失了,因为要操作的值要么包含在管道中,要么作为次要参数列表非常清楚地提供。

大多数函数将简单地包装并尽可能回退到标准库函数。

字符串函数

以下所有函数都在 Crell\fp 命名空间中。

explode(string $delimiter) - 使用 $delimiter 分割管道字符串。

implode(string $glue) - 使用 $glue 合并管道数组。

replace(array|string $find, array|string $replace) - 在管道字符串中进行查找/替换,使用 str_replace()

对象函数

以下所有函数都在 Crell\fp 命名空间中。

prop(string $prop) - 返回管道对象的 $prop 公共属性。

method(string $method, ...$args) - 使用 $args 作为参数在管道对象上调用 $method。支持位置参数和命名参数。

typeIs(string $type) - 如果管道值是指定的类型,则返回 true,否则返回 false。合法类型包括 intstringfloatboolarrayresource 或类/接口名称。这通常是管道中的最后一个函数。

数组函数

以下所有函数都在 Crell\fp 命名空间中。

以下许多情况中,函数有多种版本。它们在两个轴上有所不同:是否返回数组或可迭代对象,以及是否操作数组键。

PHP 的内置数组函数不接受可迭代对象;以下几乎所有函数都接受。以 a 开头的函数将返回一个数组,即使传入的是迭代器也是如此。以 it 开头的函数将返回一个生成器,它将懒性地产生值。

除非另有说明,否则函数仅对数组值进行操作。数组键将被显式忽略,不会传递给提供的回调,但会保留。如果函数有 withKeys 后缀,则键将可用于提供的回调。

这是由于三个相互作用的 PHP 特性的组合。

  1. 所有数组都是关联数组,但有些数组会通过列表键短路,而不是将列表和映射作为两个不同的结构。
  2. PHP 支持可选参数,这意味着如果将数组键作为可选的第二个参数传递,某些函数可能会出现错误。
  3. PHP 用户空间函数将静默忽略多余的参数,但组合函数如果调用时带有过多参数将失败。

这些设计选择的结果是,在不知道键是否重要的情况下,无法可靠地构建一个将可调用的函数应用于数组的函数。这个区分必须由开发者进行。非键版本需要带有单个参数(数组值)的回调,而 withKeys 版本将值和键作为两个单独的参数传递给回调。

决定是否使用贪婪(数组)或懒惰(可迭代)版本的函数取决于适合您用例的权衡。一般来说,贪婪版本将更快,但可能使用更多内存,而懒惰版本将使用更少的内存,但可能更慢。差异有多大将因具体用例而异。

映射

将提供的可调用的函数应用于可迭代的每个条目,产生一个新的可迭代对象,具有与源相同的键,但值被替换为相应的回调结果。

  • amap(callable $c)
  • amapWithKeys(callable $c)
  • itmap(callable $c)
  • itmapWithKeys(callable $c)

过滤

产生一个新的数组,只包含那些可调用的回调返回 true 的数组条目。数组键被保留。如果没有提供回调,则使用默认的 "is truthy",就像 PHP 的原生 array_filter() 一样。

  • afilter(?callable $c = null)
  • afilterWithKeys(?callable $c = null)
  • itfilter(?callable $c = null)
  • itfilterWithKeys(?callable $c = null)

收集

collect() 函数将接受一个管道可迭代对象或数组,并产生一个数组。它实际上只是 iterator_to_array() 的包装,用于防止将其传递给不支持在 PHP 8.1 中传递的数组。在 PHP 8.2 及更高版本中,此函数与直接在管道中使用 iterator_to_array(...) 相当,因为它现在也接受数组。

归约

减少,也称为某些语言中的foldfoldl,涉及迭代地将一个操作应用于数组,以生成一个最终结果。有关更多信息,请参阅array_reduce()

  • reduce(mixed $init, callable $c) - 从$init开始,$c将使用$init和管道可迭代中的每个元素调用,并将结果用作下一个条目的$init。可调用的签名是($runningValue, $valueFromTheArray)。返回最后一个可调用调用的结果。
  • reduceWithKeys(mixed $init, callable $c) - 与reduce()相同,但回调签名是($runningValue, $valueFromTheArray, $keyFromTheArray)
  • reduceUntil(mixed $init, callable $c, callable $stop) - 与reduce()相同,但在每次迭代后调用$stop($runningValue)。如果返回true,则提前停止过程,并返回当前的运行值。

首先,有条件地

一些函数提供了一种获取满足某些标准的首个序列值的方法。在所有情况下,如果没有找到任何内容,则返回null。

  • first(callable $c) - 返回管道可迭代中第一个使$c返回true的值。
  • firstWithKeys(callable $c) - 与first()相同,但回调传递每个条目的值和键,而不仅仅是值。
  • firstValue(callable $c) - 对管道可迭代中的每个项目调用提供的可调用函数,并根据PHP返回第一个真值结果。
  • firstValueWithKeys(callable $c) - 与firstValue()相同,但回调传递每个条目的值和键,而不仅仅是值。

其他函数

  • indexBy(callable $keyMaker) - 接受管道数组,并返回一个新数组,其中具有相同的值,但每个值的键是调用$keyMaker的结果。
  • keyedMap(callable $values, ?callable $keys = null) - 从管道数组生成新数组,其中键是调用$keys($key, $value)的结果,值是调用$values($key, $value)的结果。如果没有指定$keys回调,则提供一个默认回调,该回调只是按数字索引条目。
  • any(callable $c) - 如果$c对管道可迭代中的任何值返回true,则返回true。它可能不会在所有项上调用。
  • anyWithKeys(callable $c) - 与any()相同,但回调传递每个条目的值和键,而不仅仅是值。
  • all(callable $c) - 如果$c对管道可迭代中的所有值返回true,则返回true。它可能不会在所有项上调用。
  • allWithKeys(callable $c) - 与all()相同,但回调传递每个条目的值和键,而不仅仅是值。
  • flatten(array $arr) - 接受多维管道数组,并返回具有相同值的新数组,但将它们展平到单维顺序数组。
  • append(mixed $value, mixed $key = null) - 返回一个管道数组,但添加了提供值。如果提供了$key,则无论它是否已存在,都将值分配给该键。如果没有,则使用[]将值附加,并应用PHP的正常数组处理。
  • atake(int $count) - 接受一个管道可迭代,并返回一个数组,其中包含可迭代/数组的第一个$count个项,或者如果项少于$count,则返回所有项。
  • ittake(int $count) - 接受一个管道可迭代,并返回一个可迭代,其中包含可迭代/数组的第一个$count个项,或者如果项少于$count,则返回所有项。
  • headtail(mixed $init, callable $first, callable $rest) - 类似于 reduce(),但为第一个项目使用不同的归约函数。

实用函数

以下函数不是专为与 pipe() 一起使用而设计的,但它们是更“传统”的函数。尽管如此,它们可以作为一等闭包引用。

  • iterate(mixed $init, callable $mapper) - 生成一个无限列表生成器。第一个项目是 $init,第二个是调用 $mapper($init) 的结果,第三个是调用 $mapper 在该结果上的结果,依此类推。注意:此生成器产生一个无限列表!在调用它时确保有某些终止检查,以避免无限迭代。
  • nth(int $count, mixed $init, callable $mapper) - 与 iterate() 类似,但返回序列中的第 $count 个项目,然后停止。
  • head(array $a) - 返回数组的第一个项目,如果数组为空,则返回 null
  • tail(array $a) - 返回数组中除了第一个项目之外的所有项目。

实用特质

Crell/fp 还提供了两个特质,以帮助在函数式环境中使用对象。

可进化

Evolvable 特质提供了一个名为 with() 的单方法,它接受一个可变数量的命名参数列表。它将生成与提供的值分配给同名属性相同的对象的副本。请注意,因为它在私有作用域中运行,所以忽略了可见性。在某些情况下,这可能是不可取的。

此特质与 readonly 类配合使用最有用

use Crell\fp\Evolvable;

readonly class Person
{
    use Evolvable;

    public function __construct(
        public string $name,
        public int $age,
        public string $jobTitle,
    ) {}
}

$p = new Person("Larry");
$p2 = $p->with(age: 18, jobTitle: "Developer");

可构造

PHP 中的构造函数调用很糟糕,不能轻松地链接或管道。Newable 特质提供了一个简单的标准静态方法,它包装构造函数,使其能够链接和管道。它的参数是可变的,并直接传递给构造函数。

use Crell\fp\Newable;

readonly class Person
{
    use Newable;

    public function __construct(
        public string $name,
        public int $age,
        public string $jobTitle,
    ) {}
}

$p = Person::new("Larry", 18, "Developer");

示例

上述所有内容如何结合在一起可能并不完全明显。为了帮助使其清晰,以下是一些管道及其函数在行动中的示例。

以下示例将接受一个输入文件名 $inputFile,将其内容加载到内存中,删除空白,按行将其拆分成数组,在该数组上调用自定义 pairUp() 函数,过滤结果数组,然后计算剩余的值。所有这些都在一个简单的语句中。

$result = pipe($inputFile,
    file_get_contents(...),
    trim(...),
    explode(PHP_EOL),
    pairUp(...),
    afilter(static fn($v): bool => $v[0] > $v[1]),
    count(...),
);

此示例使用自定义函数来懒惰地读取文件中的行。这会产生一个懒惰生成器,然后将其传递给 itmap(),该函数将对生成器中的每个项目应用 parse(),但自身也是一个生成器,因此也是懒惰的。parse() 从每行输入生成一个 Step 对象。reduce() 因此接收一个 Step 对象的可迭代,并依次对每个对象应用 move(),从一个起点开始。每次,move() 都返回一个新的 Position

换句话说,这些几行代码构成了一个完整的脚本解析器,尽管是一个简单的解析器。因为每个步骤都是懒惰的,所以一次只将一个 Step 加载到内存中。

function lines(string $file): iterable
{
    $fp = fopen($file, 'rb');

    while ($line = fgets($fp)) {
        yield trim($line);
    }

    fclose($fp);
}

function parse(string $line): Step
{
    [$cmd, $size] = \explode(' ', $line);
    return new Step(cmd: Command::from($cmd), size: (int)$size);
}

$end = pipe($inputFile,
    lines(...),
    itmap(parse(...)),
    reduce(new Position(0, 0), move(...)),
);

以下替代版本使用贪婪的 amap()。这将产生一个已计算的包含 Step 对象的数组,而不是生成产生 Step 对象的生成器。

$end = pipe($inputFile,
    lines(...),
    amap(parse(...)),
    reduce(new Position(0, 0), move(...)),
);

有关更详细的示例,请参阅以下文章,这些文章使用了 Crell/fp 解决了 2021 年 Advent of Code 10 天的挑战。

  • 第 1 天:函数组合、管道和部分应用。
  • 第 2 天:映射、归约、生成器、不可变对象和 with-er 方法。
  • 第 3 天:递归、记忆化和位。
  • 第4天:首先,头部,数组展平和状态处理。
  • 第5天:压缩和嵌套管道。
  • 第6天:效率和无限流处理。
  • 第7天:函数式方法和中位数。
  • 第8天:编码、解码以及一等函数思维的价值观。
  • 第9天:更多递归的乐趣。
  • 第10天:缩减和递归,以及如何在这两者之间转换。

变更日志

有关最近更改的更多信息,请参阅变更日志

贡献

请参阅贡献指南行为准则以获取详细信息。

安全性

如果您发现任何安全相关的问题,请使用GitHub安全报告表而不是问题队列。

鸣谢

许可证

较弱的GPL版本3或更高版本。有关更多信息,请参阅许可证文件