PHP-函数式编程高级教程-三-

44 阅读25分钟

PHP 函数式编程高级教程(三)

原文:Pro Functional PHP Programming

协议:CC BY-NC-SA 4.0

六、使用函数管理业务逻辑

在这一章中,你将会看到函数式编程的其他一些常见用法。您将从了解功能代码如何帮助您管理程序中实现的业务逻辑开始。然后,您将了解什么是基于事件的编程,以及函数式编程如何帮助您处理管理传入事件的复杂性,并保持数据流的畅通。最后,您将快速浏览一下异步编程,并了解为什么函数式编程在该领域也是一个有用的工具。

管理业务逻辑

大多数(有用的)程序执行大量不同的操作,从与数据库对话到生成漂亮的界面屏幕等等。但是定义一个程序做什么的关键是它的“业务逻辑”的实现业务逻辑(有时称为领域逻辑)是将现实世界的业务规则编码成代码的程序的一部分。例如,在会计程序中,业务规则包括如何加、减和舍入货币值;如何处理销售税和折旧;货币之间如何换算;以及如何将资金从一个账户或分类账转移到另一个账户或分类账。实现这些规则的 PHP(或其他语言)代码就是业务逻辑。诸如创建用户界面、生成 PDF 报告等功能通常不被认为是业务逻辑,尽管在特别重要的业务规则(如舍入精度、监管机构指定的输出格式等)中它们可能是业务逻辑。)都有涉及。虽然它包含单词 business,但是业务逻辑不仅仅适用于商业或金融应用。以 Photoshop 这样的程序为例。Photoshop 中有很多代码处理加载和保存图像、创建用户界面、响应用户鼠标和键盘输入等等。然而,Photoshop 中的业务逻辑由应用变换、滤镜、绘画和工具操作等的算法组成。这些是艺术家的“商业”任务,当你反转图像或应用抖动时,艺术家会有“规则”(或期望)。一个软件的书面规范通常从软件需要实现的业务规则开始。

与其他代码相比,业务逻辑有一些特殊的需求。

  • 业务逻辑需要是可测试的:如果你的会计网站不小心使用了 Arial 字体而不是 Verdana,这不太可能是什么大问题。但如果它将数百万笔交易四舍五入到最接近的整数,而不是整数,有人就会被解雇。测试代码会带来开销,如果这是一个问题,那么识别关键的业务逻辑来集中有限的测试时间通常是一个聪明的举动。使业务逻辑易于测试,可以最大限度地利用有限的测试资源。
  • 业务逻辑需要集中起来,封装成小的单一用途的代码段:以增值税和商品及服务税等销售税为例,根据所讨论的产品和买卖双方的所在地,销售税可以有不同的百分比。如果英国政府决定将欧盟公民的计算机图书增值税从 0%提高到 10 %,但不包括非欧盟公民,图书零售商将需要确保新税率的计算及其在网站产品页面、购物篮页面、结账页面、电子邮件确认、卡处理系统和后端会计系统上的正确应用。在一个位置指定税率,并且具有确定购买者位置和产品类型的单一目的的集中功能,这确保了变化同时反映在系统的所有部分中,并且确保了对不相关的代码(或税率等)产生意外影响的机会。)被最小化。
  • 业务逻辑需要清楚地映射到现实世界的业务规则上,以便清楚地了解它们之间的相互关系:如果您能够阅读英语业务规则,并同时遵循代码实现,那么就更容易验证它们是否都被实现了。
  • 业务逻辑需要对失败具有弹性:如果网站无法从用户设置中加载用户喜欢的文本颜色,默认的黑色文本通常只会让那些希望使用柔和的炭灰色字体的用户感到些许烦恼。如果同一个网站未能加载其电子商务页面的税率数组,对所有销售应用零百分比税将会使税务部门非常不高兴。

函数式编程,正如我希望你现在已经感受到的,可以帮助你管理你的业务逻辑并满足这些需求。倒过来看前面三点,下面是它的作用:

  • 消除副作用可以提高对某些类型的失败的恢复能力,使用像 Maybe monad 这样的结构可以帮助你处理其他问题。
  • 我谈到函数式编程是一种“声明性”的编码风格。这意味着你以一种描述你正在做“什么”的方式编码,而不是描述你正在“如何”做(至少在更高层次的组合功能上)。这种声明式风格使得将现实世界的业务规则与其对应的代码相匹配变得更加容易,并且可以同时关注两者。
  • 正如您所看到的,函数式编程鼓励您将代码分解成单一用途的函数,然后将函数链和部分函数构建成易于阅读(和推理)的代码块。这鼓励代码重用,并且在函数中封装值(例如,税率)有助于鼓励不变性。
  • 函数式编程使测试(如单元测试)变得容易。

因此,即使您的整个程序不是以函数式风格编写的,识别您的关键业务逻辑并使用函数式编程实现它仍然可以给您带来好处。我将在下一章进一步讨论将其他编程范例与函数式代码混合和匹配。但是现在让我们看一个简单的例子,将关键业务逻辑封装在纯函数中。

下面的例子实现了一个假想的电子商务网站所使用的一些(非常简化的)关键财务逻辑。它分为三个文件:business_data.phpbusiness_logic.phpshopping.php。第一个和第二个包含您的集中式业务逻辑。您可能想知道为什么要在business_data.php文件中创建函数来返回数据(产品价格、税率等)。)而不仅仅是提供静态数组或变量(假设这是函数返回的内容)。将它们创建为函数使您可以在以后灵活地用函数替换它们,例如,根据更复杂的公式调整数据,或者从其他函数或源生成或收集数据。“但是你可以运行这样一个函数并用输出替换变量/数组,”你哭着说,但是这将阻止你使用诸如生成器(如果你不熟悉的话,见第五章)或其他类似的可迭代结构的宝石。使用数据的代码已经设置为调用函数来获取数据,所以您只需要在将来修改函数的实现(当然,除非您需要添加新的参数)。

当您浏览这些函数时,您会注意到您已经将它们中的大部分实现为闭包,函数“use”调用其他函数。像这样使用闭包的一个优点是,它有助于保持所用函数的不变性。例如,如果在您的调用代码中,您意外地将另一个函数(例如,总是返回零)赋给了$get_tax_rate,已经创建的使用$get_tax_rate的闭包将不会受到影响,因为闭包“关闭”或封装了在闭包创建时而不是执行时赋给$get_tax_rate的“值”(函数)。

为了简化示例,我将省略任何错误检查或清理代码(与本书中的大多数示例一样),但是在现实世界中,在开始处理数据之前,通常值得使用“guard”函数来检查来自经过仔细测试的完全纯业务逻辑内部的有效数据。像 Maybe monad 这样的结构也可以用来处理常见的故障模式。

所以,事不宜迟,让我们看看代码。第一个文件如清单 6-1 所示,包含一些业务逻辑数据。

<?php

# First let's create core business data.

# Rather than just define arrays, we're going to create functions
# that return arrays. We'll discuss why in the chapter.

# Every sale is either local, within our own country, or beyond

$locations = function () {
  return ['local', 'country', 'global'];
};

# Each category of products that we sell has a different tax rate,
# and that rate varies depending on where our purchaser is located

$rates = function () {
  return [
     'clothes' => ['local' => 0, 'country' => 5, 'global' => 10],
     'books' => ['local' => 0, 'country' => 5, 'global' => 5],
     'cheeses' => ['local' => 20, 'country' => 17.5, 'global' =>2]
  ];
};

# A list of our products, with their category and price

$products = function () {
  return [

     'T-shirt' => [ 'Category' => 'clothes', 'Price' => 15.99 ],
     'Shorts'  => ['Category' => 'clothes', 'Price' => 9.99 ],
     'The Dictionary'  => ['Category' => 'books', 'Price' => 4.99 ],
     'War and Peace' => ['Category' => 'books', 'Price' => 29.45 ],
     'Camembert'  => ['Category' => 'cheeses', 'Price' => 3.50 ],
     'Brie' => ['Category' => 'cheeses', 'Price' => 7.00 ]

  ];
};

# We only sell in dollars, but we format the prices differently
# depending on the location of the purchaser.

$price_formats = function () {
  return [
    'local' => ['symbol' => '$', 'separator' => '.'],
    'country' => ['symbol' => '$', 'separator' => '.'],
    'global' => ['symbol' => 'USD ', 'separator' => ',']
  ];
};

Listing 6-1.
business_data.php

清单 6-2 中显示的下一个文件包含一些关键的业务逻辑功能。

<?php

# Now we're going to create a set of functions which describe our business
# logic. We're going to keep them as simple as possible, and reference
# other functions within this file where possible to keep a
# "single source of truth" for when we need to update them.

# Load our business data

require('business_data.php');

# Fetch the details of a single product from the list of products

$get_product_details = function ($product) use ($products) {

  return  $products()[$product];

};

# Get the category name from the details of a single product

$get_category = function ($product_details)  {

  return $product_details['Category'];

};

# Get the tax rate for a category of products based on the location
# of the purchaser

$get_tax_rate = function ($category, $location) use ($rates) {

  return $rates()[$category][$location];

};

# Get the net (tax exclusive) price of a product by name.

$get_net_price = function ($product) use ($get_product_details) {

  return $get_product_details($product)["Price"];

};

# Roll the above functions together to create a function that gets
# the gross (tax inclusive) price for a certain quantity of products
# based on the location of our purchaser.
# Note that the tax is rounded using the PHP_ROUND_HALF_DOWN constant
# to indicate the particular rounding method.

$get_gross_price = function ($product, $quantity, $location) use
    ($get_net_price, $get_tax_rate, $get_category, $get_product_details)   {

        return round(
                      $get_net_price($product) *
                      $quantity *
                      ( 1 + (
                              $get_tax_rate(
                                $get_category(
                                  $get_product_details($product)
                                ),
                                $location)
                               /100
                             )
                      ),
                      2, PHP_ROUND_HALF_DOWN) ;

};

# A function to get the actual amount of tax charged. Note that this doesn't
# simply use the tax rate, as the actual amount charged may differ depending on
# the rounding performed and any future logic added to $get_gross_price.
# Instead we call $get_net_price and $get_gross_price and return the difference.

$get_tax_charged = function ($product, $quantity, $location) use
                            ($get_gross_price, $get_net_price) {

  return $get_gross_price($product, $quantity, $location) -
          ( $quantity * $get_net_price($product) );

};

# Finally, a function to format a string to display the price, based
# on the purchasers location.

$format_price = function ($price, $location) use ($price_formats) {

  $format = $price_formats()[$location];

  return $format["symbol"] . str_replace('.',
                                         $format["separator"],
                                         (string) $price
                                         );
};

Listing 6-2.
business_logic.php

最后,清单 6-3 展示了一组使用业务逻辑的常见业务任务。实际上,这些可能被分割到许多不同的脚本和系统上,尽管它们都“需要”相同的业务逻辑脚本。

<?php

# Import our set of pure functions which encapsulate our business logic.

require('business_logic.php');

# Now we can use them in our not so pure, not so functional code, safe in the
# knowledge that they (should) provide us with consistent, correct results
# regardless of what we do to the global or external state here.

# Let's generate a shopping cart of products for a user in Bolivia

$cart = ['Brie' => 3, 'Shorts' => 1, 'The Dictionary' => 2 ];
$user = ["location" => 'global'];

# One common function is to list the contents of the cart. Let's do
# that here

echo "Your shopping cart contains :\n\n";

echo "Item - Quantity - Net Price Each - Total Price inc. Tax\n";
echo "=======================================================\n\n";

