函数式编程杂谈

本文首发于 vivo互联网技术 微信公众号 
连接: https://mp.weixin.qq.com/s/gqw57pBYB4VRGKmNlkAODg
做者:张文博

比起命令式编程,函数式编程更增强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断演进,逐层推导出复杂的运算。本文经过函数式编程的一些趣味用法来阐述学习函数式编程的奇妙之处。html

1、编程范式综述

编程是为了解决问题,而解决问题能够有多种视角和思路,其中普适且行之有效的模式被归结为“编程范式”。编程语言突飞猛进,从汇编、Pascal、C、C++、Ruby、Python、JS,etc...其背后的编程范式其实并无发生太多变化。抛开各语言繁纷复杂的表象去探究其背后抽象的编程范式能够帮助咱们更好地使用computer进行compute。python

1.命令式

计算机本质上是执行一个个指令,所以编程人员只须要一步步写下须要执行的指令,好比:先算什么再算什么,怎么输入怎么计算怎么输出。因此编程语言大多都具有这四种类型的语句:git

  1. 运算语句将结果存入存储器中以便往后使用;
  2. 循环语句使得一些语句能够被反复运行;
  3. 条件分支语句容许仅当某些条件成立时才运行某个指令集合;
  4. 以及存有争议的相似goto这样的无条件分支语句。

使得执行顺序可以转移到其余指令之处。github

不管使用汇编、C、Java、JS 均可以写出这样的指令集合,其主要思想是关注计算机执行的步骤,即一步一步告诉计算机先作什么再作什么。因此命令式语言特别适合解决线性的计算场景,它强调自上而下的设计方式。这种方式很是相似咱们的工做、生活,由于咱们的平常活动都是循序渐进的顺序进行的,甚至你能够认为是面向过程的。也比较贴合咱们的思惟方式,所以咱们写出的绝大多数代码都是这样的。算法

2.声明式

声明式编程是以数据结构的形式来表达程序执行的逻辑,它的主要思想是告诉计算机应该作什么,但不指定具体要怎么作(固然在一些场景中,咱们也仍是要指定、探究其如何作)。SQL 语句就是最明显的一种声明式编程的例子,例如:“SELECT * FROM student WHERE age> 18”。由于咱们概括剥离了how,咱们就能够专一于what,让数据库来帮咱们执行、优化how。数据库

有时候对于某个业务逻辑目前没有任何能够概括提取的通用实现,咱们只能写命令式编程代码。当咱们写成之后,若是进行思考概括抽象、进一步优化,就为之后的声明式作下铺垫。编程

经过对比,命令式编程模拟电脑运算,是行动导向的,关键在于定义解法,即“怎么作”,于是算法是显性而目标是隐性的;声明式编程模拟人脑思惟,是目标驱动的,关键在于描述问题,即“作什么”,于是目标是显性而算法是隐性的。json

3.函数式

函数式编程将计算机运算视为函数运算,而且避免使用程序状态以及易变对象。这里的“函数”不是指计算机中的函数,而是指数学中的函数,即自变量的映射。也就是说一个函数的值仅决定于函数参数的值,不依赖其余状态。好比f(x),只要x不变,不论何时调用,调用几回,值都是不变的。比起命令式编程,函数式编程更增强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断演进,逐层推导出复杂的运算,而不是设计一个复杂的执行过程。函数做为一等公民,能够出如今任何地方,好比你能够把函数做为参数传递给另外一个函数、还能够将函数做为返回值。数组

函数式编程的特色:promise

  1. 减小了可变量的声明,程序更为安全;
  2. 相比命令式编程,少了很是多的状态变量的声明与维护,自然适合高并发多线程并行计算等任务,我想这也是函数是编程近年又大热的重要缘由之一;
  3. 代码更为简洁,可是可读性是高是低也依赖于不一样场景、仁者见仁智者见智。

2、函数式编程的一些趣味用法

1.Closure(闭包)

public class OutClass {

