函数式编程

昨天看RxJava时提到了函数式编程:

今天在看极客时也遇到了讲解:

虽然C语言简单灵活,能够让程序员在高级语言特性之上轻松进行底层上的微观控制,被誉为 高级语言中的汇编语言 ,
但其基于过程和底层的设计初衷又成了它的短板.

在程序世界中,编程工作更多的是解决业务上的问题,而不是计算机的问题,我们需要更为贴近业务,更为抽象的语言,如典型的面向对象语言C++和Java等.

C++很大程度上解决了C语言中的各种问题和不便,尤其是通过类,模板,虚函数和运行时识别等解决了C语言的泛型编程问题.
然而,如何做更为抽象的泛型呢?答案就是函数式编程(Functional Programming)

相对于计算机的历史而言,函数式编程其实是一个非常古老的概念.函数式编程的基础模型来源于λ演算,而λ演算并没有被设计在计算机上执行.
我们来看一下函数式编程,它的理念就来自于数学中的代数

f(x)=5x^2+4x+3
g(x)=2f(x)+5=10x^2+8x+11
h(x)=f(x)+g(x)=15x^2+12x+14

假设f(x)是一个函数,g(x)是第二个函数,把f(x)这个函数套下来,并展开.然后还可以定义一个由两个一元函数组合成的二元函数,还可以做递归,下面这个函数定义就是斐波那契数列

f(x)=f(x-1)+f(x-2)

对于函数式编程来说,它只关心定义输入数据和输出数据相关的关系,数学表达式里面其实是在做一种映射(mapping),
输入的数据和输出的数据关系是什么样的,是用函数来定义的.

函数式编程有以下特点:

特征

  • stateless: 函数不维护任何状态.函数式编程的核心精神是stateless,简而言之就是它不能存在状态,打个比方,你给我数据我处理完扔出来.里面的数据是不变的.
  • immutable: 输入数据是不能动的,动了输入数据就有危险,所以要返回新的数据集.

优势:

  • 没有状态就没有伤害
  • 并行执行无伤害
  • Copy-Paste重构代码无伤害
  • 函数的执行没有顺序上的问题

函数式编程还带来了以下一些好处:

  • 惰性求值:
    这需要编译器的支持,表达式不在它被绑定到变量之后就立即求值,而是在该值被取用的时候求值.
    也就是说,语句如x:=expression;(把一个表达式的结果赋值给一个变量)显式地调用这个表达式被计算并把结果放置到x中,
    但是先不管实际在x中的是什么,直到通过后面的表达式中到x的引用而有了对它的值的需求的时候,而后面表达式自身的求值也可以被延迟,
    最终为了生成让外界看到的某个符号而计算这个快速增长的依赖树.

  • 确定性:
    所谓确定性,就是像在数学中那样,f(x)=y这个函数无论在什么场景下,都会得到同样的结果,而不是像程序中的很多函数那样.同一个参数,在不同
    的场景下会计算出不同的结果,这个我们称之为函数的确定性.所谓不同的场景,就是我们的函数会根据运行中的状态信息的不同而发生变化.

We know,because of the "state",在并行执行和copy-paste时引发bug的概率是非常高的,所以没有状态就没有伤害,就像没有依赖就没有伤害一样.
并行执行无伤害,copy代码无伤害,因为没有状态,代码怎样copy都行.

劣势:

  • 数据复制比较严重
    有一些人可能会觉得这会对性能造成影响,其实,这个劣势不见得会导致性能不好.因为没有状态,所以代码在并行上根本不需要锁(不需要对状态修改的锁),
    所以可以拼命地并发,反而可以让性能很不错. 比如: Erlang就是其中的代表.

对于纯函数式(也就是完全没有状态的函数)的编程来说,各个语言支持的程度如下:

  • 完全纯函数式 Haskell
  • 容易写纯函数 F#,Ocaml,Clojure,Sala
  • 纯函数需要花点精力 C#,Java,JavaScript

完全纯函数的语言呢,很容易写成函数,纯函数需要花精力.只要所谓的纯函数的问题,传进来的数据不改,改完的东西复制一份拷出去,然后没有状态显示.
But most people 并不习惯函数式编程,因为函数式编程和过程式编程的思维方式完全不同.
过程式编程是在把具体的流程描述出来,所以可以不假思索,而函数式编程的抽象度更大,在实现方式上,有
函数套函数, 函数返回函数, 函数里定义函数 , 把人搞得confused