foreach ($cart as $product => $quantity) {

  $net_price = $get_net_price($product);

  $total = $get_gross_price($product, $quantity, $user["location"]);

  echo "$product - $quantity - $net_price - $total \n";

};
echo "=======================================================\n\n";

# In a confirmation e-mail we may want to just list a (formatted) total price...

$total_price = array_reduce(  array_keys($cart),

                  # loop through the cart and add gross price for each item

                  function ($running_total, $product) use
                  ( $user, $get_gross_price, $cart ) {

                      return $running_total +
                             $get_gross_price( $product,
                                              $cart[$product],
                                              $user["location"]);
}, 0);

echo "Thank you for your order.\n";
echo $format_price($total_price, $user["location"]).' will ';
echo "be charged to your card when your order is dispatched.\n\n";

# And on the backend system we may have a routine that keeps details of
# all the tax charged, ready to send to the Government. Let's create a
# summary of the tax for this order.

$tax_summary = array_reduce( array_keys($cart),

    # Loop through each item and add the tax charged to the relevant category

    function ($taxes, $product) use
    ( $user, $get_tax_charged, $cart, $get_category, $get_product_details ) {

          $category = $get_category($get_product_details($product));

          $tax = $get_tax_charged($product, $cart[$product], $user["location"]);

          isset($taxes[$category]) ?
                    $taxes[$category] =+ $tax : $taxes[$category] = $tax;

          return $taxes;

}, []);

echo "Tax Summary for this order :\n\n";

var_dump($tax_summary);

Listing 6-3.
shopping.php

清单 6-4 显示了输出。

Your shopping cart contains :

Item - Quantity - Net Price Each - Total Price inc. Tax
=======================================================

Brie - 3 - 7 - 21.42
Shorts - 1 - 9.99 - 10.99
The Dictionary - 2 - 4.99 - 10.48
=======================================================

Thank you for your order.
USD 42,89 will be charged to your card when your order is dispatched.

Tax Summary for this order :

array(3) {
  ["cheeses"]=>
  float(0.42)
  ["clothes"]=>
  float(1)
  ["books"]=>
  float(0.5)
}

Listing 6-4.shopping-output.txt

像这样构造您的业务逻辑也使得扩展它变得更容易。这个例子是一个虚构的美国零售商(从货币符号可以看出!)从一个网站向全世界销售。然而,在做了一些市场调查后,零售商发现它可以通过制作一个专门的欧洲网站来增加其欧洲销售额,推动布里干酪(针对喜欢奶酪的法国人)和字典(因为英国人不会说话)的销售。鉴于新网站上的所有交易都将遵循“全球”税率,他们可以通过创建一个名为$get_eu_tax_rate的部分函数来简化代码,将位置固定为global。像这样扩展逻辑,而不是重写它,意味着他们仍然可以与使用这种通用业务逻辑的现有后端系统紧密集成,当他们在美国扩展奶酪范围以包括 Monterey Jack 时,很容易向欧洲人宣传它是现代奶酪汉堡的装饰。

基于事件的编程

当您编写基于 PHP 的网页时,web 服务器会调用您的脚本来响应用户的请求。这些请求是事件,你永远不知道它们什么时候来,什么页面会被请求,以什么顺序,来自哪个用户。您编写脚本来处理单个特定的页面请求和 web 服务器(Apache、Nginx 等)。)负责管理所有的输入事件,为每个事件调用相关的脚本,并将输出返回给正确的浏览器。您的脚本只“看到”它被调用的当前请求,而不需要(例如)计算出它需要将输出发送回潜在的许多并发访问者中的哪一个。它不需要将每个脚本的状态与其他脚本(或同一脚本的实例)分开,这些脚本可能在它自己执行之前、期间或之后被调用。您的脚本以一种简单的“自顶向下”的过程方式运行,在事件/请求被处理之后,您的脚本就完成了。在这个场景中,web 服务器处理基于事件的编程问题,您的生活很简单!

PHP,作为一种通用的语言,确实让你可以走出这个舒适区,编写你自己的基于事件的脚本。您可以编写长时间运行的脚本,以类似于 Apache 等软件的方式对一组正在发生的事件做出反应并进行处理。有许多方法可以做到这一点,但是在 PHP 中开始基于事件编程的最简单的方法之一是使用 PECL 的“event”扩展。这个扩展是成熟的跨平台libevent库的包装器。在其核心,libevent提供了一种将回调函数附加到事件的方法。它可以响应的事件包括信号、文件和网络事件、超时以及任何可以构建在这些基本类型之上的事件。libevent本机支持的并且 PHP 事件扩展完全包装的事件类型之一是 HTTP 事件。您将使用 HTTP events 编写一个简单的 web 服务器,您将使用它来执行一些数学函数,这样您就可以看到一种管理事件的函数方法,当然还可以像任何优秀的 web 服务器一样提供可爱的猫图片。

那么,为什么要用函数式编程来编写基于事件的程序呢?前面的描述概述了为什么基于事件的编程很难。您不知道您的回调函数将以什么顺序被调用(即,事件将以什么顺序到达您的程序),您需要将来自不同用户的事件分开,但是您仍然需要管理适用于每个用户的状态,这可能会跨越多个事件。您需要在同一个长时间运行的脚本中完成所有这些工作(而不是在每个事件后终止的独立实例)。我相信你能想象为什么用全局或外部状态/变量来管理这样的状态转换会很快变成一个混乱的噩梦来保持你的数据。相比之下,函数式编程教你从一个函数调用到另一个函数调用“沿着链”传递状态,避免使用可变或不纯的外部数据。你的(函数式编程)函数不需要去猜测它们正在处理谁的请求/事件,或者用户当前会话的状态是什么,所有这些信息都作为它们的输入参数传递给它们。对于 HTTP 请求,您可以通过在 HTML 输出中对函数的返回值进行编码,并通过每个 HTTP 请求提供的 URI 参数将这些值作为输入参数接收回来,从而在一系列 HTTP 请求中携带这样的信息。即使事先不清楚哪些函数将以何种顺序被调用,拥有小的、单一用途的函数也可以更容易地推断出程序将做什么。

有两种 PECL 扩展可用于包装libevent:名副其实的libevent扩展和更新的event扩展。我建议使用后者,只是因为它比libevent扩展更全面,并且仍在积极维护中。要使用event扩展,你需要用 PECL 安装它,但是首先你需要在你的系统上安装libevent和它的头文件。清单 6-5 中的安装步骤适用于基于 Debian/Ubuntu 的操作系统;其他操作系统的说明可以在下面的“进一步阅读”部分找到。

# Install the libevent library and it header files

sudo apt-get install libevent-2.0-5 libevent-dev

# Ensure that PECL (which comes as part of the PEAR package)
# and the phpize command which PECL needs are installed

sudo apt-get install php-pear php-dev

# Install the event extension

sudo pecl install event

# Finally make the extension available to the PHP CLI binary
# by editing php.ini

sudo nano /etc/php/7.0/cli/php.ini

# and adding the following line in the section where other .so
# extensions are include

extension=event.so

Listing 6-5.
install_event.txt

进一步阅读

现在您已经安装了libeventevent扩展,您可以编写一个程序来充当 HTTP 服务器并处理传入的 HTTP 请求事件。您将把它分成两个脚本,一个包含您的业务逻辑函数,另一个设置服务器并将函数作为回调连接到传入的请求事件。您将创建一些简单的数学函数(如add()subtract()),它们被映射到 URIs(如/add/subtract),对一个值进行操作,该值从一个请求传递到另一个请求。清单 6-6 展示了你的函数的脚本。

<?php

# We'll create a set of functions that implement the logic that should
# occur in response to the events that we'll handle.

# Use our trusty partial function generator

require('../Chapter 3/partial_generator.php');

# A generic function to output an HTTP header. $req is an object representing
# the current HTTP request, which ensures that our function deals with the
# right request at all times.

$header = function ($name, $value, $req) {

    $req->addHeader ( $name , $value, EventHttpRequest::OUTPUT_HEADER );

};

# We are going to be serving different types of content (html, images etc.)
# so we need to output a content header each time. Let's create a
# partial function based on $header...

$content_header = partial($header, 'Content-Type' );

# and then make it specific for each type of content...

$image_header = partial($content_header, "image/jpeg");

$text_header  = partial($content_header, "text/plain; charset=ISO-8859-1");

$html_header = partial($content_header, "text/html; charset=utf-8");

# The following function creates a "buffer" to hold our $content and
# then sends it to the browser along with an appropriate HTTP status
# code (Let's assume our requests always work fine so send 200 for everything).
# Note that it's a pure function right up until we call sendReply. You could
# return the EventBuffer instead, and wrap it all into an IO or Writer monad to
# put the impure sendReply at the end if you wish.

$send_content = function($req, $content) {

    $output = new EventBuffer;

  $output->add($content);

  $req->sendReply(200, "OK", $output);

};

# The input parameters for our maths functions are held in the URI parameters.
# The URI is held in the $req request object as a string. Let's get the
# URI and parse out the parameters into an associative array.

$parse_uri_params = function ($req) {

    $uri = $req->getUri();

    parse_str(

        # Grab just the parameters (everything after the ?)

        substr( $uri, strpos( $uri, '?' ) + 1 ),

        # and parse it into $params array

        $params);

    return $params;

};

# Get the URI "value" parameter

$current_value = function($req) use ($parse_uri_params) {

    return $parse_uri_params($req)["value"];

};

# Get the URL "amount" parameter

$amount = function($req) use ($parse_uri_params) {

    return $parse_uri_params($req)["amount"];

};

# A function to send the results of one of our maths functions which follow.

$send_sum_results = function($req, $result) use ($html_header, $send_content) {

  # Create some HTML output, with the current result, plus some links
    # to perform more maths functions. Note the uri parameters contain
    # all of the state needed for the function to give a deterministic,
    # reproducable result each time. We also include some links to
    # the other utility functions. When you visit them, note that you
    # can use your browser back button to come back to the maths functions
    # and carry on where you left off, as the parameters the functions
    # need are provided by the URI parameters and no "state" has been
    # altered of lost

    $output = <<<ENDCONTENT

    <p><b>The current value is : $result</b></p>

    <p><a href="/add?value=$result&amount=3">Add 3</a></p>
    <p><a href="/add?value=$result&amount=13">Add 13</a></p>
    <p><a href="/add?value=$result&amount=50">Add 50</a></p>
    <p><a href="/subtract?value=$result&amount=2">Subtract 2</a></p>
    <p><a href="/subtract?value=$result&amount=5">Subtract 5</a></p>
    <p><a href="/multiply?value=$result&amount=2">Multiply by 2</a></p>
    <p><a href="/multiply?value=$result&amount=4">Multiply by 4</a></p>
    <p><a href="/divide?value=$result&amount=2">Divide by 2</a></p>
    <p><a href="/divide?value=$result&amount=3">Divide by 3</a></p>
    <p><a href="/floor?value=$result">Floor</a></p>

    <p><A href="/show_headers">[Show headers]</a>&nbsp;
    <a href="/really/cute">[Get cat]</a>&nbsp;
    <a href="/close_server">[Close down server]</a></p>

ENDCONTENT;

  # Send the content header and content.

    $html_header($req);

    $send_content($req, $output);

};

