-->

左耳听风_034_33_编程范式游记4-_函数式编程

你好,我是陈浩网名做我的house.从前面三讲的内容中呢,我们了解到,虽然c语言简单灵活,能够让程序员在高级语言的特性之上,轻松的进行底层上的微观控制,被誉为高级语言中的汇编语言。

但是它基于过程和底层的设计初衷呢又成为了它的短板。

在程序世界中呢,编程工作啊更多的是解决业务上的问题,而不是计算机的问题。

所以呢我们就需要更为贴近业务,更为抽象的语言啊,比如典型的面向对象语言、c加加和java等等。

那c加加在很大程度上解决了c语言中各种的问题和不便,尤其啊是通过类模板虚函数和运行时识别,解决了c语言的泛性编程问题。

呃,但是我们怎么来做更为抽象的泛型呢?那答案呢就是函数式编程。

那相对于计算机的历史来说呢,函数式编程其实是一个非常古老的概念。

函数式编程的基础模型呢来源于兰姆达也算,而兰姆达演算啊并不是设计在计算机上执行的。

它是由along the church和stephen cal clean.在二十世纪三十年代啊,引入的一套用于研究函数,定义函数应用和递归的形式系统。

那正如along zhol说的那样,像bullons inteintegers啊或者是其他的数据结果都可以被函数来取代掉。

那我们来看一下函数这个编程。

那它的理念呢就来自于数学中的代数。

比如我假设FX呢是一个函数,比如让FX呢等于五x平方加四x再加三让。

第二个函数GX呢把FX这个函数啊,套下来啊并且展开。

然后呢,我还可以定义一个有两个一元函数组合成的二元函数,那我还可以做递归,比如FX等于FX减一,加上FX减二。

那这个函数呢就代表斐莫纳契数列。

那对于函数式编程来说呢,他只关心定义输入数据和输出数据相关的关系。

那在数学表达式里面啊,其实是在做一种映射。

那输入的数据和输出的数据关系是什么样的?是用函数来定义的那函数式编程有两个特征。

那第一个特征呢是stattliace,嗯,意思呢就是说函数不维护任何的状态,函数式编程的核心性是啊就是stteless.那简单来说呢就是它不能存在状态。

打个比方,你给我数据,我处理完啊,扔出来里面的数据啊是不变的那第二呢是immutable,就是输入数据啊是不能动的啊,动了输入数据啊就有危险,所以呢要返回新的数据集。

因为有了statleless和immusable,所以函数式编程呢就带来了以下几个优势。

首先呢没有状态就没有伤害,那然后呢并行执行没有伤害。

第三呢,copy paste重构代码,无伤害函数的执行啊,没有顺序上的问题。

那这样呢它就带来了以下一些好处。

首先呢是惰性求值,那这个呢需要编译器的知识。

那一个表达式呢可以不在它绑定到变量之后啊,就立即求值,而是在这个值被取用的时候啊再求值。

我举个例子,一个赋值语句啊,也就是x等于expression啊,也就是把一个表达式expression的结果赋值给一个变量x啊,但是呢先不管实际在x中的是什么,直到后面表达式用到了x啊,有了对x的值的需求的时候啊,才会去真正计算这个expression啊,最终呢为了生成让外界看到的某个符号,而计算这个快速增长的依赖数。

那函数式编程的另一个好处呢是确定性。

所谓确定性的,就是像在数学中那样FX等于y这个函数啊,无论在什么场景下都会得到同样的结果,而不是像程序中的很多函数那样,同一个参数在不同场景下会计算出不同的结果。

那这个呢我们就称之为函数的确定性。

所谓不同的场景啊,就是我们的函数啊会根据运行时的状态,信息的不同而发生变化。

我们知道因为有了状态,在并行执行和coffee pace的时候呢,引发bug的概率是非常高的,所以没有状态就没有伤害啊,就像没有依赖就没有伤害一样。

并行执行无伤害啊,拷贝代码呢无伤害,因为没有状态啊,代码怎么样拷都行。

那以上说的都是函数式编程的优势,而函数式编程的劣势呢就在于它的数据复制啊比较严重。

