Functional Programming 的见解

仅仅是一篇观后感,写于: 2018-03-26,修改于:2019年11月23日

​ 最近在我违反了 no mutation ,就是一个函数里面,修改了传入的变量。导致我在另外一个地方使用该变量的时候,并不知道它已经发生了变化。

​ 然后,我突然想起了好几个月前看得一个演讲:Anjana Vakil: Learning Functional Programming with JavaScript - JSUnconf 2016。演讲稿地址:https://slidr.io/vakila/learning-functional-programming-with-javascript;这里面就有讲什么是 Functional Programming。我发现,在JS中,遵循了下面几条原则,可以更好的复用、维护代码。

在平时写业务的过程中设计模式是谈不上了,但是Functional Programming恰处处可见。

What is Functional Programming?

什么是Functional Programming

  • A programming paradigm.
    一种编程的范式。就像面向过程面向对象。总的来说,Function is King

  • A code style.
    一种代码的风格。如何去组织你的代码。

  • A mindset.
    一种思维模式。该使用什么样的方式去解决你的问题?就像你不想去破解一个代码块完整性(内聚),那么你可以加入一个切面,去影响该代码块的执行结果。

  • A sexy, buzz-wordy trend.

    我不知道啥意思

Why Functional Javascript?

  • Object-oriented in javascript gets tricky.
    因为在JavaScript中,面向对象往往纠缠不清。就比如this,貌似真的很多时候,this的指向会变化多端。

  • Safer, easier to debug/maintain.

    更加安全且容易去调试/维护。

  • Established community.

How Functional Programming in Javascript?

  • Do everything in function:以函数方式思考
    非常简单,就是一个input -> output的过程。你只需要简单的把input交给一个function处理,然后它会给你需要的output。就像一种数据的流向。比如以下的例子:

    以下是非Functional的形式(A):
    var name = "Alan";
    var greeting = "Hi,I'm ";
    console.log(greeting+name);
    => "Hi,I'm Alan"
    
    以下是Functional的形式(B):
    function greet(name){  
        return "Hi,I'm "+name;
      }
      greet("alan");
    
    => "Hi,I'm Alan"
    

    例子A中:这种明显就是并行处理方式,并没有function,也没有体现出输入 -> 处理 -> 输出的数据流形式;而是定义完greet,然后定义name,然后一起打印。

    例子B中:是将name交给一个greet函数处理,它会返回拼接一个greet然后返回给你。这明显是非常函数style。

  • Use pure function:使用纯正的函数

    使用纯正的函数,去避免一些隐藏的问题。

    ​ 在Functional Programming中,我们会遇到一个问题:函数A中,改变了输入的内容,然后你在函数B中使用该input的时候,发现它已经被改变!然后,也许函数B中的执行结果,会因为函数A中改变了input而改变。这个就是文章开头提及的情况。这时候,你可能会绞尽脑汁,究竟在哪里改变了它。所以,纯净的function,是不应该去改变输入的内容。你应该在一个function里面拿了输入内容,然后只读取该输入内容,然后处理好,并且得出结果,然后把output返回

    var name = "alan";
    function greet(){  
        name = "jade";  
        return "Hi,I'm "+name;
    }
    function sayMyName(name){  
        return "Hi,I'm "+name;
    }
    greet();
    sayMyName(name);
    => "Hi,I'm alan "
    

    同样,以下也不是纯净的function

    var name = "alan";
    function greet(){  
      console.log("Hi,I'm "+name);
    }
    => "Hi,I'm alan "
    

    并没有input,而是直接使用了全局的变量。而且,并没有返回计算的结果。我们需要的是:function帮我们计算并返回结果。而打印并不是function需要做的事情。

    正确做法应该如下:function唯一需要做的,就是使用input去计算,然后得出我们需要的output,并将output返回。如下:

    var name = "alan";
    function greet(name){  
        return "Hi,I'm "+name;
    }
    => "Hi,I'm alan "
    

    总之,一个函数,需要尽可能的纯净。

  • Use higher-order functions:使用更高阶的函数