  private void helloWorld() {
    System.out.println("Hello World!");
  }

  public InnerClass getInnerClass() {
    return new InnerClass();
  }

  public class InnerClass {
    public void hello() {
      helloWorld();
    }
  }

  /**
   * @param args
   */
  public static void main(String[] args) {
    // 在外部使用OutClass的private方法
    new OutClass().getInnerClass().hello();
  }
}

在Java中有不少方式实现上述目的,由于咱们的做用域和JS有着巨大差别。可是借鉴闭包的原理,咱们来看一个场景。假设接口A有一个方法m;接口B也有一个同名的方法m,两个方法的签名彻底同样可是功能却不同。类C想要同时实现接口A和接口B中的方法。由于两个接口中的方法签名彻底一致,因此C只能有一个m方法,这种状况下应该怎么实现需求呢?

public class C implements A {

  @Override
  public void m() {
    //...
  }

  private void o() {
    //...
  }

  public D getD() {
    return new D();
  }

  class D implements B {
    @Override
    public void m() {
      o();
    }
  }

  public static void main(String[] args) {
      C c = new C();
      c.m();
      c.getD().m();
  }
}

2.Currying(柯里化)

我对柯里化(Currying)的理解:柯里化函数能够接收一些参数,接收了这些参数以后,该函数并非当即求值,而是继续返回另外一个函数,刚才传入的参数在函数造成的闭包中被保存起来,待到函数真正须要求值的时候,以前传入的全部参数都能用于求值。

下面先经过JS(我的感受经过JS比较好理解)对柯里化有一个直观的认识。

var calculator = function(x, y, z){
    return(x + y)* z;
}

调用:calculator( 2, 7, 3);

柯里化写法:

var calculator=function(x){
  return function(y){
    return function(z){
      return(x + y)* z;
    };
  };
};

调用:calculator(2)(7)(3);

经过对比,咱们发现柯里化的数学描述应该相似这样,calculator(2, 7, 3) ---> calculator(2)(7)(3)。

如今咱们来回头看看柯里化较为学术的定义,是把接受多个参数的函数变换成接受一个单一参数的函数,而且返回接受余下的参数的新函数,这个新函数最后还能返回全部输入的运算结果。

Java 中的柯里化实现

Function<Integer, Function<Integer, Function<Integer, Integer>>> currying =
    new Function<Integer, Function<Integer, Function<Integer, Integer>>>() {

    @Override
    public Function<Integer, Function<Integer, Integer>> apply(Integer x) {
        return new Function<Integer, Function<Integer, Integer>>() {

            @Override
            public Function<Integer, Integer> apply(Integer y) {

                return new Function<Integer, Integer>() {
                    @Override
                    public Integer apply(Integer z) {
                        return (x + y) * z;
                    }
                };
            }
        };
    }
};

//在这里,咱们能够发现,虽然依次输入二、7,可是咱们并不会计算结果,而是等到最后输入结束时才会返回值。
Function function1 = curryingFun().apply(2);//返回的是函数
Function function2 = curryingFun().apply(2).apply(7);//返回的是函数
Integer value = curryingFun().apply(2).apply(7).apply(3);//参数所有输入,返回最后的值

柯里化的争论

(1)支持的观点

  • 延迟计算,只有在最后的输入结束才会进行计算;
  • 当你发现你要调用一个函数,而且调用参数都是同样的状况下,这个参数就能够被柯里化,以便更好的完成任务;
  • 优雅的写法,语义更有表达力;

(2)不过也有一些人持反对观点,参数的不肯定性、排查错误困难。

3.Promise

Promise 是异步编程的一种解决方案,比传统的诸如“回调函数、事件”解决方案,更合理和更强大。ES6已经普遍应用。我在这里主要分析两个最多见的用法。

  • then

Promise实例生成之后,能够用then方法分别指定resolved状态和rejected状态的回调函数。then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。所以能够采用链式写法,即then方法后面再调用另外一个then方法。

