函数式编程

技术 · 2019-05-16

本期主题:函数式编程

引言:
函数式编程,其实是一种“编程范式”。那函数式编程与我们之前遇到的过程式编程,以及面向对象编程又有什么区别,使用过程中又该如何选择。所以,我们需要了解函数式编程(Functional Programming)是什么,它怎么来的,如果真的好用为什么现在没有被广泛使用,闭包、高阶函数、惰性计算、柯里化、无副作用都是什么含义。

起源
一个哲学问题:什么是完美?现实主义者会认为没有完美的存在,因为总有一些我们无法看到的瑕疵。那么问题来了,如果不存在“完美”,那又如何理解“完美”这个词的定义呢。这样就有了数学的本质,数学可以画出一个完美的圆,数学用定律描述宇宙。
然后就不得不提到一个数学家Alonzo Church(他的三个哥们:艾伦·图灵、约翰·冯·诺依曼和库尔特·哥德尔),Alonzo Church 与其他人合作开发了一种称为lambda 演算的正式系统,λ是基于将其他函数作为参数并返回函数作为结果的函数。λ的发展却是由于二次世界大战,陆军聘请了大量数学家来不断计算求解大炮弹道射击表所需的微分方程。1949 年,电子离散变量自动计算机 ( EDVAC ) 面世,冯·诺依曼架构的图灵机面世。1958年,麻省理工学院教授约翰·麦卡锡推出了列表处理语言 (Lisp),就是λ验算的实现。
(业余知识:邱奇数(一个特征值,函数的处理次数);图灵是邱奇的学生,二战时帮助英国破解了德国的密码系统;图灵逻辑(任何复杂的操作,都可以由01这种简单操作完成,三体);)
函数式编程
定义
举个例子:要计算(1+2)*3-4,对于java来说就是:
int a = 1 + 2;
int b = a * 3;
int c = b - 4;

而对于函数式编程,就需要把实现过程定义为一些函数,比如:
int result = substract(multiply(add(1, 2), 3), 4);

用java的函数式写会更清晰:
Function<Integer, Integer> add = a -> a + 2;
Function<Integer, Integer> multiply = b -> b * 3;
Function<Integer, Integer> substract = c -> c - 4;

int result = substract.apply(multiply.apply(add.apply(1)));

特点

  • 惰性计算

比如Supplier a = () -> 10 +1; int b = a.get() +1;在a.get()时才会计算

  • 高阶函数

使用函数,作为函数的入参或者出参。函数是“第一公民”

  • 只用“表达式”,不用“语句”

保留计算过程的单纯性,把I/O的限制降到最小,减少不必要的读写

  • 没有“副作用”

函数保持独立,不修改外部变量的值

  • 不修改状态

最好的例子是递归,不修改变量状态。比如:将字符串reverse

  • 引用透明性

对于函数而言,提供同样的输入,永远会得到同样的结果
优势

  1. 代码简洁,开发迅速
    比如写一个线程:Runnable r = () -> System.out.println(message);同时,大量函数的使用也减少了重复代码的使用。
  2. 更容易理解,更符合阅读习惯
  3. 单元测试
    函数会保持一致性,更利于单元测试
  4. 易于并发
    因为函数不会修改函数外变量,所以不会产生死锁。
  5. 热代码部署
    柯里化
    在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel 和 Gottlob Frege 发明的。
    柯里化并不复杂,高中数学就有所涉及,比如:F(x) = f(g(x));
    作用

    1. 惰性求值

    在第一次调用时,只会返回一个函数,第二次调用才会真正执行计算。

    1. 动态生成函数

    比如要计算一个数字的n次幂。我们可以定义一个方法:
    function power(n){
    return function (number){

     let result = 1;
     for(let i = 0; i < n; ++i){
         result *= number;
     }
     return result;

    }
    }

然后求平方,就可以:
let p2 = power(2);
p2(4) // 输出16
p2(5) // 输出25

求5次方,就可以:
let p3 = power(3);
p3(4) // 输出64
p3(5) // 输出125

扩展
适配器模式 adapter
在计算机编程中,适配器模式(有时候也称包装样式或者包装)将一个类的接口适配成用户所期待的。
比如中国与美国的电压制式不同,那同样的电脑如何在不同的电压下运行,就需要电源适配器,将不同的电压转化为输出相同的电流。代码的接口层也是如此。
外观模式 facade
外观模式(Facade Pattern)隐藏系统的复杂性,并向客户端提供了一个客户端可以访问系统的接口。
比如电脑开机,需要电源供电、主板自检、操作系统载入、cpu运行、驱动加载、外部设备接入等等,而实际操作中,用户只需要点击一下开机键即可。
Java的应用
抽象 vs 高内聚&低耦合
越抽象,意味着性能越低。机器直接执行的汇报指令性能最强,C次之,java由于较高的抽象层次,底层数据增删查改接口性能最好,上层业务接口由于各种业务校验,性能较低。
对性能的顾虑,制约了程序员对模块更合理的抽象。