有一些人可能会觉得数据复制会对性能造成影响。

那其实呢这个劣势啊不见得会导致性能不好,因为没有了状态,所以代码在并行上根本不需要锁,所以呢就可以拼命的并发,反而呢可以让性能很不错。

比如a long就是其中的代表。

那对于函数式啊,也就是完全没有状态的函数的编程来说呢,各个语言啊有不一样的知识程度。

那有一些呢是完全的纯函数式啊,比如houstcare.那有一些呢是能比较容易的写出纯函数啊,比如f sharp、 o camel、 cloger,还有scala等等。

那还有一些语言呢写纯函数啊,需要花一些精力啊,比如c sharp、 java和javascript.呃,但是很多人啊并不习惯函数式编程,因为函数式编程啊和过程式编程的思维方式完全不一样。

那过程式编程呢是把具体的流程给描述出来,所以呢可以不假思索的写代码,而函数式编程的抽象程度更大。

那在实现方式上呢,有函数、套函数、函数、返回函数,还有函数里面定义函数等等,这样呢就会把人搞得很糊涂。

那下面呢我们再来谈一谈函数式编程用到的一些技术。

那第一个呢是first cast function啊,也就是头等函数。

那这个技术呢可以让你把函数啊像变量一样来使用。

也就是说呢,你的函数可以像变量一样被创建修改,并且呢当做变量一样传递返回啊,或者是在函数中啊嵌到函数。

那第二个呢是伪递归优化。

我们都知道递归的害处,那就是递归很深的话,stack会受不了。

并且呢还会导致性能大幅度下降。

所以呢我们使用伪递归优化技术,就是在每次递归的时候啊,都来重用这个stack啊,这样呢就能提升性能了啊,当然呢这需要语言或者编译器的支持啊,python呢就不支持。

那第三呢是map和reduce,那这个技术啊就不用多说了。

函数式编程里啊最常见的技术啊就是对一个集合啊,做map和reduce操作和过程式的语言相比啊,这种写法在代码上要更容易阅读。

那这个呢很像CI加STR中for each fandief和countive等一些函数的玩儿法。

那第四呢是pipeline啊,也就是管道。

那这个技术的意思是说呢,将函数实例化啊变成一个一个的action,然后呢把一组action啊放到一个数组或者列表中,再把数据啊传给这个action list.那数据呢就像一个pipeline一样,顺序的被各个函数所操作,最终呢得到我们想要的结果。

那第五呢是递归,递归最大的好处呢就是简化代码。

它可以把一个复杂的问题啊,用很简单的代码描述出来。

那这里需要注意递归的精髓呢是描述问题,而这个啊正是函数式编程的精髓。

那第六呢是科理化,就是将一个函数的多个参数啊分解成多个函数。

然后呢把这些函数啊多层封装起来,每层函数呢都返回一个函数啊去接收下一个参数。

那这样呢可以简化函数的多个参数,在c加加中呢这个很像STL中的band, first和band. Second,那最后第七个技术呢是高阶函数。

所谓高阶函数呢,就是把函数当做参数,把传入的函数呢做一个封装,然后呢返回这个封装函数。

那在现象上呢就是这个函数传进传出,就像面向对象的对象满天飞一样。

那这个技术呢用来做decorator就很不错。

那上面这些技术啊还是太抽象了啊,我们还是从一个最简单的例子开始,我们先来看文中的这段代码。

我先声明了一个全局变量,然后呢我定义了一个函数incrediment,在这个函数里面呢调用加加操作啊,让这个全局变量加一。

所以说这个函数里面啊是有状态的那这个状态啊在外部。

所以呢如果是多线程的话,这里面的代码就是不安全了。

那如果我把它写成纯函数呢,就是让这个incrediment的函数啊接收一个参数,你传给我什么,我就返回这个参数加一。

那这样呢你就会发现代码随便考,而且呢与线程无关。

代码在并行的时候呢不用锁啊,因为是复制了原有的数据,而且啊返回了新的数据。

我们再来看另外一个例子,文中这段python的代码开始有点变得复杂了。

我们可以看到这个例子中的个函数返回了另一个函数ininink x于是呢我就可以用ink函数啊来构造各种版本的in ink函数啊,比如ink二、ink五等等。

