ps/fluent-traversable

通过函数方式操作集合和数组。受guava的FluentIterable、java8 Stream框架和scala特性的启发

0.3.2 2014-11-11 17:35 UTC

This package is auto-updated.

Last update: 2024-09-11 03:44:14 UTC


README

Build Status

FluentTraversable是一个小型工具,为PHP添加了少许函数式编程特性,尤其是针对数组和集合。此库受java8 Stream框架、guava FluentIterable和Scala函数式特性的启发。要充分利用此库,了解基本的函数式模式是有益的。

摘要和主要特性:

  • 允许操作数组和实现Traversable接口的所有内容
  • 基于函数式编程概念
  • 简化了算法代码。让我们看看几个代码示例,并尝试用循环和if语句重写这些示例
  • 使代码更具声明性、可读性、更简单且更易于维护
  • 简单、考虑周到的接口:95%的方法具有0个或1个参数,5%的方法具有2个参数,0%的方法具有3个或更多参数
  • 实现中没有魔法,完全支持IDE代码补全
  • 受其他几种技术的启发,但完全适应PHP世界。此库不是其他工具的盲目复制。
  • 框架无关,只有一个小的外部依赖项 - php-option。此库不会下载互联网的一半。

快速示例

我们有一个患者数组,我们想了解按血型分组的女性患者的百分比。

    $patients = array(...);
    
    $info = FluentTraversable::from($patients)
        ->groupBy(get::value('bloodType'))
        ->map(
            FluentComposer::forArray()
                ->partition(is::eq('sex', 'female'))
                ->map(func::unary('count'))
                ->collect(function($elements){
                    list($femalesCount, $malesCount) = $elements;
                    return $femalesCount / ($femalesCount + $malesCount) * 100;
                })
        )
        ->toMap();

有关此示例的解释和更多信息,请此处获取。

目录

  1. 安装
  2. FluentTraversable
  3. FluentComposer
    1. FluentComposer作为断言/映射函数
  4. 断言
  5. Puppet
  6. 贡献
  7. 许可证

安装

安装非常简单(感谢composer ;)))

(在composer.json文件的require部分中添加)

"ps/fluent-traversable": "*"