举个例子:
“用户”是系统的常见实体,所以我们定义一个通用领域模块“User”,除了常见的信息如用户id外,我们重点关注两个字段:用户的部门department,用户的权限permission。
public class User{

//用户id
private Long uid;
//用户部门,来自于远程调用
private String department;
//用户权限,来自于远程调用
private Set<String> permission;

}

这看起来很棒,把用户的属性都集中到一个实体中,只要将这个User作为方法参数,方法就不需要再查询用户的其他信息了。但是,问题在于用户部门和权限都来自于远程调用,所以每次构造User,就需要付出两次远程调用的代价,即使这个信息本次用不到。再举个例子:
public boolean ifSameDepartment(User u1, User u2){

return Objects.equals(u1.getDepartment(), u2.getDepartment());

}

需要判断两个user的部门是否相同,但是必须执行远程调用,获取用户权限信息,如果远程接口报错,还会影响其他无关的接口。
那该如何做呢,放弃抽象,改为用uid游走在各个系统之间,又不是一个好办法。
其实,上面的代码中,把部门和权限改为惰性加载,只在需要的时候调用,就可以解决了。
Lazy

public class Lazy implements Supplier {

private final Supplier<? extends T> supplier;
private @Nullable T value = null;
private boolean resolved = false;

public T get() {
   T value = getNullable();
   if (value == null) {
      throw new IllegalStateException("Expected lazy evaluation to yield a non-null value but got null!");
   }
   return value;
}


@Nullable
public T getNullable() {
   T value = this.value;

   if (this.resolved) {
      return value;
   }

   value = supplier.get();

   this.value = value;
   this.resolved = true;

   return value;
}

}

再来看前面的User例子:
public class User{

//用户id
private Long uid;
//用户部门,来自于远程调用
private Lazy<String> department;
//用户权限,来自于远程调用
private Lazy<Set<String>> permission;

}

构造方法就变成了
long uid = 1L;
User user = new User();
user.setUid(uid);
user.setDepartment(Lazy.of(() -> departmentService.getDepartment(uid)));

user.setPermission(Lazy.of(() -> permissionService.getPermission(user.getDepartment().get())));//???????????????

问题又来了,user的部门是根据uid获取,但是用户权限,却是根据部门获取,那又该如何解决呢?
函子Functor
import java.util.function.Function;
public interface Functor {

<R> Functor<R> map(Function<T, R> f);

}

特点:

  1. 不直接操作值,而是操作函子
  2. 函子是一个实现了map方法的接口
  3. 函子中对值的操作,均通过map方法实现
  4. 通过map方法变形后,获得另一个函子,实现函子的嵌套
    对函子的理解,建议参考文章:图解 Monad - 阮一峰的网络日志
    结合上栗,permission的赋值就变成了:
    public User buildUser(long uid) {

     Lazy<String> departmentLazy = Lazy.of(() -> departmentService.getDepartment(uid));
     // 通过部门获得主管
     // department -> supervisor
     Lazy<Long> supervisorLazy = departmentLazy.map(
         department -> SupervisorService.getSupervisor(department)
     );
     // 通过部门和主管获得权限
     // department, supervisor -> permission
     Lazy<Set<String>> permissionsLazy = departmentLazy.flatMap(department ->
         supervisorLazy.map(
             supervisor -> permissionService.getPermissions(department, supervisor)
         )
     );
     
     User user = new User();
     user.setUid(uid);
     user.setDepartment(departmentLazy);
     user.setSupervisor(supervisorLazy);
     user.setPermissions(permissionsLazy);

    }

小Tip:虽然user.getPermissions变成了类似的一个内存服务,但实际还是会调用远程服务,还是可能会出现各种异常,比如接口超时等。建议在外层进行统一异常判断和处理。
函数式接口

Consumer< T > 消费性接口 (有来无回),即有参数没有返回值

调用方法:void accept(T t)

Predicate< T > 段言型,有参数,返回值为boolean类型

调用方法:boolean test(T t)

Function< T,R > 函数型接口,有参数有返回值,皆为任意类型

调用方法:R apply(T t)

Supplier 供给型接口,无参数,有返回值

调用方法:T get()

后记
函数式编程有具体应用的价值,还是只是个语法糖呢?
比如:
Identity<byte[]> idBytes = new Identity<>(customer)

    .map(Customer::getAddress)
    .map(Address::street)
    .map((String s) -> s.substring(0, 3))
    .map(String::toLowerCase)
    .map(String::getBytes);

等价于:
byte[] bytes = customer

    .getAddress()
    .street()
    .substring(0, 3)
    .toLowerCase()
    .getBytes();

好处:

  1. 函子抽象了内部描述,代码黑盒更明确,工厂模式更加灵活
  2. 函数非阻塞
  3. golang号称百万并发,两大核心:异步回调 和 协程。所以函数式编程就是异步回调的最佳实践,把函数作为参数,传递到另一个线程,比如Runable、Callable
    (协程的本质就是函数,线程在等待IO时会阻塞,协程会进行其他操作,将IO操作改为异步回调)
Theme Jasmine by Kent Liao