crell / fp
适用于 PHP 8 及以上版本的函数工具
Requires
- php: ~8.1
Requires (Dev)
- phpbench/phpbench: ^1.2
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ~10.4
README
此库包含用于 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
。合法类型包括 int
、string
、float
、bool
、array
、resource
或类/接口名称。这通常是管道中的最后一个函数。
数组函数
以下所有函数都在 Crell\fp
命名空间中。
以下许多情况中,函数有多种版本。它们在两个轴上有所不同:是否返回数组或可迭代对象,以及是否操作数组键。
PHP 的内置数组函数不接受可迭代对象;以下几乎所有函数都接受。以 a
开头的函数将返回一个数组,即使传入的是迭代器也是如此。以 it
开头的函数将返回一个生成器,它将懒性地产生值。
除非另有说明,否则函数仅对数组值进行操作。数组键将被显式忽略,不会传递给提供的回调,但会保留。如果函数有 withKeys
后缀,则键将可用于提供的回调。
这是由于三个相互作用的 PHP 特性的组合。
- 所有数组都是关联数组,但有些数组会通过列表键短路,而不是将列表和映射作为两个不同的结构。
- PHP 支持可选参数,这意味着如果将数组键作为可选的第二个参数传递,某些函数可能会出现错误。
- 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(...)
相当,因为它现在也接受数组。
归约
减少,也称为某些语言中的fold
或foldl
,涉及迭代地将一个操作应用于数组,以生成一个最终结果。有关更多信息,请参阅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或更高版本。有关更多信息,请参阅许可证文件。