# These are our key maths functions. Each one operates like a good Functional
# function by only using the values supplied as input parameters, in this
# case as part of $req. We call a couple of helper functions ($current_value
# and $amount) to help extract those values, $req isn't necessarily
# immutable (we could alter values or call methods), but we'll use
# our discipline to keep it so right up until we're ready to send_contents.
# While we don't formally "return" a value, $send_sum_results effectively
# acts a return statement for us. Any return value would simply go back to
# libevent (which is the caller, and it just ignore it).
# If we want to keep to strictly using explicit return statements, we could
# wrap this in another function that does the same as $send_sum_results, (and
# for the same reason wouldn't have a return statement) or we could create an
# Writer monad or similar to gather the results and only output to the browser
# at the end. For this simple example we'll go with using $send_sum_results
# though for simplicity and clarity.

$add = function ($req) use ($send_sum_results, $current_value, $amount) {

  $send_sum_results($req, $current_value($req) + $amount($req) );

};

$subtract = function ($req) use ($send_sum_results, $current_value, $amount) {

  $send_sum_results($req, $current_value($req) - $amount($req) );

};

$multiply = function ($req) use ($send_sum_results, $current_value, $amount) {

  $send_sum_results($req, $current_value($req) * $amount($req) );

};

$divide = function ($req) use ($send_sum_results, $current_value, $amount) {

  $send_sum_results($req, $current_value($req) / $amount($req) );

};

$floor = function ($req) use ($send_sum_results, $current_value) {

  $send_sum_results($req, floor($current_value($req)) );

};

# Now we'll define some utility functions

# Grab the HTTP headers from the current request and return them as an array

$get_input_headers = function ($req) {

    return $req->getInputHeaders();

};

# A recursive function to loop through an array of headers and return
# an HTML formatted string

$format_headers = function ($headers, $output = '') use (&$format_headers) {

    # if we've done all the headers, return the $output
    if (!$headers) {

        return $output;

    } else {

        # else grab a header off the top of the array, add it to the
        # $output and recursively call this function on the remaining headers.

        $output .= '<pre>'.array_shift($headers).'</pre>';

        return $format_headers($headers, $output);

    };

};

# Use the function above to format the headers of the current request for
# viewing

$show_headers = function ($req) use ($html_header, $send_content, $format_headers) {

    $html_header($req);

    $send_content($req, $format_headers( $req->getInputHeaders() ) );
};

# Let's handle all requests, so there are no 404's

$default_handler = function ($req) use ($html_header, $send_content) {

    $html_header($req);

    $output = '<h1>This is the default response</h1>';

    $output .= '<p>Why not try <a href="/add?value=0&amount=0">some math</a></p>';

    $send_content($req, $output);

};

# Ensure that there are sufficient supplies of cat pictures available
# in all corners of the Internet

$send_cat = function($req) use ($image_header, $send_content) {

    # Note we send a different header so that the browser knows
    # a binary image is coming

    $image_header($req);

    # An impure function, you could alway use an IO monad or

    $send_content($req, file_get_contents('cat.jpg'));
};

# A function to shut down the web server script by visiting a particular URI.

$close_server = function($req, $base) use ($html_header, $send_content) {

    $html_header($req);

    $send_content($req, '<h1>Server is now shutting down</h1>');

    $base->exit();

};

Listing 6-6.
server_functions.php

清单 6-7 显示了您的脚本,它实际上运行 HTTP 服务器并将早期的函数连接到 URIs。

<?php

# Let's get all of our functions that implement our
# business logic

require('server_functions.php');

# Now we're ready to build up our event framework

# First we create an "EventBase", which is libevent's vehicle for holding
# and polling a set of events.

$base = new EventBase();

# Then we add an EventHttp object to the base, which is the Event
# extension's helper for HTTP connections/events.

$http = new EventHttp($base);

# We'll choose to respond to just GET  HTTP requests

$http->setAllowedMethods( EventHttpRequest::CMD_GET );

# Next we'll tie our functions we created above to specific URIs using
# function callbacks. We've created them all as anonymous/closure functions
# and so we just bind the variable holding them to the URI. We
# could use named functions if we want, suppling the name in "quotes".
# with the EventHttpRequest object representing the current request as
# the first paramter. If you need other parameters here for your callback,
# you can specify them as an optional third parameter below.

# Our set of maths functions...

$http->setCallback("/add", $add);

$http->setCallback("/subtract", $subtract);

$http->setCallback("/multiply", $multiply);

$http->setCallback("/divide", $divide);

$http->setCallback("/floor", $floor);

# A function to shut down the server, which needs access to the server $base

$http->setCallback("/close_server", $close_server, $base);

# A utility function to explore the headers your browser is sending

$http->setCallback("/show_headers", $show_headers);

# And a compulsory function for all internet connected devices

$http->setCallback("/really/cute", $send_cat);

# Finally we'll add a default function callback to handle all other URIs.
# You could, in fact, just specify this default handler and not those
# above, and then handle URIs as you wish from inside this function using
# it as a router function.

$http->setDefaultCallback($default_handler);

# We'll bind our script to an address and port to enable it to listen for
# connections. In this case, 0.0.0.0 will bind it to the localhost, and
# we'll choose port 12345

$http->bind("0.0.0.0", 12345);

# Then we start our event loop using the loop() function of our base. Our
# script will remain in this loop indefinitely, servicing http requests
# with the functions above, until we exit it by killing the script or,
# more ideally, calling $base->exit() as we do in the close_server()
# function above.

$base->loop();

# We'll only hit this point in the script if some code has called
# $base->exit();

echo "Server has been gracefully closed\n";

Listing 6-7.
web_server.php

要启动 HTTP 服务器,只需在命令行输入php web_server.php。您现在可以在 web 浏览器中访问http://localhost:12345,您将看到默认的响应。单击链接开始使用一些数学函数,并访问实用函数的链接。现在,尝试打开另一个浏览器标签(或者实际上是另一个浏览器)并访问同一个 URL 试着点击一些数学函数和我的猫的可爱照片。在浏览器标签/浏览器之间切换,检查每个页面是否保持正确的状态,不管你在其他标签/浏览器中做什么。因为您将状态作为输入/URL 参数和 HTML 输出中的“返回值”来传递,所以您的状态遵循每个单独的事件流,并且您不需要明确地跟踪用户及其各自的状态。

现在,当然,这是一个玩具问题来说明这个想法,但理想情况下,您可以看到函数式编程中固有的属性如何帮助消除在编写基于事件的程序时跟踪状态的复杂方法的需要。这些不必是网络服务器;任何基于事件的模型(例如,响应系统事件或文件系统变化的程序)都可以受益于函数式编程。

异步 PHP

异步(async)编程是一种编写代码的方式,通过在等待外部 I/O(如数据库调用和磁盘 I/O)完成的同时执行代码,在单线程应用(如 PHP 脚本)中充分利用处理器。由于函数式编程非常适合基于事件的编程的原因,它也是管理异步编程中固有的无序处理的相关复杂性的一个很好的选择。PHP 本身并不支持异步编程,所以我不会在本书中涉及它,但是有几个库确实实现了异步功能,下面将详细介绍。这些库或多或少局限于异步运行 I/O 类型的函数,而不是任意代码(这需要多任务或多线程能力)。脸书的 Hack 语言是 PHP 的一个扩展但基本兼容的实现,具有本机 I/O 异步功能(见下面的链接),但这些功能与主 PHP VM 或任何 PHP 库都不兼容。无论您选择哪种方法,使用本书中概述的功能原则将有助于您对代码的执行进行推理。

进一步阅读

七、在面向对象和过程式应用中使用函数式编程

到目前为止,在本书中,您已经了解了什么是函数式编程的基础知识,以及如何使用它来解决一些常见的编程问题。在这一章中,你将会看到如何把功能代码放到你现有的或者新的应用中。具体来说,您将看到以下内容:

  • 如何构建一个功能性的应用,以及是否让它完全发挥作用
  • 如何以及何时混合和匹配像函数式编程和面向对象编程这样的范例

在本章结束时,我希望你有信心开始探索甚至在你自己的应用中实现基本的功能策略。

PHP 范例的历史

要理解函数式编程在应用中的位置,通常理解它在 PHP 中的位置是很方便的。谈到编程范例,PHP 是一个大杂烩,这既是一件好事,也是一件坏事。为了理解 PHP 是如何发展成现在这个样子的,你首先需要了解它的历史。早在 1994 年,当拉斯马斯·勒德尔夫第一次创建 PHP 时,它是一种纯粹的过程语言。随着时间的推移,PHP 变得越来越普及,人们开始将它用于越来越大的基于 web 的系统。更大的系统,特别是那些有开发团队而不是单个编码人员的系统,需要不断增加的代码纪律,因此出现了需要面向对象语言特性的学者和专业编码人员。面向对象特性的交付始于版本 3;然而,与此同时,在过程方面有一个更强大和一致的语言语法的推动,所以版本 3 提供了两者。

在那个阶段,基本的 OO 特性被许多人嘲笑(特别是那些来自其他语言如 Java 和 C++的特性),这阻止了许多过程式 PHP 程序员尝试 OOP 功能。直到版本 5,随着 Zend Engine 2 中新的对象模型的实现,才出现了合适的 OO 特性。即使在那时,与对象相关的语法和特性支持也没有达到您当前所拥有的那种程度,直到版本 5 系列的后续版本。这一点,再加上 PHP 开发人员(完全合理地)不愿意打破向后兼容性,意味着 PHP 的过程式特性和它新的 OO 特性一起得到了很好的磨砺。

如果你看一下 PHP 手册中的“PHP 历史”一页,你不会发现任何地方提到函数式编程。函数式编程,作为一个概念,从来都不是 PHP 正式开发路线图的一部分。事实上,你可以用 PHP 进行(某种形式的)函数式编程,这要感谢那些发现了其他语言中的函数式编程元素(比如闭包)并把它们带到 PHP 中来为过程模型和面向对象模型增添趣味的人。

所有这些都意味着你可以在 PHP 中挑选你的编程范式,而 PHP 很少阻止你将它们混合在一起。本书远非函数式编程的啦啦队长,而是旨在强调每种范式的优点和缺点。为此,“进一步阅读”一节将更详细地介绍每种方法的缺点。

进一步阅读

PHP 不是函数式语言

PHP 不是函数式语言。让我再说一遍,以防不清楚:PHP 不是函数式语言。当然,你可以用 PHP 进行函数式编程。如果你不能,这将是一本非常短的书。但是 PHP 不是函数式语言。PHP 是一种非常通用的、几乎与范式无关的通用编程语言。但是它仍然不是一种函数式语言。

您可以使用本书中介绍的功能代码来编写整个程序、应用或系统。但是如果你这样做了,你将会错过以下几点:

  • 访问用 OO 或过程 PHP 编写的库和其他第三方代码
  • 访问您自己现有的不起作用的 PHP 代码
  • 对 I/O 的自由访问(PHP 让这变得如此简单)
  • 来自你周围其他程序员的支持和理解(除非他们也热衷于函数式编程!)

为了更好地理解 I/O,您可以使用 IO 或 Writer monad 以函数方式处理 I/O,但这通常意味着将脚本执行推到脚本的末尾,虽然函数脚本通常更容易推理和理解,但大多数人发现 monad 的代码流正好相反。对于第三方库,您可能会发现它们有您在函数式风格中使用的很好的函数或对象方法,但是(除非您仔细检查并可能重写它们的代码),您不能确保这些函数的实现遵守了函数原则,如不变性和引用透明性。

