用函数式开发商业逻辑

思考并回答以下问题:

在本章中,您将了解函数式编程的其他一些常见用法。您将首先查看功能代码如何帮助您管理程序中实现的业务逻辑。然后,您将了解什么是基于事件的编程,以及函数式编程如何帮助您应对管理传入事件的复杂性并保持数据流的顺畅。最后,您将快速了解异步编程,并了解为什么函数式编程在该领域也是有用的工具。

处理业务逻辑

Listing 6-1 business_data.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
<?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-2 business_logic.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
<?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-3 shopping.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
<?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-5 install_event.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 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-6 server_functions.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232

<?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
# embed the image binary data here!
$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-7 web_server.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
<?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".
# In fact, you can use any kind of callable here. All will be called
# 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";
0%