那这个技术呢其实就是刚才所说的科底化技术。

那从这个技术上呢,你可能已经体会到了函数式编程的理念。

首先呢就是把函数当成变量来用关注描述问题,而不是怎么实现。

那这样呢可以让代码更加易读。

那其次呢,因为函数返回了里面这个函数,所以呢函数关注的是表达式,关注的是描述这个问题啊,而不是怎么实现这个事情。

那说到函数式语言呢,就不可避免的要说一下list.那下面呢我们再来看一看scheme语言的函数式玩法。

Scheme语言呢是list p的一个方言。

在scheme里呢所有的操作啊都是函数啊,包括加减乘除这样的东西。

所以呢一个表达式的形式啊,一般都是这样啊,函数名参数一,参数二这样。

那举个例子,我们来看文中这三个函数。

那第一个呢是用内置的加号函数啊,定义了一个新的plus函数。

那第二个呢是用内置的乘号函数,定义了一个新的times函数。

那第三个呢是用刚刚定义的time函数啊来定义一个square函数。

那么我们用这个plus times和square函数就可以定义出FX等于五倍x方加十这个函数了。

我们呢还可以换一种方式啊,用兰姆达匿名函数来定义。

那写法呢就是这样。

那在这里呢我们使用兰姆达来定义函数f二,然后呢也同样用lambda啊定义了两个函数plus和times.最后呢用这两个函数啊定义了f二。

我们可以用类似的方式啊定义出一个表示阶乘的函数。

呃,当然呢还可以换一种写法,使用尾滴硅技术啊来实现啊具体的代码,你可以去文章中看一下。

那接下来呢我们来聊一聊函数式编程的思维方式。

那前面呢我提到过多次函数式编程呢,关注的是describe what to do, rather than how to do it.于是呢我们把之前的过程式变成范式,叫做imperative programming啊,也就是指令式编程。

而把函数式编程范式呢叫做declarative programming啊,也就是生命式编程。

那这里呢我们用一个例子来对比一下这两个编程范式。

比如说我们有三辆车来比赛。

那简单起见呢,我们分别给这三辆车百分之七十的概率让他们啊可以往前走一步。

那一共呢有五次机会,然后呢我打出每一次这三辆车的前行状态。

那首先呢我们用传统的指令式编程四位啊来写这段代码。

那么呢我就需要声明两个全局变量啊,分别代表当前的剩余机会和三辆车的位置。

然后呢我写两层循环,在外层循环上呢,让剩余机会递减。

在内层循环呢变利这三辆车,然后呢让他们移动之后呢,再打印出当前的位置。

那当然呢我们可以把这两重循环变成一些函数模块,像文中这样把移动打印还有机会递减,这些操作都抽离成一个一个的函数。

那这样呢能让我们更容易的阅读代码。

那从这段代码的主循环开始,我们可以很清楚的看到整个程序的主干。

因为我们把程序的逻辑啊分成了几个函数。

那这样一来呢,代码逻辑啊就会变成几个小碎片啊,于是我们读代码的时候啊,要考虑的上下文啊就少了很多啊,阅读代码也会更容易。

不像一开始两层循环的写法,如果没有注释和说明啊,你还是需要花点时间啊来理解一下。

而我们把代码逻辑封装成函数之后呢,就相当于给每个相对独立的程序逻辑啊起了个名字。

于是呢代码就成了字解似的。

但是呢你会发现封装成函数之后呢,这些函数啊都会依赖于共享的变量来同步状态。

于是呢在读代码的过程中啊,每当我们进入到函数里读到访问了一个外部变量的时候呢,我们马上要去查看这个变量的上下文。

然后呢还要在大脑里啊推演这个变量的状态,才能知道程序的真正逻辑。

也就是说呢这些函数啊必须知道其他函数是怎么修改它们之间的共享变量的。

所以呢这些函数啊是有状态的。

我们知道有状态的话并不是一件很好的事情啊,无论是对代码重用,还是对代码的并行来说啊,都是有副作用的。

所以呢我们要想一个方法,把这些状态搞搞掉,是是就就出现函函式式编程的变成范式。

