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

思考并回答以下问题:

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

  • 如何构造函数式应用程序以及使其充分发挥功能
  • 如何以及何时混合和匹配范式,例如功能和面向对象的编程

PHP范式的历史

函数的引用透明性(referential transparency)

  1. 基础
    初学程序设计时,比较容易混淆的两个概念是数学函数(math function)和程序中使用的函数。

在数学函数中 y=f(x),一个输入值有固定的输出值。例如,无论计算多少次,sinπ 的结果总是 0。如果 f(x)=x/2,那么 f(10) 无论计算 100 次还是 1000 次,其结果都是 5.

程序设计中的函数却不具备这种稳定的特性,因为函数的执行不仅依赖于输入值,而且会受到全局变量,输入文件,类的成员变量等诸多因素的影响。如下:

1
2
3
4
int counter = 0;
int count(){
return ++counter;
}

此函数输入没有输入值,但每次都返回不同的结果。当然,就像数学函数那样,程序中函数还可以设计成“对同一输入值每次都返回相同结果”的形式。

函数的返回值只依赖于其输入值,这种特性就称为引用透明性(referential transparency)

  1. 动态规划的缓存
    显然,动态规划所使用的制表法(也即缓存)只能应用于具有引用透明性的函数。如果外在因素使相同输入值返回不同结果值,则不能缓存。

也即缓存对应的 map,实现的是同一个输入(key),同一个输出(value),而不可能出现同一个输入,可以得到不同的输出,也即输出结果的不确定性。

使事情简单明了的最佳方法是将您的代码分为功能和非功能代码块。 一个明显的安排是用功能代码编写业务逻辑,高性能算法等,并将其夹在不纯的OO或过程代码之间以处理输入和输出,如图7-1所示。

通过这样的安排,您可以使用干净的功能代码块来进行推理和测试,并且当问题确实发生时,更容易找出它们可能位于的位置。 当您面对使用功能性技术更新现有代码库时,一种好的方法是首先确定适合图7-1中间部分的代码部分,然后优先确定它们的优先级。

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
<?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
$double_object = function ($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); # #3 (#2 was our closure object $double_object)
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
<?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)
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
<?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_maths = compose(
$triple_object,
$multiply_object_by_pi,
$triple_object
);

# and then call that composition on our object.
var_dump ( $more_maths($my_object) );

# Let's check our original object remains unchanged
var_dump ($my_object);
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
<?php

# Create a class to represent an immutable array

# Make the clas "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
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

# 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
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
<?php

# Grab our compose function

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

# Define some maths 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 maths 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?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;
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
<?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
0%