我的建议?按照你认为合适的方式混合搭配范例。函数式编程只是你工具箱中的另一个工具;在它看起来能为手头的问题提供解决方案的地方和时间使用它。也就是说,不要走极端!保持事情简单明了的最好方法是将您的代码分成功能性和非功能性代码块。一个显而易见的安排是将你的业务逻辑、高性能算法等写在功能代码中,将它们夹在不纯的面向对象或过程代码之间来处理输入和输出,如图 7-1 所示。

A447397_1_En_7_Fig1_HTML.jpg

图 7-1。

Example of mixed architecture

有了这样的安排,您就有了清晰的功能代码块来进行推理和测试,并且当问题确实发生时,更容易找出它们可能位于何处。当你面临使用函数式技术更新现有的代码库时,一个好的方法是首先识别出恰好适合图 7-1 中间的代码部分,并优先考虑它们。

这在理论上是好的,但是在现实世界中事情经常变得更加混乱,你的代码范例开始重叠。在接下来的几节中,您将看到在将面向对象和过程性代码与纯函数性代码混合时的一些潜在问题。

Aside

不要在 PHP 中使用单子。真的,PHP 里不要用单子。单子是由纯函数式代码推广的,这些代码没有其他方法来处理像 I/O 这样可能不纯的操作。PHP 不是纯函数式语言,还有很多其他方法可以最小化 I/O 变坏的有害影响。它们可能看起来像一个巧妙的技巧,当你最终“得到”单子时的幸福感是美好的,当你的代码最终通过测试并且正确的值从你的脚本中慢慢流出时的多巴胺热潮是值得珍惜的。有时候你会意外地写出类似单子的代码,或者恰好符合单子定律的代码。只要那种风格是你正常编码风格的一部分,或者是一种必要代码结构的偶然特征,那就可以了。但是,在代码更改时为了单子而保持单子是一件令人头痛的事情,初级开发人员会看着你的代码开始哭泣,而你的半懂技术的老板会看着你的代码说,“回到纯 OOP 吧,大家。”把它们当作玩具,让同性或异性成员惊叹的东西,用于学术研究,或者吓唬实习生,但是看在上帝的份上,不要让它们出现在生产代码中。我知道我已经向你们介绍了它们,向你们展示了它们是如何工作的,并且慷慨地提到了它们。把这想象成一个家长和他们的孩子谈论毒品:你需要确保你的孩子了解他们,从你而不是他们的“朋友”那里获得关于他们的事实,并理解他们。但是你不想让他们用。

总结一下,不要在 PHP 中使用单子。

对象和可变性

当您将一个值传递给一个函数时,您不希望该函数改变原始值。如果是这样的话,它将会使任何依赖于该值的代码变得更加难以推理。当你传递对象时,这说起来容易做起来难。

PHP 中的对象实际上并不存储在变量中。当你使用类似于$my_object = new some_class();的代码创建一个新对象时,这个对象实际上并没有被赋给变量。相反,对象标识符被分配给变量,这允许访问代码来定位实际的对象。这意味着,如果你将一个对象变量作为参数传递给一个函数,你传递的值是对象标识符,而不是对象本身。当函数内部的代码使用该标识符时,它是在处理原始对象,而不是副本(标识符是一个副本,但却是指向原始对象的忠实副本)。让我们来看一个例子(参见清单 7-1 和清单 7-2 )。

<?php

# Create a class to encapsulate a value

class my_class
{

        # The value we want to encapsulate

    private $value = 0;

        # Constructor to set the value (or default to -1)

        public function __construct($initial_value = -1) {

            $this->value = $initial_value;

        }

        # Method to get the value

    public function get_value() {

        return $this->value;

    }

        # Method to set the value

        public function set_value($new_value) {

        $this->value = $new_value;

    }
}

# Let's create a new object with a value of 20

$my_object = new my_class(20);

# Check the value

var_dump ($my_object->get_value()); # int(20)

# Demonstrate we can mutate the value to 30

$my_object->set_value(30);

var_dump ($my_object->get_value()); # int (30)

# Now let's create a function which doubles the value
# of the object. Note that the function parameter
# doesn't have a "&" to indicate it's passed by reference

function double_object ($an_object) {

    # Get the value from $an_object, double it and set it back

    $an_object->set_value( $an_object->get_value() * 2 );

    # return the object

    return $an_object;

}

# Now we call the function on our $my_object object from
# above, and assign the returned object to a new variable

$new_object = double_object($my_object);

# Check that the returned object has double the value (30)
# of the object we passed in as a parameter

var_dump( $new_object->get_value() ); # int(60)

# Let's just check the value on the original object

var_dump( $my_object->get_value()); # int(60)

# It's also changed. Let's var_dump the original object
# and returned object, and check their object reference number
# (look for the number after the #)

var_dump ($my_object); # #1

var_dump ($new_object); # #1

# They're both the same. Just for clarity, create a new
# object from scratch and check it's reference number

$last_object = new my_class();

var_dump ($last_object); # #2

Listing 7-1.passing_objects.php

int(20)
int(30)
int(60)
int(60)
object(my_class)#1 (1) {
  ["value":"my_class":private]=>
  int(60)
}
object(my_class)#1 (1) {
  ["value":"my_class":private]=>
  int(60)
}
object(my_class)#2 (1) {
  ["value":"my_class":private]=>
  int(-1)
}
Listing 7-2.passing_objects-output.txt

那么,当你想在一些函数代码中将对象作为参数传递时,你该怎么做呢?有几种选择。第一个很简单:不要做。你真的需要传递整个对象吗?在许多情况下,您可以从对象中获取一个(或两个)值,将其传递到一个组合的函数堆栈中(可以包括您正在使用的类中的方法),然后将函数代码的结果设置回您的对象中。这确保了您的对象在代码的功能部分不会发生变化。清单 7-3 显示了一个简单的例子。

<?php

# use our trusty compose function
include('../Chapter 3/compose.php');

# The same class as before, but with an added static method

class new_class
{

    private $value = 0;

        public function __construct($initial_value = -1) {

            $this->value = $initial_value;

        }

    public function get_value() {

        return $this->value;

    }

        public function set_value($new_value) {

        $this->value = $new_value;

    }

        # a static method to halve the provided value

        public static function halve($value) {

            return $value / 2;

        }

}

# Let's create a new object with an initial value of 25

$my_object = new new_class(73.4);

# Let's stack some math functions together including our
# static method above

$do_math = compose (

                            'acosh',
                            'new_class::halve',
                            'floor'
    );

# Now let's actually do the math. We set the object value
# to the result of $do_math being called on the original value.

$my_object->set_value(

                                            $do_math(

                                                                $my_object->get_value()

                                                                )
                                         );

# Show that our object value has been changed. Note that nothing changed
# while we were in our functional (compose) code.

var_dump ( $my_object->get_value() ); # float(2)

Listing 7-3.
static_methods.php

如果您的对象包含多个函数需要处理的值,您当然可以首先将它们提取到一个数据结构中,比如一个数组。

如果您确实需要将整个对象传递给函数,那么您需要首先克隆它,因为没有直接的方法将对象的内容通过值传递给函数。您将需要确保代码中没有其他内容试图访问这些克隆的对象,并且每个返回对象的函数都需要在出于完全相同的原因返回之前克隆它。清单 7-4 展示了这个过程的机制,输出如清单 7-5 所示。

<?php

# use our trusty compose function

include('../Chapter 3/compose.php');

# The same class as previously

class my_class
{

    private $value = 0;

        public function __construct($initial_value = -1) {

            $this->value = $initial_value;

        }

    public function get_value() {

        return $this->value;

    }

        public function set_value($new_value) {

        $this->value = $new_value;

    }

}

# A function to triple the value of the object

$triple_object = function ($an_object) {

    # First clone it to make sure we don't mutate the object that
    # $an_object refers to

    $cloned_object = clone $an_object;

    # Then set the value to triple the current value

    $cloned_object->set_value( $cloned_object->get_value() * 3 );

    # and return the new object

    return $cloned_object;

};

# A function to multiply the value of the object by Pi.
# Again we clone the object first and return the mutated clone

$multiply_object_by_pi = function ($an_object) {

    $cloned_object = clone $an_object;

    $cloned_object->set_value( $cloned_object->get_value() * pi() );

    return $cloned_object;

};

# Let's create an object encapsulating the value 10.

$my_object = new my_class(10);

# We'll compose the above functions together

$more_math = compose(
                                            $triple_object,
                                            $multiply_object_by_pi,
                                            $triple_object
    );

# and then call that composition on our object.

var_dump ( $more_math($my_object) );

# Let's check our original object remains unchanged

var_dump ($my_object);

Listing 7-4.
clones.php

object(my_class)#4 (1) {
  ["value":"my_class":private]=>
  float(282.74333882308)
}
object(my_class)#3 (1) {
  ["value":"my_class":private]=>
  int(10)
}
Listing 7-5.clones-output.txt

对象#1 和#2 是包含$triple_object and $multiply_object_by_pi函数的闭包对象。对象#3 是原始对象,对象#4 是返回的对象。每个函数中的克隆对象只有在变量$cloned_object引用它们时才存在。函数一返回,$cloned_object(像函数范围内的所有变量一样)就被销毁,PHP 自动删除不再被引用的对象。因此,通过调用$more_math函数创建的对象可以使用#4 标识符(尽管它在var_dump语句后也会被销毁,因为它没有被赋给任何变量)。

正如您从前面的代码中看到的,这样做可能会很麻烦,并且您正在进行的克隆类型是一个“浅层”拷贝,因此根据您的类/对象的结构,会有一些限制。如果您已经为其他目的实现了一个__clone方法,请注意,它也会在这些情况下自动使用。如果对象中的方法访问另一个对象,克隆它不会克隆另一个对象。出于这些原因,我建议您将函数代码与对象分开,至少在涉及值传递时是这样。

最后,值得一提的是 PHP 资源变量(比如文件指针和数据库句柄)和对象一样,只是对外部资源的引用。与对象一样,这意味着当您将它们作为参数传递给函数时,实际的资源没有被复制,只有变量值。您已经了解了为什么像文件这样的资源被认为是外部状态,并且会给函数带来副作用。但是,如果您认为某个特定的资源对于您的特定目的(例如,用于日志记录的输出文件)是足够稳定和有保证的,那么一定要考虑到所指向的资源可能会被脚本的另一部分所改变,因为您的变量指向的是共享资源,而不是您自己的独特副本。

进一步阅读

带有对象的不可变数据

已经了解了为什么对象在某种意义上是天生可变的,现在让我们看看硬币的另一面,看看对象如何帮助您保持其他数据结构不可变。

正如我在本书前面所讨论的,PHP 拥有的唯一真正不可变的构造是常量,使用defineconst声明。不过,也有一些缺点。只有标量和最近的数组可以被定义为常量;对象不能。常量也是全局定义的(像超全局变量一样),而不是在普通的变量范围内定义的,我之前说过要避免这种情况,因为它被认为是无法推理的外部状态(因此是函数的副作用)。在这种情况下,这似乎不是什么大问题,因为每个常数一旦定义就不可改变,所以你可以依赖它的值。然而,考虑一下define语句允许您有条件地创建常数,并根据变量或其他计算值设置它们的值,而不是硬编码的值。这意味着你根本不能依赖有值的常量,更不用说期望值了,所以如果它不是作为函数的参数之一传入的,你就不能可靠地推断出函数的输出。

解决这些问题的一种方法是创建一个类来封装值并强制保持它们不变(或不变异)。这种类的对象可以根据给定的值构造,然后通过避免使用 setters 之类的方法来确保该值在对象的生命周期内不会改变。作为一个普通的对象变量,通常的变量范围规则将适用。前一节中提到的关于传递对象的问题在这里不太适用,因为您有意地最小化了对象的可变性。