您应选择最后一个稳定版本,通配符字符("*”)只是一个示例。

FluentTraversable

由于FluentTraversable类,您可以使用声明性和可读性的方式操作数组和集合。这里有一个简单示例。

我们想获取在2007年之前发布的书籍的男性作者的电子邮件。

    $books = array(/* some books */);
    
    $emails = array();
    
    foreach($books as $book) {
        if($book->getReleaseDate() < 2007) {
            $authors = $book->getAuthors();
            foreach($authors as $author) {
                if($author->getSex() == 'male' && $author->getEmail()) {
                    $emails[] = $author->getEmail();
                }
            }
        }
    }

好吧,嵌套循环,嵌套if语句……这看起来不太好。如果我使用php的array_map和array_filter函数,结果不会更好,甚至会更糟,所以我省略了这个示例。

使用FluentTraversable的相同代码

    //some imports
    use FluentTraversable\FluentTraversable;
    use FluentTraversable\Semantics\is;
    use FluentTraversable\Semantics\get;

    $books = array(/* some books */);
    
    $emails = FluentTraversable::from($books)
        ->filter(is::lt('releaseDate', 2007))
        ->flatMap(get::value('authors'))
        ->filter(is::eq('sex', 'male'))
        ->map(get::value('email'))
        ->filter(is::notNull())
        ->toArray();       

重要

在示例中,使用了toMaptoArray函数将元素转换为数组。这两个函数之间的区别是toArray 重新索引元素,而toMap 保留索引。当您的用例中索引不重要时,应使用toArray方法,否则应使用toMap

没有循环,没有if语句,看起来很直观,流程清晰且明确(当你知道filterflatMapmap等方法的用途时——正如我之前所说,基本函数式编程模式是必要的)。

重要

flatMap做什么?它将单个值映射到值的集合中,然后将所有这些集合合并成一个集合。在上面的例子中,Book有许多作者,多亏了flatMap,我们能够将所有作者提取到一个一维数组中。如果我们使用map,输出将会是一个作者数组的数组。

is类(Predicates类的别名)是具有一个参数的闭包工厂,并评估它为布尔值。有ltgteqnot等方法。PHP中的闭包非常冗长,你必须写function关键字、花括号、return语句、分号等——很多语法噪音。闭包是多行的(是的,我知道它可以写成单行,但那样会难以阅读),因此它并不紧凑。为了处理简单的谓词情况,你可能需要使用is类。更多关于谓词的内容可以在谓词部分阅读。

get::value('authors')也是闭包的快捷方式,这和下面这个表达式语义等价:

    function($object){
        return $object->getAuthors();
    }

谓词和get::value函数支持嵌套路径,因此这段代码可以按预期工作:get::value('address.city.name')

重要

在大多数函数(有意义的函数)中,谓词/映射函数提供了两个参数:元素值和索引

    FluentTraversable::from(array('a' => 'A', 'b' => 'B'))
        ->map(function($value, $index){
            return $value.$index;
        })
        ->toMap();
        //result will be: array('a' => 'Aa', 'b' => 'Bb')

当你不想将索引作为第二个参数传递时,可以使用func::unary($func)函数。它非常有用,尤其是当你想使用PHP内置函数,该函数有可选的第二个参数,具有不同的意义时,例如str_split

    FluentTraversable::from(array('some', 'values'))
        ->flatMap(func::unary('str_split'))
        ->toArray();
        //result will be: array('s', 'o', 'm', 'e', 'v', 'a', 'l', 'u', 'e')

FluentTraversable有很多有用的方法:mapflatMapfilteruniquegroupByorderByallMatchanyMatchnoneMatchfirstMatchmaxByminByreducetoArraytoMap等。所有这些方法的列表、描述和示例都可以在TraversableFlow接口中找到。每个方法属于两个组之一:中间操作或终止操作。中间操作对输入数组做一些工作,修改它,并返回FluentTraversable对象以进行进一步处理,因此你可以链式调用另一个操作。终止操作对数组的每个元素做一些计算,并返回这个计算的结果。例如,size操作返回一个整数,即输入数组的长度,因此不能再链式调用操作。

示例

    FluentTraversable::from(array())
        ->filter(...)//intermediate operation, so I can chain
        ->map(...)//intermediate operation, so I can chain
        ->size()//terminate operation, I cannot chain - it returns integer

有几个终端操作返回Option值(如果你不知道Option或Optional值模式是什么,请点击以下链接:php-optionOptional解释在Java中)。例如,firstMatch方法可能找不到任何东西,所以它返回null或添加第二个可选参数以提供默认值,而是返回Option对象。Option对象是值的包装器,它可以包含值,但不必包含。你应该将Option视为包含0个或1个值的集合。Option类提供了一些与FluentTraversable类似的方法,例如mapfilter。你可以通过getOrElse方法从Option中获取值。

示例

    FluentTraversable::from($books)
        ->firstMatch(is::eq('author.name', 'Stephen King'))
        //there is Option instance, you can transform value (thanks to map) if this value exists
        ->map(function($book){
            return 'Found book: '.$book->getTitle();
        })
        //provide default value if book wasn't found
        ->orElse(Option::fromValue('Not found any book...'))
        //print result to stdout thanks to Option::map method
        ->map('printf')
        //or you can call "->get()" and assign to variable, it is safe because you provided default value by "orElse"
        ;

如果找到斯蒂芬·金的书籍,将打印“Found book: TITLE”,否则打印“Not found any book...”。

正确使用时,选项功能非常强大,并且与 FluentTraversable 完美集成。Option::map 方法非常低调,但非常有用。多亏了 Option::map,您可以在有值的情况下执行代码块,而不必使用 if 语句。

    FluentTraversable::from($books)
        ->maxBy(get::value('rating'))
        ->map(function(Book $book){
            $this->takeToBackpack($book);
        });

重要

在许多情况下,Option 非常有用,并且它经常简化代码。如果您不知道如何正确使用它,请查看本文档中的“更大的示例”部分以及所有包含 Option 的示例。OptiongetOrElse 方法,因此您最终可以使用它来获取值或默认值。但是,我建议您学习如何正确使用这种模式,在文献中它也被称为 MaybeOptional 模式。

重要

当您想要使用 Option::map 函数时,请注意,当提供的映射函数返回 null 时,map 函数将返回 Some(null)(而不是 None())——这可能是不可取的。以下示例不正确,因为 $patientRepo::find() 方法可能会返回 null

Option::fromValue($patientId)
  ->map([$patientRepo,'find'])
  //there could be `Some(null)` value! 
  ->map(get::value('doctor.phone'))
  //there could be also `Some(null)` value, so `null` might be passed to `$this::callToDoctor`
  ->each([$this,'callToDoctor']);

当您想要转换被 Option 包裹的值,并且映射函数可能返回 null 时,您应使用 flatMapget::option() 组合。下面是一个正确的示例

Option::fromArrayValue($patientId)
  ->flatMap(get::option([$patientRepo,'find']))
  //when `$this::callToDoctor` return `null` there will be `None`
  ->flatMap(get::option('doctor.phone'))
  //when doctor has not phone set, there will be `None` value
  ->each([$this,'callToDoctor']);

get::optionget::value 类似,区别在于它将值包裹在 Option 类型中。

FluentComposer

FluentComposer 是一个用于在数组上组合复杂操作的工具。您可以通过组合器定义一个复杂操作,并将其多次应用于任何数组。FluentComposerFluentTraversable 具有相同的接口(这两个类实现了相同的接口:TraversableFlow)。

下面有一个示例

    $maxEvenPrinter = FluentComposer::forArray();

    //very important is, to not chain directly from `forArray()` method, first you should assign created object
    //to variable, and then using reference to object you can compose your function

    $maxEvenPrinter
        ->filter(function($number){
            //only even numbers
            return $number % 2 === 0;
        })
        ->max()
        //"max" (as same as firstMatch) returns Option, because there is possibility given array is empty
        ->map(function($value){
            return 'max even number: '.$value;
        })
        ->orElse(Option::fromValue('max even number not found'))
        ->map('printf');

好的,我们有了 $maxEvenPrinter 对象,接下来是什么?

    $maxEvenPrinter(array(1, 3, 5, 2, 4));
    //output will be: "max even number: 4"
    
    $maxEvenPrinter(array(1, 3, 5));
    //output will be: "max even number not found"

正如我所说的,FluentComposer 几乎与 FluentTraversable 具有相同的函数。这两个类之间的区别在于,FluentTraversable 需要输入数组来创建对象,并且它应该只使用一次,而 FluentComposer 在创建对象时不需要数组,并且可以使用不同的输入数组多次调用。内部上,FluentComposer 使用 FluentTraversable 实例 ;) 您应该将 FluentComposer 视为组合函数的工具。

