函数式编程杂谈

本文首发于vivo互联网技术微信公众号链接:https://mp.weixin.qq.com/s/gqw57pBYB4VRGKmNlkAODg作者:张文博比起命令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断演进,逐层推导出复杂的运算。本文通过函数式编程的一些趣味用法来阐述学习函数式编程的奇妙之处。一、编程范式综述编程是为了解决问题,而解决问题可以...

函数式编程杂谈

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

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

一、编程范式综述

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

1.命令式

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

  1. 运算语句将结果存入存储器中以便日后使用;

  2. 循环语句使得一些语句可以被反复运行;

  3. 条件分支语句允许仅当某些条件成立时才运行某个指令集合;

  4. 以及存有争议的类似goto这样的无条件分支语句。

使得执行顺序能够转移到其他指令之处。

无论使用汇编、C、Java、JS 都可以写出这样的指令集合,其主要思想是关注计算机执行的步骤,即一步一步告诉计算机先做什么再做什么。所以命令式语言特别适合解决线性的计算场景,它强调自上而下的设计方式。这种方式非常类似我们的工作、生活,因为我们的日常活动都是按部就班的顺序进行的,甚至你可以认为是面向过程的。也比较贴合我们的思维方式,因此我们写出的绝大多数代码都是这样的。

2.声明式

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

有时候对于某个业务逻辑目前没有任何可以归纳提取的通用实现,我们只能写命令式编程代码。当我们写成以后,如果进行思考归纳抽象、进一步优化,就为以后的声明式做下铺垫。

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

3.函数式

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

函数式编程的特点:

  1. 减少了可变量的声明,程序更为安全;

  2. 相比命令式编程,少了非常多的状态变量的声明与维护,天然适合高并发多线程并行计算等任务,我想这也是函数是编程近年又大热的重要原因之一;

  3. 代码更为简洁,但是可读性是高是低也依赖于不同场景、仁者见仁智者见智。

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

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(xy)* z;}

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

柯里化写法:

var calculator=function(x){  return function(y){ return function(z){return(xy)* 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>>() {@Overridepublic Function<Integer, Integer> apply(Integer y) { return new Function<Integer, Integer>() {  @Override  public Integer apply(Integer z) {return (xy) * z;  } };}  }; }};//在这里,我们可以发现,虽然依次输入2、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 实例。

源文地址:https://www.guoxiongfei.cn/cntech/27165.html
0