functions can be inputs/outputs:函数也能作为输入、输出。

例子:

/*一个返回函数的函数*/
function makeAdjectifier(adjective) {
    return function (string) {
        return adjective + “ ” + string;   
    };
}

/*使用返回的函数,去修饰一个输入*/
var coolifier = makeAdjectifier(“cool”);
coolifier(“conference”);

返回 => “cool conference”
  • Don’t iterate

    不要迭代,我们有更加好的选择:map、reduce、filter

    通常,我们在处理一些数组/集合会使用迭代。我们都习惯了使用for之类的去循环所有的项,然后进行处理。

    但是呢,在function program中,我们有更加高级的做法:map、reduce、filter,一些可以直接调用的函数。下面的一个通过map、reduce制作三明治的图,就能很好解释map、reduce的工作原理。

    通常,我们制作一个三明治,需要循环去切原料(for一个黄瓜),然后得到三明治的原材料(list)。不过,function style,使用map,我们只需要提供切这个function黄瓜,然后就能返回三明治的原材料。

2070425-8e52422b91d792ea.jpg
mapreduce.jpg

map:就是将一个整体(集合)分割,或者说提取。

reduce:就是将多个元素进行归集。形成一个整体。

filter:将不符合条件的元素过滤掉(比如:你不喜欢黄瓜。就可以过滤名称为黄瓜的原材料,这样,你做出来的三明治就没有黄瓜)

  • Avoid mutability:不去改变原始数据

    有时候,我们改变了原始数据(input)可能会导致一些隐藏的问题。

    比如以下例子:

    var rooms = [“H1”, “H2”, “H3”]; // 我们准备了3间房:H1、H1、H3
    rooms[2] = “H4”; // 发现客人不喜欢H3的房间,于是,直接把原来的H3房间替换成H4
    rooms;
    => ["H1", "H2", "H4"] // 于是H3被改变了
    

    以上,我一开始就认为,这个数组里面的元素就是:H1、H1、H3;但是,我们并不知道,在我代码的其他地方,悄悄地将H3元素直接变成H4。于是,我就开始了漫长的bug tracking的过程:为什么在这里是H3,到了那里又变成了H4?于是我就在电脑前以泪洗面。

    一个很简单的方法,我们可以把数据当成不变的,使用一个function来解决:

    var rooms = [“H1”, “H2”, “H3”];
    Var newRooms = rooms.map(function (rm) { 
     if (rm == “H3”) { return “H4”; }
     else { return rm; }
    });
    newRooms; => ["H1", "H2", "H4"]
    rooms; => ["H1", "H2", "H3"]
    

    以上,我们使用一个函数来处理将H3更换为H4的需要。但是,我们并没有改变rooms变量的原始数据,并且,我们得到了我们需要的数据:newRooms。

  • Persistent data structures efficient immutability:复用相同的数据以提高部分数据变化的效率

    继续沿用上面的例子,如果我们想把H3更换成H4,有以下做法:

    做法1:

    var rooms = [“H1”, “H2”, “H3”]; // 我们准备了3间房:H1、H1、H3
    rooms[2] = “H4”; // 直接把原来的H3房间替换成H4
    => ["H1", "H2", "H4"] // 于是H3被改变了
    

    为了保持Avoid mutability原则,我们可以非常简单的复制一份新的数组去改变H3元素:

    var rooms = [“H1”, “H2”, “H3”]; // 我们准备了3间房:H1、H1、H3
    var newRooms = rooms.slice();
    rooms;
    newRooms;
    => [“H1”, “H2”, “H3”]
    => [“H1”, “H2”, “H4”];
    

    很好,我们做到了Avoid mutability。但是,数据量一旦变得庞大,我们这个方法就不管用了。所以,我们可以换一种思路,如果,我们能够复用相同的部分,只需要替换需要变化的元素,那么就不会浪费这些不必要的空间了。

    首先,我们可以把数组转化成Tree的结构:

