工作以来, 在编写程序的时候一直使用面向对象的思想. 当然, 对函数式编程也有所耳闻, 但也仅仅是有所耳闻, 从来没有上手写过.
最近没事的时候就找些资料看看, 同时也尝试自己写一些函数式编程思想的代码. 毕竟这也是一种编程思想嘛, 虽然应用没有面向对象这么广泛(当然, 也可能仅仅是我觉的, 毕竟我在使用中全部都是面向对象), 但了解其编程思想, 对于解决问题也提供一种新的思路不是.
以下简单总结一下我最近对函数式编程的体验.
最开始, 我以为将面向对象中的类为基本单位, 换成函数为基本单位, 就是函数式编程了, 结果发现, 这只能说明我还是在使用面向对象的思想.
那么什么是函数式编程呢?
看到函数这个名字, 最先想到的就是初中的数学了: f(x)=2x. 这是一个一元一次函数.
同时, 在对各种函数进行计算的时候, 还会用到函数的嵌套, 比如:
- f(x)=2x
- g(x)=x+2
- q(x)=g( f(x) )
这种函数的嵌套关系, 是不是也能应用到编程中呢? 没错. 比如这样一个需求: 输出列表中奇数的个位数.
传统的写法如下(PHP 版本):
function dispose($arr){
foreach ($arr as $item){
// 过滤偶数
if($item % 2 == 0) continue;
// 取出个位数字
$digit = $item % 10;
// 将个位数字输出
echo '当前数字: '.$digit, PHP_EOL;
}
}
dispose([12, 24, 37, 115]);
如果写成这种嵌套形的呢?
// 过滤偶数
function filterEvent($arr){
foreach ($arr as $item){
if($item % 2 == 0) continue;
yield $item;
}
}
// 取出个位数
function getDigit($arr){
foreach ($arr as $item){
yield $item % 10;
}
}
// 将数字转成字符串
function getEchoStr($arr){
foreach ($arr as $item){
yield '当前数字: '.$item.PHP_EOL;
}
}
// 输出数组每一个选项
function echoItem($arr){
foreach ($arr as $item){
echo $item;
}
}
echoItem(
getEchoStr(
getDigit(
filterEvent([12, 24, 37, 115])
)
)
);
后面这个是不是只看调用, 也能够清楚的看到其执行过程, 但是看起来有些丑. 类似于一个管道, 数据依次从管道中流过, 拿到最终的结果. 等等, 管道, 怎么感觉有点眼熟.
linux 中的命令使用的不就是这种思想么. 函数嵌套确实比较丑陋, 同时每一个方法中都需要进行遍历, 重复代码过多. 但是如果能够像 linux 的命令这样, 那就好看了. 别说, 还真有, 不过是在 Python 中实现的(通过运算符重载), 看到下面这个实现, 你一定会觉得很漂亮, 因为我第一次写出来的时候眼前一亮.
class Pipe(object):
def __init__(self, func):
self.func = func
# 此方法当 位运算 | 左侧操作符不支持的时候调用
def __ror__(self, other):
for item in other:
if item is None:
continue
yield self.func(item)
@Pipe
def filter_event(item):
return item if item % 2 != 0 else None
@Pipe
def get_digit(item):
return item % 10
@Pipe
def get_echo_str(item):
return '当前数字: ' + str(item)
@Pipe
def echo(item):
print(item)
def pipeline(sqs):
# 这里因为前面都是迭代器, 所以需要一个空遍历, 否则函数不会执行
for item in sqs: pass
arr = [12, 24, 37, 115]
pipeline(arr | filter_event | get_digit | get_echo_str | echo)
看这个调用是不是和 Linux 的命令行一样?
另外, 一个好消息是, Python
存在了函数式编程的包, 也就是说, 你也可以这样写:
from typing import List
from pipe import where, sort, Pipe
@Pipe
def self_dis(nums: List[int]):
# 自定义管道
print(nums)
return nums
num_list_with_duplicates = [1, 2, 3, 4, 5, 9, 7, 8]
# 进行管道操作
results = list(
num_list_with_duplicates
| where(lambda x: x % 2 == 1) # 数据过滤
| sort # 排序
| self_dis
)
print(results)
在函数式编程中, 对数据的处理有如下三种方式:
- map: 对数据进行转换, 一对一
- filter: 对数据进行过滤
- reduce: 对数据进行聚合
一个数据源, 流过各个管道, 通过以上三种方式进行处理, 得到最终结果. 等等, 这不就是spark
的处理思路嘛.
在纯函数式编程中, 函数是不会保存外部状态的, 对于一个函数, 接收确定输入的同时, 会返回确定的输出. 故而也不用考虑并发的问题, 同时因为没有外部状态, 对于单元测试来说也极度友好.
针对我对于函数式编程的使用来看, 总结函数式编程的几个特点, 可能并不全面:
- 管道操作. 可以将数据通过依次流过各个管道, 将各种简单的操作整合为一个复杂的操作.
- 将函数作为头等对象
- 延迟处理. 这个是我自己认为的. 既然函数对外部没有影响, 那么函数的返回值就可以在真正使用的时候在获得.
- 没有并发问题. 仅针对于纯函数编程.
当然, 我也尝试着使用函数式编程实现一些稍微复杂一些的功能, 怎么说呢. 在完成一些较复杂功能的时候, 感觉函数式编程思想并没有那么好用, 很可能是因为我在很大程度上思想还没有转变过来, 所以写起来比较费力.
不过, 就一些简单的例子来说, 个人感觉管道的操作确实十分优美.
此外, 函数式编程不止以上内容, 这段时间只是简单的尝试