因此,让我们来看看创建一个常量数组作为不可变对象的方法(参见清单 7-6 和清单 7-7 )。

<?php

# Create a class to represent an immutable array

# Make the class "final" so that it can't be extended to add
# methods to mutate our array

final class const_array {

  # Our array property, we use a private property to prevent
  # outside access

  private $stored_array;

  # Our constructor is the one and only place that we set the value
  # of our array. We'll use a type hint here to make sure that we're
  # getting an array, as it's the only "way in" to set/change the
  # data, our other methods can be sure they are then only dealing
  # with an array type

  public function __construct(array $an_array) {

    # PHP allows us to call the __construct method of an already created
    # object whenever we want as if it was a normal method. We
    # don't want this, as it would allow our array to be over written
    # with a new one, so we'll throw an exception if it occurs

    if (isset($this->stored_array)) {

        throw new BadMethodCallException(
                    'Constructor called on already created object'
                  );

    };

    # And finally store the array passed in as our immutable array.

    $this->stored_array = $an_array;

  }

  # A function to get the array

  public function get_array() {

          return $this->stored_array;

  }

  # We don't want people to be able to set additional properties on this
  # object, as it de facto mutates it by doing so. So we'll throw an
  # exception if they try to

  public function __set($key,$val) {

    throw new BadMethodCallException(
                'Attempted to set a new property on immutable class.'
              );

  }

  # Likewise, we don't want people to be able to unset properties, so
  # we'll do the same again. As it happens, we don't have any public
  # properties, and the methods above stop the user adding any, so
  # it's redundant in this case, but here for completeness.

  public function __unset($key) {

              throw new BadMethodCallException(
                          'Attempted to unset a property on immutable object.'
                        );

  }

}

# Let's create a normal array

$mutable_array = ["country" => "UK", "currency" => "GBP", "symbol" => "£"];

# and create an const_array object from it

$immutable_array = new const_array($mutable_array);

var_dump ($immutable_array);

# Let's mutate our original array

$mutable_array["currency"] = "EURO";

# our const_array is unaffected

var_dump ($immutable_array);

# We can read the array values like normal

foreach ( $immutable_array->get_array() as $key => $value) {

    echo "Key [$key] is set to value [$value] \n\n";

};

# And use dereferencing to get individual elements

echo "The currency symbol is ". $immutable_array->get_array()["symbol"]."\n\n";

# Need to copy it? Just clone it like any other object, and the methods
# which make it immutable will be cloned too.

$new_array = clone $immutable_array;

var_dump ($new_array);

# The following operations aren't permitted though, and will throw exceptions

# $immutable_array->stored_array = [1,2,3];
#   BadMethodCallException: Attempted to set a new property on immutable class

# $immutable_array->__construct([1,2,3]);
#   BadMethodCallException: Constructor called on already created object

# unset($immutable_array->get_array);
#   BadMethodCallException: Attempted to unset a property on immutable object.

# $immutable_array->new_prop = [1,2,3];
#    BadMethodCallException: Attempted to set a new property on immutable class

# $test = new const_array();
#    TypeError: Argument 1 passed to const_array::__construct()
#    must be of the type array, none given

# class my_mutable_array extends const_array {
#
#   function set_array ($new_array) {
#
#       $this->stored_array = $new_array;
#
#   }
#
# };
#   Fatal error:  Class my_mutable_array may not inherit from final
#   class (const_array)

# Unfortunately, there is no practical way to stop us overwriting the object
# completely, either by unset()ing it or by assigning a new value to the
# object variable, such as by creating a new const_array on it

$immutable_array = new const_array([1,2,3]);

var_dump($immutable_array); # new values stored

Listing 7-6.const_array.php

object(const_array)#1 (1) {
  ["stored_array":"const_array":private]=>
  array(3) {
    ["country"]=>
    string(2) "UK"
    ["currency"]=>
    string(3) "GBP"
    ["symbol"]=>
    string(2) "£"
  }
}
object(const_array)#1 (1) {
  ["stored_array":"const_array":private]=>
  array(3) {
    ["country"]=>
    string(2) "UK"
    ["currency"]=>
    string(3) "GBP"
    ["symbol"]=>
    string(2) "£"
  }
}
Key [country] is set to value [UK]

Key [currency] is set to value [GBP]

Key [symbol] is set to value [£]

The currency symbol is £

object(const_array)#2 (1) {
  ["stored_array":"const_array":private]=>
  array(3) {
    ["country"]=>
    string(2) "UK"
    ["currency"]=>
    string(3) "GBP"
    ["symbol"]=>
    string(2) "£"
  }
}
object(const_array)#3 (1) {
  ["stored_array":"const_array":private]=>
  array(3) {
    [0]=>
    int(1)
    [1]=>
    int(2)
    [2]=>
    int(3)
  }
}

Listing 7-7.const_array-output.txt

你可以在最后看到它并不是完全不可改变的。您可以用新的值或对象完全覆盖对象变量。您可以向该类添加一个__destruct方法,如果对象被破坏(通过取消设置或覆盖),该方法将抛出一个异常,但是这有两个问题。首先,当你的脚本终止时,所有的对象都被销毁了,所以每次脚本运行时都会抛出异常,如果你在其他对象上有其他的析构函数而没有被调用,这可能会是一个问题。第二,正如我前面描述的,对象变量只是对实际对象的引用。这意味着如果你创建一个不可变的对象作为$a,然后做$b = $a,最后做unset($a),你的__destruct方法不会触发,因为实际的对象仍然存在,因为它被$b引用。出于这些原因,在大多数情况下,在__destruct上抛出异常可能并不实用。尽管如此,创建像这样的基本不可变的对象是防止大多数类型的数据结构(包括对象本身)中值的意外变化的一种有用的方法。

作为外部状态的对象属性

对象方法是函数,这很好,你可以在你的函数组合中使用它们。对象也有封装到对象中的属性(值)。封装作为一个概念是好的;您已经看到了它在闭包中的作用,在闭包中,您通过函数上的use子句将值封装到您的作用域中。但是一个对象属性并不等同于一个被拉入闭包函数的值;它只在对象内部,而不是在单个方法内部。当然,私有属性只能由对象中的方法访问,但是对象中的任何方法(私有或公共)都可以访问和更改该属性。实际上,该属性位于类中的“全局”类型范围内,至少就方法而言是如此,因此您应该能够理解为什么在函数式编程中使用它们时会出现问题。下面的例子演示了一个属性如何有效地变成外部状态,当你没有明确地把它作为一个参数传递给你的函数(方法)调用时,你不能总是推断出函数对于一个特定的输入会给出什么样的输出。参见清单 7-8 和清单 7-9 。

<?php

# Get our compose function
require '../Chapter 3/compose.php';

# This class will provide a set of methods to work with tax

class tax_functions {

  # Store the rate of tax

  private $tax_rate;

  # Our constructor sets the tax rate initially

  public function __construct($rate) {

    $this->tax_rate = $rate;

  }

  # Provide a method to set the tax rate at any point

  public function set_rate($rate) {

    $this->tax_rate = $rate;

  }

  # A method to add tax at the $tax_rate to the $amount

  public function add_tax($amount) {

    return $amount * (1 + $this->tax_rate / 100);

  }

  # A method to round the $amount down to the nearest penny

  public function round_to($amount) {

    return floor($amount * 100) / 100;

  }

  # A function to format the $amount for display

  public function display_price($amount) {

    return '£'.$amount.' inc '.$this->tax_rate.'% tax';

  }

}

# So let's create an object for our program containing the
# methods, with the tax rate set at 10%

$funcs = new tax_functions(10);

# Now let's compose our methods into a flow that adds tax, rounds
# the figure and then formats it for display.

# Note that to pass a method of an object as a callable, you need
# to give an array of the object and method name. If you are using
# static class methods, you can use the class::method notation instead

$add_ten_percent = compose (

    [$funcs, 'add_tax'],

    [$funcs, 'round_to'],

    [$funcs, 'display_price']

  );

# We've composed our $add_ten_percent function, but we may not want to use it
# until much later in our script.

# In the mean-time, another programmer inserts the following line in our
# code in between...

$funcs->set_rate(-20);

# and then we try to use our $add_ten_percent function to add
# tax to 19.99, hopefully getting the answer £21.98 inc 10% tax

var_dump( $add_ten_percent(19.99) ); # £15.99 inc -20% tax

Listing 7-8.properties.php

string(20) "£15.99 inc -20% tax"
Listing 7-9.properties-output.txt

如您所见,对象属性可以被视为函数的外部状态。这个例子中的副作用是由某人改变属性值引起的(尽管在这个非常做作和明显的例子中!)在你的函数流之外给了你一个你没有预料到的结果。

那么,你对此能做些什么呢?有几种策略。

  • 不要用属性!将值包装到返回值的函数/方法中。
  • 尽可能使用const将属性声明为常量。
  • 使用静态类方法,而不是实例化的对象;那么就没有需要担心的属性或$this

简而言之,像对待函数外部的任何其他状态一样对待对象属性。

在线杂质

在前面,您看到了如何构建您的程序,以便将有问题的不纯代码段与功能代码段分开。您还在本书的前面看到了单子,它允许您巧妙地分离这些杂质(通常将执行推到代码定义的末尾)。从实用的角度来看,这两种方法都有问题。单子的编写和使用都很复杂。结构化代码将 I/O 从功能代码中分离出来可能是不可取的,例如,当您需要记录数据或更新用户在长时间运行的脚本中的进度时。

一个可能的解决方案(这会让函数纯粹主义者尖叫)是传递型函数。您可以将这些函数组合到正常的函数链中,这些函数执行以下操作:

  • 将前一个函数的返回值作为其参数
  • 做一些不纯洁的行为(例如,记录)
  • 返回原始参数(未变异的)作为其返回值

这里的关键是函数不仅仅是引用透明的,而且对函数链完全透明。因此,它可以在任何时候在函数链中的任何地方被添加或删除,而不会影响链的输出。

清单 7-10 展示了一个随走随记的例子,使用一个名为$transparent的包装函数来创建一个透明版本的不纯日志记录函数。清单 7-11 显示了输出。

<?php

# Grab our compose function

require('../Chapter 3/compose.php');

# Define some math functions

$add_two = function ( $a ) {

        return $a + 2;

};

$triple = function ( $a ) {

    return $a * 3;

};

# Now we're going to create a "dirty" function to do some logging.

$log_value = function ( $value ) {

    # Do our impure stuff.

    echo "Impure Logging : $value\n";

    # Oops, we mutated the parameter value...

    $value = $value * 234;

    # ...and returned it even more mutated

    return $value.' is a number';

};

# Now we're going to create a higher-order function which returns a
# wrapped function which executes our impure function but returns
# the original input parameter rather than any output from our impure
# function. Note that we must pass $value to $impure_func by value and
# not by reference (&) to ensure it doesn't mess with it. Also see
# the sections on the mutability of objects if you pass those through,
# as the same concerns will apply here.

$transparent = function ($impure_func) {

    return function ($value) use ($impure_func) {

            $impure_func($value);

            return $value;

    };

};

# Compose the math functions together, with the $log_value impure function
# made transparent by our wrapper function

$do_sums = compose(
            $add_two,
            $transparent($log_value),
            $triple,
            $transparent($log_value)
    );

# We should get the expected result

var_dump( $do_sums(5) ); # 21

Listing 7-10.transparent.php