FluentComposer 有三个工厂方法,它们在创建的函数接受的参数方面有所不同

  • FluentComposer::forArray() - 创建的函数接受一个数组/可遍历参数

        $func = FluentComposer::forArray();
        $func-> /* some chaining methods */;
            
        $func(array('value1', 'value2', 'value3'));
  • FluentComposer::forVarargs() - 创建的函数接受可变数量的参数(varargs)

        $func = FluentComposer::forVarargs();
        $func-> /* some chaining methods */;
        
        $func('value1', 'value2', 'value3');
  • FluentComposer::forValue() - 创建的函数接受一个参数,该参数将被视为数组的唯一元素。此方法类似于 FluentComposer::forVarargs(),区别在于除了第一个参数外,所有参数都被忽略。

        $func = FluentComposer::forValue();
        $func-> /* some chaining methods */;
    
        $func('value1', 'this value will be ignored')

FluentComposer作为断言/映射函数

您可以使用 FluentComposer 创建用于 FluentTraversable 的谓词或映射函数,特别是在将单个值转换为值数组的函数之后(例如 groupBypartition 等)。

示例

我们有一个病人数组,我们想知道按血型分组的女性病人百分比。

    $patients = array(...);
    
    $info = FluentTraversable::from($patients)
        ->groupBy(get::value('bloodType'))
        //we have multi-dimensional array, where key is bloodType, value is array of patients
        ->map(
            //we map array of patients for each blood type to percentage value, so lets compose a function
            FluentComposer::forArray()
                //split array of patients into two arrays, first females, second males
                ->partition(is::eq('sex', 'female'))
                //map those arrays to its size, so we have number of females and males
                ->map(func::unary('count'))
                //calculate a percent
                ->collect(function($elements){
                    list($femalesCount, $malesCount) = $elements;
                    return $femalesCount / ($femalesCount + $malesCount) * 100;
                })
        )
        //get our result with index preserving 
        ->toMap();