promise.then(function(value) {
 // success
}, function(error) {
 // failure
}).then(...);
  • all

Promise.all方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.all([p1, p2, p3]);

上面代码中,Promise.all方法接受一个数组做为参数,p一、p二、p3都是 Promise 实例,p的状态由p一、p二、p3决定,分红两种状况。

  • 只有p一、p二、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p一、p二、p3的返回值组成一个数组,传递给p的回调函数。
  • 只要p一、p二、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

下面是一个具体的例子:

// 生成一个Promise对象的数组
const promises = [1,2,3.....].map(function (id) {
  return getJSON('/post/' + id + ".json");
});

Promise.all(promises).then(function (posts) {
  // ...
}).catch(function(reason){
   // ...
});

Java的实现

Java中的使用方法目前确实不如js方便,能够看看CompletableFuture,给咱们提供了一些方法。

4.Partial Function

其定义以下:当函数的参数个数太多,能够建立一个新的函数,这个新函数能够固定住原函数的部分参数,从而在调用时更简单。下面是基于Python的实现。我的以为,最大的便利就是避免咱们再去写一些重载的方法。不过暂时没有看到partial的Java版本。看到这里,你们确定认为“偏函数”这个翻译实在是不许确,若是直译过来叫“部分函数”好像也不怎么清晰,咱们姑且仍是称其为Partial Function。

# !/usr/bin/python
# -*- coding: UTF-8 -*-
from functools import partial
def multiply(x, y):
  return x * y
print(multiply(3,4))# 输出12

multiply4 = partial(multiply, y =4)# 不须要定义重载函数
print(multiply4(3))# 输出12

5.map/reduce

Java如今对map、reduce也作了支持,特别是map已是你们平常编码的利器,相信你们也都不陌生了。map(flatMap)按照规则转换输入内容,而reduce则是经过某个链接动做将全部元素汇总的操做。可是在这里我仍是使用Python的例子来进行阐述,由于我以为Python看起来更简洁明了。

# !/usr/bin/python
# -*- coding: UTF-8 -*-
from functools import reduce

def addTen(x):
    return x + 10

def add(x, y):
    return x + y

r = map(addTen, [1, 2, 3, 4, 5, 6, 7, 8, 9])
print r  #[11, 12, 13, 14, 15, 16, 17, 18, 19]
total = reduce(add, r)
print total #[11, 12, 13, 14, 15, 16, 17, 18, 19]加和等于135

6.divmod

divmod是Python的函数,我之因此专门来说述,是由于它所表明的思想确实新颖。函数会把除数和余数运算结果结合起来返回,以下。不过Java确定不支持。

//把秒数转换成时分秒结构显示
def parseDuration( seconds ):
    m, s = divmod(int(seconds), 60)
    h, m = divmod(m, 60)
    return  ("%02d:%02d:%02d" % (h, m, s))

3、关于Scala

上述不少特性,Scala都提供了支持,它集成了面向对象编程和函数式编程的一些特性,感兴趣的同窗能够了解一下。以前看过介绍,Twitter对于Scala的应用比较多,推荐阅读 Twitter Effective Scala 。

4、结语:咱们为何要学习函数式编程

在不少时候,无能否认命令式编程很好用。当咱们写业务逻辑时会书写大量的命令式代码,甚至在不少时候并无能够概括抽离的实现。可是,若是咱们花时间去学习、发现能够概括抽离的部分使其朝着声明式迈进,结合函数式的思惟来思考,能为咱们的编程带来巨大的便捷。

经过其余语言来举一反三函数式编程的奇技淫巧,确实能带给咱们新的视野。我相信随着机器运算能力不断提高、底层能力更加完善,咱们也须要跳出如何作的思惟限制,更多地站在更高的抽象层去思考作什么,方能进入一个充满想象、神奇的computable world。

更多内容敬请关注 vivo 互联网技术 微信公众号

注:转载文章请先与微信号:labs2020 联系。

相关文章
相关标签/搜索