Impure Logging : 7
Impure Logging : 21
int(21)
Listing 7-11.transparent-output.txt

需要注意的一个关键点是,$transparent包装函数并没有使$log_value函数变得纯粹。不纯函数仍然可以通过抛出异常或错误来影响代码,虽然您可以推理您的函数代码,但在很大程度上忽略不纯函数,您不能(必然地)推理不纯函数本身。然而,它是一个有用的工具,可以最小化不纯函数的潜在影响,出于实用的原因,您希望在代码中包含不纯函数。它最适合执行输出,因为通常执行输入的主要原因是获取要在函数中处理的值,而这种方法不允许。图 7-2 展示了这样一个透明的功能是如何适应混合功能和非功能代码的正常流程的。

A447397_1_En_7_Fig2_HTML.jpg

图 7-2。

Mixed architecture with transparent functions

过程编程注意事项

混合函数式编程和过程式编程通常问题较少;毕竟,您在函数式编程中使用的大多数语法和结构都是从标准过程代码中借用的。正如本章前面所描述的,尽可能将两者分开是值得的;然而,随着您对对象可变性关注的减少,常见的是将程序代码块包装在函数中,而不是将其装订起来。如果你这样做了,只需注意你在函数外部的状态上做了什么,尽量不要让它影响你的函数的返回值。最常见的情况是,当“有必要”(理解为实用)从函数流中输出或写入文件时,将过程代码包装到函数中。最后,永远记住,仅仅因为你在过程代码中使用了函数,并不一定意味着你在编写函数代码(或者变异函数,比如array_walk)。

当您需要混合现有的过程代码时,您可以包含或要求脚本,并将它们视为一种功能。当您包含或需要一个文件时,PHP 在当前范围(例如,全局或函数范围)内执行该脚本。如果在脚本中添加返回值语句,PHP 将把它作为includerequire语句的返回值返回。这意味着,如果你小心的话,你可以把一个程序代码文件包装到一个函数的范围内。考虑清单 7-12 中所示的程序代码。

<?php

# This is some typical procedural code

echo ("a is $a\n");

$number = $a + 5;

$number = $number * 2;

for ($i = 0; $i < 5; $i++) {

    echo "We're doing procedural stuff here\n";

};

$b = 50;

# Note the addition of a return statement.

return $number;

Listing 7-12.procedural.php

现在考虑清单 7-13 ,它拉进过程文件两次,但方式略有不同。清单 7-14 显示了输出。

<?php

# First set some variables in global scope

$a = 25;
$b = 0;

# Do a simple require of the file.

$return_value =  require "procedural.php";

var_dump ( $return_value ); #60 - the script operated on our $a value of 25
var_dump ( $a ); # 25
var_dump ( $b ); # 50 - the script has mutated $b in the global scope

# Reset $b

$b = 0;

# This function executes the file as if it were a function, within the
# scope of the function. You can pass in a set of parameters as an array,
# and the extract line creates variables in the function scope which
# the code in the file can access. Finally, it requires the file and
# returns the files return value as its own.

$file_as_func = function ($filename, $params) {

        extract ($params);

        return require $filename;

};

# We'll call it on our procedural.php file, with a couple of parameters
# that have the same name but different values to our global $a and $b

var_dump ( $file_as_func( 'procedural.php', ['a'=>50, 'b'=>100] ) ); # 110
# this clearly operated on our parameter "a" and not the global $a

var_dump ( $a ); # 25
var_dump ( $b ); # 0 - unchanged this time

Listing 7-13.procedural2.php

a is 25
We're doing procedural stuff here
We're doing procedural stuff here
We're doing procedural stuff here
We're doing procedural stuff here
We're doing procedural stuff here
int(60)
int(25)
int(50)
a is 50
We're doing procedural stuff here
We're doing procedural stuff here
We're doing procedural stuff here
We're doing procedural stuff here
We're doing procedural stuff here
int(110)
int(25)
int(0)
Listing 7-14.procedural2-output.txt

正如您所看到的,这个方法为您提供了一种方便的方法来限制一段过程代码的范围,但仍然以参数化的方式推入数据并返回一个返回值。这不会特别增加您对过程代码进行推理的能力,或者限制许多类型的副作用的范围,但是它确实最小化了某些类型的错误的机会,并且帮助您在思想上划分代码。

摘要

您已经看到了混合编码范例时的各种方法和陷阱,但也探索了为什么在应用中为功能代码找到一个位置通常是一个好主意,而不是试图构造完全功能的代码库。PHP 是一门务实的语言,做一个务实的 PHP 程序员吧!

八、在应用中使用助手库

到目前为止,在本书中,您已经看到了如何从头开始编写自己的函数代码(尽管您在 monad 部分使用了一些库)。在这一章中,你将更深入地了解一些更流行的库,它们可以帮助你把你的应用变成功能强大的应用。这些可以帮助您减少开发时间,实现复杂或复杂的功能,或者将您的代码整理成一致的风格。

如何选择库

不幸的是,PHP 中没有单一的函数库可以推荐。正如您已经发现的,与其他一些语言不同,PHP 没有关于函数式编程以及如何构造函数式编程的正式思想,所以我将要介绍的函数库包含了实现函数式结构和过程的不同方式。您在自己的项目中选择使用哪一个将取决于您自己的编码风格、您的 PHP 经验的一般水平,并且在某些情况下,哪些库提供了您想要使用的特定构造。当您依次查看每个库时,我将对它的任何特殊优势或劣势进行评论;您将看到一些示例代码;我将列出该库的官方下载、网站和文档。在选定一个库之前,您可能希望了解每个库的以下方面:

  • 这个库包含你需要的所有功能吗?
  • 库还在维护吗?(如果它与当前的 PHP 版本兼容并且稳定,这可能没什么关系。)
  • 你需要的功能在这个库中实现得好吗(一致的,高性能的,可扩展的)?
  • 自动循环等功能是否存在(如果您需要的话)?
  • 库到底有多“纯”?

最后一项值得进一步解释。许多可用的库对不纯的、产生副作用的操作采取了实用的方法,比如我一直提倡的文件 I/O,并简单地将它们划分成单独的函数。其他人严格执行无副作用的代码,并实现他们自己的严格类型和不可变值。确保你对你的库强加的(或不强加的)功能纯度水平感到满意,然后再围绕它构建你的应用。

挑选库

为一个函数使用整个库通常不太理想,因为包括整个代码库只是为了使用其中的一小部分而浪费资源。函数式编程库,如果它们是以本书中提到的函数方式构建的,通常是由小的、单一用途的、可组合的函数组成的。假设你很高兴这个库处于一个稳定的发布点,深入源代码,取出你需要的函数,并粘贴/重新实现到你的代码中(当然,要尊重版权!).也就是说,如果您希望您的项目能够使用更多的功能,或者库还不稳定,那么包含(和更新)库的开销可能是值得的。

基于 Ramda 的库

Ramda 是一个用于函数式编程的流行 JavaScript 库。下面的 PHP 库受到了 Ramda 库的特性和语法的启发。

普拉达

  • 下载: https://github.com/kapolos/pramda
  • 文档: https://github.com/kapolos/pramda#pramda
  • 要点:Pramda 具有广泛的惰性评估和自动生成功能。该文档对使用该库的函数式编程概念进行了很好的基本介绍,但对所提供的函数的文档却很有限。
  • 示例代码:在清单 8-1 中,您将重复您的 Shakespeare analysis 函数来获取三个包含单词 hero 的长句。注意,库函数是作为P类的静态方法公开的(例如P::compose)。要使用该库,只需需要它或通过 Composer 安装它并需要自动加载程序。清单 8-2 显示了输出。
<?php

# Require the library

require('pramda/src/pramda.php');

# Start timing

$start_time = microtime(true);

# The same $match_word function

$match_word = function($word, $str) {

    return preg_match("/[^a-z]${word}[^a-z]/i", $str);

};

# we'll replace str_len with a composition of P::size and str_split.
# it provides no advantage here, other than to demostrate the composition
# of functions (and the fact that there's more than one way to skin a cat).
# Note that Pramda's compose method operates "right to left", i.e. it
# executes functions in the opposite order to the compose function
# we've used up to this point. Also note that we call the composed function
# immediately upon creation on the $str.

$longer_than = function($len, $str) {

    return P::compose(
                                 'P::size',
                                 'str_split'
                                 )($str) > $len;

};

# Create a function to get lines with the word hero in. Pramda doesn't have
# a simple partial function, so instead we curry the function.

$match_hero = P::curry2($match_word)('hero');

# Ditto with a function to get lines with more than 60 chars

$over_sixty = P::curry2($longer_than)(60);

# Pramda's own functions are mostly auto-currying (where it make sense),
# and so we can simply call the P::filter method (similar to array_filter)
# with just a callback, which creates a partial/curried function that
# just needs an array to be called with. We don't need to explicitly
# call a currying function on it.

$filter_hero = P::filter($match_hero);

$filter_sixty = P::filter($over_sixty);

$first_three = P::slice(0,3);

# Now we'll compose these together. Note that we use P::pipe and not P::compose,
# as mentioned above P::compose operates right-to-left, whereas it's easier
# to read left-to-right (or top-to-bottom as we've laid the code out here).
# If you look at the Pramda source code, P::pipe simply reverses the arguments
# and calls P::compose on them!

$three_long_heros = P::pipe(
                                                        'P::file', //lazy file reader
                                                        $filter_hero,
                                                        $filter_sixty,
                                                        $first_three
                                                 );

# We call the composed function in the normal way

$result = $three_long_heros('../Chapter 5/all_shakespeare.txt');

print_r($result);

echo 'Time taken : '.(microtime(true) - $start_time);

Listing 8-1.pramda-example.php

Array
(
    [0] =>     Enter DON PEDRO, DON JOHN, LEONATO, FRIAR FRANCIS, CLAUDIO, BENEDICK, HERO, BEATRICE, and Attendants

    [1] =>     Sweet Hero! She is wronged, she is slandered, she is undone.

    [2] =>     Think you in your soul the Count Claudio hath wronged Hero?

)
Time taken : 1.4434311389923

Listing 8-2.pramda-example-output.txt

如你所见,输出的句子和你在第五章得到的是一样的。在清单 8-3 中,您将在一家餐馆的菜单上做一些工作(因为我又饿了)以查看更多可用于数组的函数。清单 8-4 显示了输出。

<?php

# Get the library

require('pramda/src/pramda.php');

# Define our menu data

$menu = [

    [   'Item' => 'Apple Pie',
        'Category' => 'Dessert',
        'Price' => 4.99,
        'Ingredients' => ['Apples' => 3, 'Pastry' => 1, 'Magic' => 100]
    ],

    [   'Item' => 'Strawberry Ice Cream',
        'Category' => 'Dessert',
        'Price' => 2.22,
        'Ingredients' => ['Strawberries' => 20, 'Milk' => 10, 'Sugar' => 200]
    ],

    [   'Item' => 'Chocolate and Strawberry Cake',
        'Category' => 'Dessert',
        'Price' => 5.99,
        'Ingredients' => ['Chocolate' => 4, 'Strawberries' => 5, 'Cake' => 4]
    ],

    [   'Item' => 'Cheese Toasty',
        'Category' => 'Main Courses',
        'Price' => 3.45,
        'Ingredients' => ['Cheese' => 5, 'Bread' => 2, 'Butter' => 6]
    ]
];

# Let's get a list of all the distinct ingredients used in the menu