重要

直接从 FluentComposer::forArray()(和其他工厂方法)链式调用并不总是安全的,一些方法不返回 FluentComposer,而是返回 Option 对象。返回 Option 的方法是:reducefirstMatchmaxminfirstlastget。当您最终想直接从 FluentComposer::forArray() 链式调用并使用返回 Option 的终端操作时,您可以应用一个技巧

  ->map(
      $f = FluentComposer::forArray(), $f
           ->firstMatch(is::eq('name', 'Stefan'))
           ->getOrElse('Not found')
  )

还有 FluentComposer::forValue() 方法来创建只有一个参数的函数。它可能对创建针对单个值的谓词或映射函数很有用。

示例

我们想找到所有病人都是女性的医生(妇科医生?)。

    
    $doctors = array(/* some doctors */);

    $doctors = FluentTraversable::from($doctors)
        ->filter(
            FluentComposer::forValue()
                ->flatMap(get::value('patients'))
                ->allMatch(is::eq('sex', 'female'))
        )
        ->toArray();

断言

谓词是一个将单个值评估为布尔值的函数。预定义的谓词在isPredicates类中可用。这两个类相同,isPredicates的别名,因此您可以选择使用哪一个(is可以使代码更具表达性)。谓词非常适合用于FluentTraversable类的filterfirstMatchpartitionallMatchnoneMatchanyMatch方法。

大多数谓词(例如:eqnotEqgtqteidenticalnotIdenticalinnotIncontains)都有两个版本

  • 一元:predicate($valueToCompare)

        $gt25 = is::gt(25);    
        $gt25(26);//evaluates to true
  • 二元 - predicate($property, $valueToCompare)

        $ageGt25 = is::gt('age', 25);
        $gt25(array('age', 26));//evaluates to true

少数谓词(如nullnotNullfalsetrueblanknotBlank)也有两个版本,但不同

  • 无参数:predicate()

        $true = is::true();    
        $true(true);//evaluates to true
  • 一元:predicate($property)

        $true = is::true('awesome');    
        $true(array('awesome' => true));//evaluates to true

还有逻辑谓词(如notallTrue - 逻辑与,anyTrue - 逻辑或),但创建复杂的谓词时,可能更好的方法是直接使用闭包。allTrueanyTrue也可以接受评估后的值,例如

    $alwaysFalse = is::allTrue(false, is::eq(25));
    $alwaysTrue = is::anyTrue(true, is::eq(25));

评估后的值在过滤某些值取决于外部条件且您不想因为可读性目的使用单独的if语句时很有用——当然,如果您的数组真的很大,请注意,将遍历所有元素,因此请谨慎使用此功能并明智地使用它。

重要

谓词还可以与分组函数一起使用。目前只有size::of()函数。

示例

我们想找到拥有少于5名病人的医生

    $doctors = array(...);
    
    $doctors = FluentTraversable::from($doctors)
        ->filter(is::lt(size::of('patients'), 5))
        ->toArray();

Puppet

Puppet是一个非常小的类(小于100行代码),但功能强大。什么是Puppet?多亏了Puppet,您可以“记录”某些行为,并在各种对象上多次执行此行为。

示例

    $book = ...;
    $puppet = Puppet::record()->getPublisher()->getName();
    
    echo $puppet($book);//$book->getPublisher()->getName() will be invoked

Puppet支持属性访问、数组访问和带参数的方法调用。最初它是为了简化mapflatMap操作而创建的,它还由FluentComposer内部使用,但您可能会为Puppet找到其他用途。

Puppet有两个工厂方法:recordobject——这些方法相同,object方法仅出于语义目的创建。您可以使用PuppetmapflatMap等函数创建映射函数,但推荐使用get::value()

the类是Puppet的别名,它仅在FluentTraversable上下文中使用Puppet时增加语义意义:`->map(the::object()->getName())`比`->map(Puppet::record()->getName())`更易于阅读。

贡献

欢迎提出建议、PR、错误报告等;)

许可证

MIT - 详细信息请参阅LICENSE文件