2070425-760924a5a4b40c2b.jpg
tree1.jpg

然后,当我们需要替换节点3的时候,只需要连接节点4,建立一个新的tree。这样,只需要做一个小小的改动,我们就可以共享结构了。

2070425-ba94df85abbd4573.jpg
tree2.jpg

我们可以使用一个immutable-js https://immutable-js.github.io/immutable-js/ js库,来达到以上效果,而不需要自己去写算法。下面是immutable-js 的演示:

2070425-09b7d0c991f1e6bb.gif
immutable演示.gif

同样,还有推荐一下function style的库:

● Mori (http://swannodette.github.io/mori/)
● Immutable.js (https://facebook.github.io/immutable-js/)
● Underscore (http://underscorejs.org/)
● Lodash (https://lodash.com/)
● Ramda (http://ramdajs.com/)

更多的FP教程

《An introduction to functional programming》by Mary Rose Cook
https://codewords.recurse.com/issues/one/an-introduction-to-functional-programming

额外的

下面是我写的一些map、reduce的例子

JAVA

准备工作,有以下类:

class Person{
    private String name;
    private int age;
    private BigDecimal money;
    ...
}
  • 循环Object集合

    传统做法:

    List<Person> list = new ArrayList<>();
    for(Person p:list){
        names.add(p.getName());
    }
    

    foreach做法:

    List<Person> list = new ArrayList<>();
    list.stream().forEach(p->{ 
        //do something
    });
    
  • 在一个Object集合中,只抽取Object其中一个属性,形成一个list

    传统做法:

    List<Person> list = new ArrayList<>();
    List<String> names = new ArrayList<>();
    ...
    for(Person p:list){
        names.add(p.getName());
    }
    

    map的做法:

    List<Person> list = new ArrayList<>();
    List<String> names = list.stream().map(Person::getName).collect(Colletcors.toList());
    
  • 在一个Object集合中,我们需要将某个属性作为key,形成一个map

    传统做法:

    List<Person> list = new ArrayList<>();
    Map<String,Person> map = new HashMap<>();
    ...
    for(Person p:list){
        map.put(p.getName(),p);
    }
    

    map做法

    List<Person> list = new ArrayList<>();
    Map<String,Person> map = list.stream()
      .collect(Collectors.toMap(Person::getName, p -> p));
    
  • 在一个Object集合中,我们需要将不符合条件的对象过滤掉

    filter的做法:

    // 我们将name不是alan的过滤掉
    List<Person> list = new ArrayList<>();
    List<Person> newList = list.stream().filter(p->{
      return "alan".equals(p.getName());
    }).collect(Collectors.toList());
    
  • 在一个Object集合中,我们需要统计某个number类型属性的合计。

    传统做法:

    List<Person> list = new ArrayList<>();
    int result = 0;
    for(Person p:list){
        result+=p.getAge();
    }
    

    stream做法:

    List<Person> list = new ArrayList<>();
    int result = list.stream().collect(Collectors.summingInt(Person::getAge));
    

    对于BigDecimal,我们还可以这样:

    List<Person> list = new ArrayList<>();
    // 先map获得集合,再reduce进行归集。
    int result = list.stream()
        .map(Person::getMoney)
        .reduce(new BigDecimal("0"),BigDecimal::add);
    
  • 延伸以上,对于一个非Object的集合,而是一个map结构<String,Interge>的数据,我们可以使用以下进行统计:

    传统做法:

    Map<String,Integer> map = new HashMap<>();
    Integer result = 0;
    for(Map.Entry entry:map.entrySet()){
        result += entry.getValue();
    }
    

    map做法:

    // 方法1:
    Map<String,Integer> map = new HashMap<>();
    Integer result = map.values().stream()
        .mapToInt(Integer::intValue).sum();
    
    // 方法2:
    Integer result = map.values().stream()
        .collect(Collectors.summingInt(Integer::intValue));
    

版权声明:本文为qq22692150原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。