$all_ingredients = P::pipe(

                                                        # get just the ingredient array from each element

                                                        P::pluck('Ingredients'),

                                                        # reduce them into a single array

                                                        P::reduce('P::merge', []), #

                                                        # grab just the keys (the ingredient names)
                                                        # which will be unique due to the merge above

                                                        'array_keys'

                                                    );

var_dump( $all_ingredients($menu) );

# This time we want to count the quantity of fruit used in our menu, if
# we were making one of each dish

$fruit = ['Apples' => true, 'Strawberries' => true, 'Plums' => true];

# A function to get only items who contain fruit

$get_only_fruit = function($item) use ($fruit) {

        # P::prop returns an array element with a particular key, in this
        # case the element holding an array of Ingredients, from which
        # we get the elements which intersect with the keys in $fruit

        return array_intersect_key(P::prop('Ingredients', $item), $fruit);

};

$count_fruit = P::pipe ( # compose a function which...

                                              P::map( # ... maps a function onto the input which

                                                              P::pipe(

                                                                              $get_only_fruit, # gets the fruits and

                                                                              'P::sum' # sums the quantities

                                                                           ) # for each element/item

                                                           ),

                                              'P::sum' # ...and then sums all the quantities

                                            );

var_dump( $count_fruit($menu) ); #28 (3 apples, 20 strawberries, 5 strawberries)

# Now let's say we want to get a dessert menu, ordered by price,
# starting with the most expensive to increase our profits

$dessert_menu = P::pipe(

                                            # First, sort the data by price

                                            P::sort( P::prop('Price') ),

                                            # Reverse the results so the most expensive is first

                                            'P::reverse',

                                            # Filter the results so that we only have
                                            # desserts

                                            P::filter(

                                                function ($value, $key) {

                                                                            return P::contains('Dessert', $value);

                                                }

                                            ),

                                            # P::filter returns a generator, but because we need
                                            # to iterate over it twice below, we need to convert
                                            # to an array first

                                            'P::toArray',

                                            # Now let's pick out just the information we want to
                                            # display in our menu

                                            function ($items) {

                                                        # Get an array of Item names to use as keys,
                                                        # and an array of Prices to use as values,
                                                        # and array_combine them into a single array.
                                                        # Again, P:pluck returns a generator, we want
                                                        # an array.

                                                        return array_combine(

                                                                         P::toArray( P::pluck('Item',$items) ),

                                                                         P::toArray( P::pluck('Price',$items) )

                                                                     );

                                            }
    );

print_r( $dessert_menu($menu) );

Listing 8-3.pramda-example2.php

array(11) {
  [0]=>
  string(6) "Apples"
  [1]=>
  string(6) "Pastry"
  [2]=>
  string(5) "Magic"
  [3]=>
  string(12) "Strawberries"
  [4]=>
  string(4) "Milk"
  [5]=>
  string(5) "Sugar"
  [6]=>
  string(9) "Chocolate"
  [7]=>
  string(4) "Cake"
  [8]=>
  string(6) "Cheese"
  [9]=>
  string(5) "Bread"
  [10]=>
  string(6) "Butter"
}
int(28)
Array
(
    [Chocolate and Strawberry Cake] => 5.99
    [Apple Pie] => 4.99
    [Strawberry Ice Cream] => 2.22
)
Listing 8-4.pramda-example2-output.txt

制药公司

  • 下载: https://github.com/mpajunen/phamda
  • 文档: http://phamda.readthedocs.io/en/latest/
  • 重点:Phamda 和 Pramda 差不多(除了名字上的单字符区别!).不过,对于许多任务,Phamda 比 Pramda 快,而且它确实提供了一些 Pramda 还没有的附加功能。
  • 示例代码:在清单 8-5 中,您将再次使用菜单数据并探索一些 Pramda 没有的功能,例如ifElsenotevolve。要使用该库,只需像这里一样要求它或通过 Composer 安装它。清单 8-6 显示了输出。
<?php

# Load via composer, or require the four files below

require('phamda/src/CoreFunctionsTrait.php');
require('phamda/src/Exception/InvalidFunctionCompositionException.php');
require('phamda/src/Collection/Collection.php');
require('phamda/src/Phamda.php');

use Phamda\Phamda as P;

# Same data as before

$menu = [

    [   'Item' => 'Apple Pie',
        'Category' => 'Dessert',
        'Price' => 4.99,
        'Ingredients' => ['Apples' => 3, 'Pastry' => 1, 'Magic' => 100]
    ],

    [   'Item' => 'Strawberry Ice Cream',
        'Category' => 'Dessert',
        'Price' => 2.22,
        'Ingredients' => ['Strawberries' => 20, 'Milk' => 10, 'Sugar' => 200]
    ],

    [   'Item' => 'Chocolate and Strawberry Cake',
        'Category' => 'Dessert',
        'Price' => 5.99,
        'Ingredients' => ['Chocolate' => 4, 'Strawberries' => 5, 'Cake' => 4]
    ],

    [   'Item' => 'Cheese Toasty',
        'Category' => 'Main Courses',
        'Price' => 3.45,
        'Ingredients' => ['Cheese' => 5, 'Bread' => 2, 'Butter' => 6]
    ]
];

# A function to mark an item as a "special" if it's price is over 5\. The
# Phamda functions we use here are :
# P::ifElse - If the first parameter is true, call the second parameter, else
#   call the third
# P::lt - If the first parameter (5) is less than the second, then return true
#   Note that due to auto currying the $price will be supplied as the second
#   parameter, which is why we use lt rather than gt
# P::concat - add the "Special: " string to the price, (called if P::lt returns
#      true)
# P::identity - the identity function returns the value passed in, so if P::lt
#   returns false this is called, and the price is returned unchanged.
#
# Note that we add ($price) on the end to execute the function straight away

$specials = function ($price) {

        return P::ifElse(P::lt(5), P::concat('Special: '), P::identity())($price);

};

# A function to format the menu item for our menu

$format_item = P::pipe(
                                                # Get just the fields that we want for the menu

                                              P::pick(['Item','Price']),

                                                # "Evolve" those fields, by applying callbacks to them.
                                                # Item is made into uppercase letters, and Price
                                                # is passed through our $specials function above
                                                # to add Special: to any item that costs over 5

                                                P::evolve(['Item'=>'strtoupper', 'Price'=>$specials])

                                                );

# A function to parse our menu data, filter out non-desserts,
# and format the remaining items

$new_dessert_menu = P::pipe(

                                            # It would be more robust to use P::contains('Dessert')
                                            # on the P::prop('Category') lest we introduce
                                            # entrées at a later date, but for now to demonstrate
                                            # P::not and the scope of P::contains, we'll do this:

                                            P::filter( P::not ( P::contains('Main Courses') ) ),

                                            # Map the $format_item function above onto the remaining
                                            # (hopefully only Dessert) items

                                            P::map($format_item)

);

# Finally, create our menu

print_r( $new_dessert_menu( $menu ) );

Listing 8-5.phamda-example.php

Array
(
    [0] => Array
        (
            [Item] => APPLE PIE
            [Price] => 4.99
        )

    [1] => Array
        (
            [Item] => STRAWBERRY ICE CREAM
            [Price] => 2.22
        )

    [2] => Array
        (
            [Item] => CHOCOLATE AND STRAWBERRY CAKE
            [Price] => Special: 5.99
        )

)

Listing 8-6.phamda-example-output.txt

您可能想知道为什么使用not函数,而不是 PHP 自带的!(感叹号)操作符。事实上,如果你看看 Phamda 的源代码,你会看到!被用来实现not功能。然而,!就其本身而言,不容易组合,不具备自动循环的特性,并且很容易遗漏本应易读的声明性代码。Phamda 的not函数只是本机!操作符的一个包装器,将它变成一个一级函数。

基于下划线的库

下划线是一个流行的 JavaScript 函数式编程库,一些库已经尝试将其移植到 PHP,或者受到其语法和特性的启发。注意这里有两个同名的库,所以我将它们标记为(1)和(2)。

Underscore.php(1)

  • 下载: https://github.com/brianhaveri/Underscore.php
  • 文档: http://brianhaveri.github.io/Underscore.php/
  • 要点:Underscore.php (1)是 JavaScript 库的一个相当直接的端口。虽然功能完善,但在撰写本文时,它已经有六年没有更新了。
  • 示例代码:要使用这个库,只需如下所示。在清单 8-7 中,您将使用第五章中的斐波那契示例,使用下划线的memoize函数创建一个记忆版本。回到第五章查看这是如何工作的,并将那里的结果与这里的结果进行比较。
<?php

# Get the library

require('Underscore.php/underscore.php');

# The code below is exactly the same as in Chapter 5, except where noted

$fibonacci =

        function ($n) use (&$fibonacci) {

        usleep(100000);

    return ($n < 2) ? $n : $fibonacci($n - 1) + $fibonacci($n - 2);

    };

# Here we memoize using the underscore memoize function rather than our own

$memo_fibonacci = __::memoize($fibonacci);

$timer = function($func, $params) {

    $start_time = microtime(true);

    $results = call_user_func_array($func, $params);

    $time_taken = round(microtime(true) - $start_time, 2);

    return [ "Param" => implode($params),
                     "Result" => $results,
                     "Time" => $time_taken ];

};

print_r( $timer(  $fibonacci, [6, '*'] ) );

print_r( $timer(  $memo_fibonacci, [6] ) );

print_r( $timer(  $fibonacci, [6, '*'] ) );

print_r( $timer(  $memo_fibonacci, [6] ) );

print_r( $timer(  $memo_fibonacci, [10] ) );

print_r( $timer(  $memo_fibonacci, [11] ) );

print_r( $timer(  $memo_fibonacci, [8] ) );

# We'll add an extra call with parameter 8

print_r( $timer(  $memo_fibonacci, [8] ) );
underscore-memoize.php
Array
(
    [Param] => 6*
    [Result] => 8
    [Time] => 2.5
)
Array
(
    [Param] => 6
    [Result] => 8
    [Time] => 2.5
)
Array
(
    [Param] => 6*
    [Result] => 8
    [Time] => 2.5
)
Array
(
    [Param] => 6
    [Result] => 8
    [Time] => 0
)
Array
(
    [Param] => 10
    [Result] => 55
    [Time] => 17.72
)
Array
(
    [Param] => 11
    [Result] => 89
    [Time] => 28.73
)
Array
(
    [Param] => 8
    [Result] => 21
    [Time] => 6.71
)
Array
(
    [Param] => 8
    [Result] => 21
    [Time] => 0
)

Listing 8-7.underscore-memoize-output.txt

如果你将这些结果与第五章中的结果进行比较,你会注意到一些不同之处。具体来说,当参数完全匹配时,时间只会减少。例如,计算第 11 个数字需要很长时间,即使你已经计算了第 10 个。这个库中的记忆函数只记忆外部的函数调用,而不记忆内部的递归调用。因此,该函数要么花费全部时间运行,要么在第二次调用第 6 个和第 8 个斐波那契数列时花费 0。

这里的教训是要经常检查你所依赖的函数的实现细节,即使它们的名字是一样的。你会发现在不同的库中有许多不同的记忆功能,有许多不同的操作方式。

在清单 8-8 中,您将看到库的节流函数。这是一个创建自节流函数的高阶函数,每 x 毫秒只能成功调用一次。清单 8-9 显示了输出。

<?php

# Get the library

require('Underscore.php/underscore.php');

# Create a simple function to output a dot

$write = function ($text) { echo '.'; };

# create a new function which is a throttled version of
# $write. It will execute at a maximum once per 1000ms.
# Any other calls during that 1000ms will be ignored.

