函数式编程

工作以来, 在编写程序的时候一直使用面向对象的思想. 当然, 对函数式编程也有所耳闻, 但也仅仅是有所耳闻, 从来没有上手写过.

最近没事的时候就找些资料看看, 同时也尝试自己写一些函数式编程思想的代码. 毕竟这也是一种编程思想嘛, 虽然应用没有面向对象这么广泛(当然, 也可能仅仅是我觉的, 毕竟我在使用中全部都是面向对象), 但了解其编程思想, 对于解决问题也提供一种新的思路不是.

以下简单总结一下我最近对函数式编程的体验.

最开始, 我以为将面向对象中的类为基本单位, 换成函数为基本单位, 就是函数式编程了, 结果发现, 这只能说明我还是在使用面向对象的思想.

那么什么是函数式编程呢?

看到函数这个名字, 最先想到的就是初中的数学了: 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])
        )
    )
);

后面这个是不是只看调用, 也能够清楚的看到其执行过程, 但是看起来有些丑. 类似于一个管道, 数据依次从管道中流过, 拿到最终的结果. 等等, 管道, 怎么感觉有点眼熟.

image-20210415231803940

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)

在函数式编程中, 对数据的处理有如下三种方式:

  1. map: 对数据进行转换, 一对一
  2. filter: 对数据进行过滤
  3. reduce: 对数据进行聚合

一个数据源, 流过各个管道, 通过以上三种方式进行处理, 得到最终结果. 等等, 这不就是spark的处理思路嘛.

在纯函数式编程中, 函数是不会保存外部状态的, 对于一个函数, 接收确定输入的同时, 会返回确定的输出. 故而也不用考虑并发的问题, 同时因为没有外部状态, 对于单元测试来说也极度友好.

针对我对于函数式编程的使用来看, 总结函数式编程的几个特点, 可能并不全面:

  1. 管道操作. 可以将数据通过依次流过各个管道, 将各种简单的操作整合为一个复杂的操作.
  2. 将函数作为头等对象
  3. 延迟处理. 这个是我自己认为的. 既然函数对外部没有影响, 那么函数的返回值就可以在真正使用的时候在获得.
  4. 没有并发问题. 仅针对于纯函数编程.

当然, 我也尝试着使用函数式编程实现一些稍微复杂一些的功能, 怎么说呢. 在完成一些较复杂功能的时候, 感觉函数式编程思想并没有那么好用, 很可能是因为我在很大程度上思想还没有转变过来, 所以写起来比较费力.

不过, 就一些简单的例子来说, 个人感觉管道的操作确实十分优美.

此外, 函数式编程不止以上内容,  这段时间只是简单的尝试 


原文地址 https://hujingnb.com/archives/403

转载请保留原文连接: 函数式编程 | 烟草的香味

订阅评论
提醒
guest
0 评论
内联反馈
查看所有评论
0
希望看到您的想法,请发表评论。x