那这段逻辑用函数式的方式应该怎么写呢?啊,你可以看一下文中的写法。

那这段代码呢依然把程序的逻辑啊分成了函数。

不过呢这些函数啊都是函数式的。

呃,它们呢有三个特点,一呢是它们之间啊没有共享的变量。

二呢是函数之间啊通过参数和返回值来传递数据。

那第三呢是在函数点没有临时变量。

另外呢我们还可以看到for循环被递递归取代啊,正如前面所说说的,递的本本呢就是描述问题啊是什么。

那说到函数式语言呢,就不得不提它的三套件map reduce和filter.那这个呢我们在谈c加加泛径编程的时候啊,就已经介绍过了。

那接着呢我们再来看一下python语言中的一个例子。

那这个例子呢需求就是我们先把一个字符串数组啊里面的字符串都转成小写。

那如果用常规的面向过程的方式呢,就是用一层for循环啊,遍历一下这个字符串数组在for循环里面啊,做小写的转换。

那如果我要写成函数式呢,就可以对这个字符串数组啊,用一个map函数对数组的每一项做一个转换。

那顺便说一下这个例子啊,是不是和我们CN加STL中的transform函数啊,有点像呢。

那在刚刚这个python的例子中啊,我们可以看到我们定义了一个函数to upper.那这个函数呢没有改变传进来的值啊,只是把传进来的值啊做个简单的操作,然后呢直接返回。

所以呢我们把它用在map函数中,就可以很清楚的描述出我们想要干什么啊,而不是去理解一个在循环中怎么实现的代码。

那最终呢在读了很多循环的逻辑之后呢,才发现啊是什么意思?那如果你觉得上面的代码在传统的非函数的方式下还是很容易读的。

那么我们再来看一个计算数组平均值的代码。

那这个代码呢我放到文中了,你会发现啊,如果没有注释呢,你需要看一会儿才能明白。

呃,它只是在计算数组中正数的平均值而已。

那如果我把它改写成函数式的玩法,用filter和reduce函数来实现,你会发现啊只需要两行代码就可以完成了。

呃,首先呢我们使用reduce函数,把正数呢过滤出来啊保存在一个新的数组positive number中。

然后呢我们使用reduce函数,对这个positive number数组啊进行求和啊,再除以它的个数啊,就能得到正数的平均值了。

我们可以看到隐藏的数组遍历并过滤数组控制流程的filter和reduce,不仅让代码更为简洁。

因为代码里啊只有业务逻辑了啊,所以呢还能让我们更容易的理解代码。

你看第一行就是对纳姆数组啊做一个filter,条件是x大于零。

那第二行呢就是对positive number啊进行x加y操作的reduce啊,也就是求和是不是感觉代码更加亲切了。

首先呢数据集对数据的操作和返回值啊都放在了一起。

那其次呢没有了循环体,就可以少一些临时用来控制程序,执行逻辑的变量也少了,把数据倒来倒去的控制逻辑。

那最后呢代码变成了在描述你要干什么,而不是怎么干。

那当然呢,如果你是第一次见到mamap reduce和filter,那你可能还是会有点陌生和不解。

那这个呢只是你不了解罢了。

那对于函数式编程的思路呢,文中啊有一个比较形象的图片,把面包和蔬菜map到切碎的操作上,再把结果给reduce成汉堡。

在这张图中呢,我们可以看到map和reduce啊,不关心原输入数据。

它们呢只是控制,并不是业务控制啊是描述怎么干,而业务呢是描述要干什么。

那以上呢就是函数式语言的三件套,map reduce和filter.最后呢我们来谈一谈函数式的pipeline模式啊,也就是管道模式。

那pipeline呢借鉴于unix shell的管道操作,就是把若干个命令啊串起来。

前面命令的输出呢成为后面命令的输入,如此呢完成一个流式计算。

那pipeline呢绝对是一个伟大的发明。

它的设计哲学呢就是KISS,让每个功能就做一件事儿,并把这件事儿做做极致。

软件和程序的拼装啊会变得更为简单和直观。

那这个设计理念呢影响非常深远,包括今天的web service、云计算以及大数据的流式计算等等。