$throttled_write = __::throttle( $write, 1000);

# Let's call $throttled_write 10 million times. On my
# system that takes a little over 7 seconds, but as it will
# only *actually* execute once every 1000ms (1sec) we
# will get a line of 7 dots printed.

__::times(10000000, $throttled_write);

Listing 8-8.underscore-throttle.php

.......
Listing 8-9.underscore-throttle-output.txt

强调

  • 下载: https://github.com/Im0rtality/Underscore
  • 文档: https://github.com/Im0rtality/Underscore/wiki/Intro
  • 要点:下划线是 JavaScript 库的一个更新更全面的端口。它提供了内置的函数链,但在文档方面却很少。当你需要更多地了解一个特定的函数时,你需要钻研源代码,尽管它写得相当好并且容易理解。注意,各种变化意味着文档中的例子不起作用(例如,pick函数被重命名为pluck)。
  • 示例代码:要使用这个库,您需要通过 Composer 安装它。清单 8-10 展示了如何将函数调用链接在一起。清单 8-11 显示了输出。
<?php

# Autoload the library

require __DIR__ . '/vendor/autoload.php';

use Underscore\Underscore;

# Run a set of chained functions. Note that we're *not* composing
# a function to be run later, but executing the series of functions
# right here.

# Some data to work with

$foods = [ 'Cheese' => 'Cheddar',
                     'Milk' => 'Whole',
                     'Apples' => 'Red',
                     'Grapes' => 'White'
                 ];

# The ::from function "loads" an array of data into the chain

$result = Underscore::from($foods)

                        # Let's map a function to uppercase each value and prepend the
                        # array key to it.

                        ->map(function($item,$key) {

                                                                           return strtoupper($key.' - '.$item);

                                                                                })

                        # Invoke invokes a function over each element like map

                        ->invoke(function($item) { var_dump($item);})

                        # Shuffle the order of the array

                        ->shuffle()

                        # Finally generate the return value for the function chain which
                        # is the array returned by shuffle()

                        ->value();

# Output the final array

var_dump($result);

Listing 8-10.underscore-chain.php

string(16) "CHEESE - CHEDDAR"
string(12) "MILK - WHOLE"
string(12) "APPLES - RED"
string(14) "GRAPES - WHITE"
array(4) {
  ["Grapes"]=>
  string(14) "GRAPES - WHITE"
  ["Cheese"]=>
  string(16) "CHEESE - CHEDDAR"
  ["Apples"]=>
  string(12) "APPLES - RED"
  ["Milk"]=>
  string(12) "MILK - WHOLE"
}
Listing 8-11.underscore-chain-output.txt

Underscore.php(2)

  • 下载: https://github.com/Anahkiasen/underscore-php
  • 文档: http://anahkiasen.github.io/underscore-php/
  • 要点:Underscore.php(2)可能是你见过的最灵活的下划线克隆库,也是记录最好的。它不是一个直接的端口,而是受 JavaScript 库风格的启发,同时部署了 PHP 所能提供的最好的功能。
  • 示例代码:要使用这个库,您需要通过 Composer 安装它。清单 8-12 展示了调用和链接方法的灵活方式,并展示了与原生 PHP 函数的集成。清单 8-13 显示了输出。
<?php

# Autoload the library

require __DIR__ . '/vendor/autoload.php';

# We're going to use the Arrays type

use Underscore\Types\Arrays;

# Some data

$data = [10, 25, 38, 99];

# A helper function, returns true if $number is Equivalent

$is_even = function ($number) {

    return $number % 2 == 0;

};

# We can call the library functions as static methods

var_dump( Arrays::average($data) ); # 43

# We can chain them together, here we load our data with from(),
# filter out the odd number (25 & 99) with our $is_even function,
# and then sum the remaining even numbers

var_dump ( Arrays::from($data)
                                                            ->filter($is_even)
                                                            ->sum()
                 ); #10+38 = 48

# We can also instantiate an object to encapsulate our data,
# and call the methods directly on that (which is effectively what the
# static methods do in the background.

$array = new Arrays($data);

var_dump( $array->filter($is_even)->sum() ); #48 again

# The following chain contains a "reverse" function. However no such
# function exists in the library. The library will attempt to use
# native PHP functions for such calls, for arrays it tries to find
# a native function with the same name prefixed by array_, so in
# this case it will use the native array_reverse function.

var_dump( Arrays::from($data)->reverse()->obtain() );

Listing 8-12.underscore-flexible.php

float(43)
int(48)
int(48)
array(4) {
  [0]=>
  int(99)
  [1]=>
  int(38)
  [2]=>
  int(25)
  [3]=>
  int(10)
}
Listing 8-13.underscore-flexible-output.txt

杂项库

下面的库不像你到目前为止看到的那些库那样是克隆的,它们试图自立,以自己的方式做事。

军刀

  • 下载: https://github.com/bluesnowman/fphp-saber
  • 文档: https://github.com/bluesnowman/fphp-saber#saber
  • 要点:Saber 试图使用自己的基于对象的类型系统为 PHP 带来强类型和不变性。虽然它在这些目标上取得了成功,但它(在我看来)不容易开发,导致代码补丁难以阅读。具有讽刺意味的是,虽然像这样的强类型的想法之一是使您的代码更容易推理,但背离 PHP 标准变量意味着普通 PHP 程序员更容易误解代码。缺乏完整的文档使问题更加复杂。也就是说,如果你的库所执行的那些特征对你的用例很重要,这是你唯一的选择之一。
  • 示例代码:要使用这个库,您需要通过 Composer 安装它。清单 8-14 展示了如何创建强类型值并对它们应用函数链。清单 8-15 显示了输出。
<?php

# Autoload the library

require __DIR__ . '/vendor/autoload.php';

# You will need to use the Saber datatypes for all data

use \Saber\Data\IInt32;

# Ordinary PHP variable

$start = 20;

# To work with the value, we need to "box" it into a Saber object
# which encapsulates it in a "strictly typed" object

$boxed_value = IInt32\Type::box($start);

# We can chain functions onto the boxed value (note that the parameters
# for those functions also need to be the correct boxed types)

$boxed_result = $boxed_value -> increment() # 21

                                         -> increment() # 22

                                                         -> multiply( IInt32\Type::box(3) ) # 66

                                         -> decrement(); # 65

# To get the value back out into a standard PHP variable we need to "unbox" it

var_dump( $boxed_result->unbox() ); # 65

# And check that the original boxed value object that we chained the
# functions on is unmutated

var_dump( $boxed_value->unbox() ); # 20

Listing 8-14.sabre-example.php

int(65)
int(20)
Listing 8-15.sabre-example-output.txt

函数式 PHP

  • 下载: https://github.com/lstrojny/functional-php
  • 文档: https://github.com/lstrojny/functional-php/blob/master/docs/functional-php.md
  • 要点:函数式 PHP 是一组旨在简化函数式编程的函数原语。它得到了相对良好的维护,尽管下划线被列为其灵感之一,但它也借鉴了 Scala、Dojo(一个 JavaScript 工具包)和其他资源。它有很好的文档,GitHub 主页甚至有一幅关于尾部递归的 XKCD 漫画,有什么不喜欢的呢?嗯,虽然它在函数原语(函数如mapplucksort等)上很强。),它在功能性构图方面的特征赋予得不太好。
  • 示例代码:要使用这个库,您需要通过 Composer 安装它。清单 8-16 展示了菜单数据中使用的一些功能。清单 8-17 显示了输出。
<?php

# Autoload the library

require __DIR__ . '/vendor/autoload.php';

# The recommended way to use the library (only in PHP 5.6+) is to
# import the individual functions as function names so that you
# don't need to qualify them in the code

use function Functional\select;
use function Functional\reject;
use function Functional\contains;
use function Functional\map;
use function Functional\pick;
use function Functional\sort;
use function Functional\drop_last;
use function Functional\select_keys;

# Our trusty menu data

$menu = [

    [   'Item' => 'Apple Pie',
        'Category' => 'Dessert',
        'Price' => 4.99,
        'Ingredients' => ['Apples' => 3, 'Pastry' => 1, 'Magic' => 100]
    ],

    [   'Item' => 'Strawberry Ice Cream',
        'Category' => 'Dessert',
        'Price' => 2.22,
        'Ingredients' => ['Strawberries' => 20, 'Milk' => 10, 'Sugar' => 200]
    ],

    [   'Item' => 'Chocolate and Strawberry Cake',
        'Category' => 'Dessert',
        'Price' => 5.99,
        'Ingredients' => ['Chocolate' => 4, 'Strawberries' => 5, 'Cake' => 4]
    ],

    [   'Item' => 'Cheese Toasty',
        'Category' => 'Main Courses',
        'Price' => 3.45,
        'Ingredients' => ['Cheese' => 5, 'Bread' => 2, 'Butter' => 6]
    ]
];

# Define a function to check if a food is a dessert, using the contains
# function. Returns true if it's a dessert

$is_dessert = function ($food) {
    return contains($food, 'Dessert');
};

# Using the function above, we can apply it in two different ways to our menu
# data using the select and reject functions.

$desserts = select($menu, $is_dessert);

$mains = reject($menu, $is_dessert);

# A helper function using map and pick to return an array of just item names

$list_foods = function ($foods) {

    return map($foods, function ($item) {

         return pick($item, 'Item');

     });

};

# Output the results of the select and reject statements above, using our
# helper function so we don't dump the whole array contents

echo "Desserts:\n";

print_r ( $list_foods( $desserts ) );

echo "Main Courses:\n";

print_r ( $list_foods( $mains ) );

# Our restaurant is struggling, so we want to dump our cheapest dishes.
# First, we need to use the libraries sort function (with a custom callback # function) to sort our $menu based on $price.

$sorted = sort($menu, function($item1,$item2) {

    return $item1["Price"] < $item2["Price"];

}, true);

# Now we want to drop any items that cost less than 3\. We use the drop_last
# function to drop the last elements of our sorted array that are >=3

$expensive_items = drop_last($sorted, function ($item) {

    return $item["Price"] >= 3;

});

# Let's see what we're left with :s

echo "Expensive Items:\n";

print_r( $list_foods( $expensive_items ) );

# To create our menu, we want to pick out just the Item and Price, so # we'll map the select_keys function against each element to pick those out.

$new_menu = map($expensive_items, function ($item) {

     return select_keys($item, ['Item','Price']);

 });

echo "New menu:\n";

print_r($new_menu);

Listing 8-16.functionalphp-example.php

Desserts:
Array
(
    [0] => Apple Pie
    [1] => Strawberry Ice Cream
    [2] => Chocolate and Strawberry Cake
)
Main Courses:
Array
(
    [3] => Cheese Toasty
)
Expensive Items:
Array
(
    [2] => Chocolate and Strawberry Cake
    [0] => Apple Pie
    [3] => Cheese Toasty
)
New menu:
Array
(
    [2] => Array
        (
            [Item] => Chocolate and Strawberry Cake
            [Price] => 5.99
        )

    [0] => Array
        (
            [Item] => Apple Pie
            [Price] => 4.99
        )

    [3] => Array
        (
            [Item] => Cheese Toasty
            [Price] => 3.45
        )

)

Listing 8-17.functionalphp-example-output.txt

其他库

你在本章中探索的库(可能)是目前 PHP 中最常用的函数式编程库。还有其他的,你会在本书的附录 C 中找到一些供你探索。这些库也都是通用的函数集合,还有其他一些库专注于特定类型的函数或函数概念。同样,你会在附录中找到它们。