仅仅是一篇观后感,写于: 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 KingA 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和黄瓜,然后就能返回三明治的原材料。
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的结构:
然后,当我们需要替换节点3的时候,只需要连接节点4,建立一个新的tree。这样,只需要做一个小小的改动,我们就可以共享结构了。
我们可以使用一个immutable-js https://immutable-js.github.io/immutable-js/ js库,来达到以上效果,而不需要自己去写算法。下面是immutable-js 的演示:
同样,还有推荐一下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));