函数式编程用到的技术:
下面是函数式编程用到的一些技术:

  • first class function (头等函数) :
    这个技术可以让你的函数就像变量一样来使用. 也就是说,你的函数可以像变量一样被创建,修改,并当成变量一样传递,返回.
    或是在函数中嵌套函数.
  • tail recursion optimization (尾递归优化) :
    递归的害处呢在于如果递归很深的话呢,stack受不了,并导致性能大幅度下降,因此,我们使用尾递归优化技术--每次递归时都重用
    stack,这样能够提升性能哦.当然这需要语言或编译器的支持呢. Python 就不支持.
  • map & reduce :
    这个技术不用多说了,函数式编程最常见的技术就是对一个集合做Map和Reduce操作了,这比起过程式的语言来说,在代码上要更容易阅读
    (传统过程式的语言需要使用for/while循环,然后在各种变量中把数据倒过来倒过去的)这个很像C++STL中foreach啊,find_if啊,count_if等函数的玩法.
  • pipeline (管道) :
    这个技术的意思是,将函数实例成一个一个的action,然后将一组action放到一个数组或是列表中,再把数据传给这个action list,数据就像一个pipeline一样
    顺序地被各个函数所操作,最终得到我们想要的结果.
  • recursing (递归) :
    递归最大的好处呢就是简化代码,它可以把一个复杂的问题用很简单的代码描述出来. 注意:递归的精髓是描述问题,而这正是函数式编程的精髓.
  • currying (柯里化) :
    将一个函数的多个参数分解成多个函数,然后将函数多层封装起来,每层函数都返回一个函数去接收下一个参数,这可以简化函数的多个参数.
    在C++中呢,这很像STL中的bind1st或是bind2nd.
  • higher order function (高阶函数) :
    所谓高阶函数就是函数当参数,把传入的函数做一个封装,然后返回这个封装函数,现象上就是函数传进传出,就像面向对象对象满天飞一样.
    这个技术用来做Decorator很不错呢.

!!!!

函数式编程的思维方式
前面提到多次,函数式编程关注的是: describe what to do,rather than how to do it.
于是,我们把以前的过程式编程范式叫做 Imperative Programming - 指令式编程,而把函数式编程范式叫做 Declarative Programming - 声明式编程.

传统方式的写法:
下面我们看一下相关的示例.
比如,我们有3辆车比赛,简单起见,我们分别给这3辆车70%的概率让它们可以往前走一步,一共5次机会.然后打出这一次这3辆车的前行状态:
对于 Imperative Programming 来说,代码如下: Python

from random import random

time = 5
car_positions = [1,1,1]

while time:
      # decrease time
      time -= 1
      
      print ''
      for i in range(len(car_positions)):
            # move car
            if random() > 0.3:
                  car_positions[i] += 1

            # draw car
            print '-' * car_positions[i]

经过函数模块化: 更容易地阅读代码:


from random import random

time = 5
car_positions = [1,1,1]

def run_step_of_race():
      global time
      time -= 1
      move_cars()

def move_cars():
      for i, _ in enumerate(car_positions):
            if random() > 0.3:
                  car_positions[i] += 1

def draw():
      print ''
      for car_position in car_positions:
            draw_car(car_position)

def draw_car(car_position):
      print '-' * car_position

上面代码,从主循环开始,可以清楚看到程序主干,因为哦我们把程序的逻辑分成了几个函数. 这样一来呢
代码逻辑就会变成几个小碎片,于是我们读代码时要考虑上下文就少了很多,阅读代码也会更容易.
不像第一个示例.
而将代码逻辑封装成函数后,我们就相当于给每个相对独立的程序逻辑取了个名字,于是代码成了自解释的.
但是你会发现哦,封装成函数后,这些函数都会依赖于共享的变量来同步其状态.
于是在读代码过程时,每当进入到函数里,读到访问了一个外部的变量时,我们马上要去查看这个变量的上下文,
然后还要在大脑里推演这个变量的状态,才能知道程序的真正逻辑.也就是说,
这些函数必须知道其它函数是怎样修改它们之间共享变量的,所以这些函数是有状态的.

函数式的写法 见下篇:
https://www.cnblogs.com/ukzq/p/13770596.html

原文地址:https://www.cnblogs.com/ukzq/p/13767952.html