那回过来我们先来看一段shell命令。

那这个命令呢只有一行,它用管道操作啊,PS auc、 salort和equo这四个linux命令串联起来。

先查看一个用户执行的进程。

列表列出来以后呢,取第二列,那第二列呢是它的进程ID,然后呢再排个序把它显示出来。

那抽象成函数式的样子呢,我们就可以反过来一层套一层,我们呢也可以把函数放进数组里面,然后顺序执行一下。

那这里呢我多说一句,那如果我们把这些函数比作微服务,那么管道这个事儿是在干什么呢?啊,其实呢就是在做服务的编排,像unix这些经典的技术上的实践或者理论啊,往往呢是可以反映到分布式架构的。

所以呢一般来说一个好的分布式架构师啊,通常呢都是对这些传统的微观上的经典技术啊有非常深刻的认识。

因为这些东西啊在方法论上都是相通的。

好了,还是让我们用一个简单的示例啊,来看一下如何实现pipeline.我们先来看一个程序,那这个程序的process函数啊有三个步骤啊,第一步呢找出偶数,第二步呢乘以三,第三步呢,把它转成字符串再返回。

那如果我用传统的非函数的方法来实现,可以得到文中这样的代码和输出结果。

我们可以看到输出的结果呢并不够完美。

另外呢,在代码阅读上,如果没有注释呢,你也会比较晕。

那我们来看一看函数式的pipeline应该怎么写?那可以像文中这样,那第一步呢先把三个子需求啊写成函数。

第二步呢,用嵌套的方式把这三个函数啊串联起来。

那在这里呢我们动用了python的关键字yelt,它呢是一个类类似return的关键词啊,只是这个函数返回的是一个generator啊,也就是一个生成器。

那所谓生成器呢,指的是业务的返回大,是一个可迭代的对象啊,并没有真正的执行函数。

也就是说呢只有它返回的迭代对象被迭代的时候,已有的函数啊才会真正的运行。

那运行到业务的语句的时候呢,就会停住,然后等下一次的迭代,那这个呢就是lazy evaluation啊,意思呢就是懒惰加载。

那好了,根据前面的原则呢,使用map和reduce不要使用循环。

我们啊用比较朴素的map和reduce来实现一下。

你会发现这个代码是不是更容易读了。

但是还是有一个问题啊,就是它需要通过嵌套来使用函数,那这个呢有点令人不爽。

那如果能把函数的嵌套改造成并列的方式啊就好了。

那这个呢我们可以通过对函数做reduce来实现。

那当然呢我们还可以使用python的four参数和decorator模式,把这段代码写的更像管道。

那可以参考文中最终的实现效果。

嗯,好了,这节课到这里呢就结束了,我们来做一个小小的总结。

相对于计算机发展史来说呢,函数式编程啊是个非常古老的概念。

它的核心思想呢是将运算过程啊尽量写成一系列嵌套的函数调用,关注的是做什么,而不是怎么做。

所以呢又被称为生命式编程。

它以无状态和不可变为主要的特点,代码简洁,易于理解,便于并行执行,易于做代码重构。

那函数执行呢没有顺序上的问题,支持惰性求值,还具有函数的确定性。

那所谓确定性呢,就是无论在什么场景下都会得到同样的结果。

那我们这节课呢结合递归map和reduce,还有pipeline技术,对比了非函数式编程和函数式编程,在解决相同问题时的不同处理思路,让你对函数式编程范式呢有了清晰明确的认知。

我在最后呢还引入了decorator休止器,那这样呢就使得普通函数管道化啊成为了一件轻而易举的事情。

那此时啊你可能会有疑问,decorator啊到底是什么呢?怎样使用它呢?那敬请关注下一讲中的内容来得到这些答案。

那了解了这么多函数式编程的知识啊,我想请你深入思考一个问题,你是偏好在命令式编程语言中使用函数式编程风格呢,还是坚持使用函数式语言编程呢?那原因呢又是什么?欢迎在评论区留言呢和我一起来探讨。

文末呢是编程范式游记系列文章的目录啊,方便你们了解这一系列内容的全貌。