函数式编程
介绍
而函数式编程和声明式编程是有所关联的,因为他们思想是一致的:即只关注做什么而不是怎么做。但函数式编程不仅仅局限于声明式编程。
函数式编程是面向数学的抽象,将计算描述为一种表达式求值,其实,函数式程序就是一个表达式。
一、函数式编程本质
函数式编程中的函数并部署指计算机中的函数,而是指数学中的函数,即自变量的映射。函数的值取决于函数的参数的值,不依赖于其他状态,比如abs(x)函数计算x的绝对值,只要x不变,无论何时调用、调用次数,最终的值都是一样。
二、函数式编程的特点
一、函数是第一等公民
所谓"第一等公民"(first class),指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。
def func(data:str)->String;
return data
def run(func:Function, data:str)-> void;
return func(data)
二、函数是纯函数(只用"表达式",不用"语句")
@FunctionalInterface
public interface Consumer<E> {
E getData();
}
Consumer<String> consumer3 = ()-> str + " consumer2"; //简化
System.out.println(consumer1.getData());
“表达式”(expression)是一个单纯的运算过程,总是有返回值;“语句”(statement)是执行某种操作,没有返回值。函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。原因是函数式编程的开发动机,一开始就是为了处理运算(computation),不考虑系统的读写(I/O)。"语句"属于对系统的读写操作,所以就被排斥在外。当然,实际应用中,不做I/O是不可能的。因此,编程过程中,函数式编程只要求把I/O限制到最小,不要有不必要的读写行为,保持计算过程的单纯性。
函数编程语言最重要的基础是λ演算(lambda calculus),而且λ演算的函数可以接受函数当作输入(参数)和输出(返回值)。
三、没有"副作用"
function func(data){
let num = 1;
return function(){
return data + num;
}
}
所谓"副作用"(side effect),指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果,比如在上面中func中包装了2层的命名空间。
函数式编程强调没有"副作用",意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。
在其他类型的语言中,变量往往用来保存"状态"(state)。不修改变量,意味着状态不能保存在变量中。函数式编程使用参数保存状态,最好的例子就是递归。
四、引用透明性
函数程序通常还加强引用透明性,即如果提供同样的输入,那么函数总是返回同样的结果。就是说,表达式的值不依赖于可以改变值的全局状态。这使您可以从形式上推断程序行为,因为表达式的意义只取决于其子表达式而不是计算顺序或者其他表达式的副作用。这有助于验证正确性、简化算法,甚至有助于找出优化它的方法。
副作用副作用是修改系统状态的语言结构。因为 FP 语言不包含任何赋值语句,变量值一旦被指派就永远不会改变。而且,调用函数只会计算出结果 ── 不会出现其他效果。因此,FP 语言没有副作用。
优点:
五、接近自然语言,易于理解
函数式编程的自由度很高,可以写出很接近自然语言的代码。
比如java中的stream流处理
Stream.of("fo", "bar", "hello").map(func).forEach(str->{
System.out.println(str);
});
因此,函数式编程的代码更容易理解。
六、 易于"并发编程"
函数式编程不需要考虑"死锁"(deadlock),因为它不修改变量,所以根本不存在"锁"线程的问题。不必担心一个线程的数据,被另一个线程修改,所以可以很放心地把工作分摊到多个线程,部署"并发编程"(concurrency)。
三、函数式编程的实际应用
一、lambda表达式
lambda 表达式的语法格式如下:
使用 "->"将参数和实现逻辑分离;
( ) 中的部分是需要传入Lambda体中的参数;
{ } 中部分,接收来自 ( ) 中的参数,完成一定的功能。
大部分语言如下
(parameters) -> expression
或
(parameters) ->{ statements; }
python如下
lambda 关键字声明这个一个表达式 :前面的x为接受的参数,:后面的是表达式,可以接受任意多个参数
>>> lam = lambda x:x+3
>>> n2 = []
>>> for i in numbers:
... n2.append(lam(i))
...
以下是lambda表达式的重要特征:
- 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
- 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
- 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
- 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。
二、实际应用
@FunctionalInterface
public interface Consumer<E> {
E getData();
}
Consumer<String> consumer1 = new Consumer<String>() { // 传统
@Override
public String getData() {
return str + " consumer1";
}
};
Consumer<String> consumer2 = () -> { // jdk8
return str + " consumer2";
};
Consumer<String> consumer3 = ()-> str + " consumer2"; //简化
System.out.println(consumer1.getData());
System.out.println(consumer2.getData());
System.out.println(consumer3.getData());
在函数式接口的开发中,加上@FunctionalInterface注解检测该接口只有一个方法,在调用中才能使用()-> 向上转型过程中识别该接口
public static void main(String[] args) {
List<String> arr = Arrays.asList(new String[]{"1","2","3"});
Function<String, String> func = getFunc(arr);
System.out.println("***************************");
Stream.of("fo", "bar", "hello").map(func).forEach(str->{
System.out.println(str);
});
}
public static Function<String, String> getFunc(Collection<String> items) {
System.out.println("开始");
Iterator<String> li = items.iterator();
return new Function<String, String>() {
@Override
public String apply(String str) {
return str + li.next();
}
};
// return (item)-> item + li.next(); //上面的简写
}
在上面的例子中,我们把函数封装为一个对象,这个对象返回了一个函数的引用,等到函数被调用时发生向上转型,这时,这个返回的函数才正式被调用
执行顺序是:
当顺序执行,到第三行时 Function<String, String> func = getFunc(arr); 时
getFunc被执行,这时11行和12行已经执行,数据暂存到内存中,返回一个函数的引用,到第五行时
stream生成包含3个元素的顺序数据流,在map中仍然进行了一次函数式接口的调用,返回了一个无状态数据流(返回由应用给定值的结果组成的流函数指向该流的元素。),在返回的StatelsessOp类中
opWrapSink中依旧如此,直到accept开始对数据进行消费,然后开始forEach调用,Function<String,
String> 中的第二个String表示返回的数据类型,因为上面map中返回的数据流还未被消费,直到 str->
System.out.println(str);正式被消费,str代表map中返回的数据类型
@param <E_IN> type of elements in the upstream source
- @param <E_OUT> type of elements in produced by this stage
下面是map的源码
javascript
function add(val){
let num = val;
return function(){
if (num <10){
num++;
return function(){
return num;
}
}
return num;
}
}
三、基本运算
一)、函数合成(compose)
如果一个值要经过多个函数,才能变成另外一个值,就可以把所有中间步骤合并成一个函数,这叫做"函数的合成"(compose)。
指的是将代表各个动作的多个函数合并成一个函数。
上图中,X和Y之间的变形关系是函数f,Y和之间的变形关系是函数g,那么X和Z之间的关系,就是g和f的合成函数g·f。
上面讲到,函数式编程是对过程的抽象,关注的是动作。看下下面的例子
function add(x) {
return x + 10
}
function multiply(x) {
return x * 10
}
console.log(multiply(add(2))) // 120
//将合成的动作抽象为一个函数 compose如下:
const compose = function(f,g) {
return function(x) {
return f(g(x));
};
}
let calculate=compose(multiply,add);
console.log(calculate(2)) // 120
二)、柯里化用途
现在需要实现一个功能,将一个全是数字的数组中的数字转换成百分数的形式。按照正常的逻辑,我们可以按如下代码实现
function getPercentList(array) {
return array.map(function(item) {
return item * 100 + '%'
})
}
console.log(getPercentList([1, 0.2, 3, 0.4]));
// 结果:['100%', '20%', '300%', '40%']
如果通过柯里化的方式来实现
function map(func, array) {
return array.map(func);
}
var mapCurry = createCurry(map);
var getNewArray = mapCurry(function(item) {
return item * 100 + '%'
})
console.log(getPercentList([1, 0.2, 3, 0.4]));
高阶函数
满足下列条件之一的函数就可以称为高阶函数:
- 函数作为参数被传递
- 把函数当作参数传递,这代表我们可以抽离出一部分容易变化的业务逻辑,把这部分业务逻辑放在函数参数中,这样一来可以分离业务代码中变化与不变的部分。其中一个重要应用场景就是常见的回调函数。
下面例子中js的函数都是对高阶函数的利用:
[1, 4, 2, 5, 0].sort((a, b) => a - b);
// [0, 1, 2, 4, 5]
[0, 1, 2, 3, 4].map(v => v + 1);
// [1, 2, 3, 4, 5]
[0, 1, 2, 3, 4].every(v => v < 5);
// true
函数作为返回值输出
让函数继续返回一个可执行的函数,意味着运算过程是可延续的。
const fn = (() => {
let students = [];
return {
addStudent(name) {
if (students.includes(name)) {
return false;
}
students.push(name);
},
showStudent(name) {
if (Object.is(students.length, 0)) {
return false;
}
return students.join(",");
}
}
})();
fn.addStudent("python");
fn.addStudent("java");
fn.showStudent(); //输出:python,java
一个函数2个方法体
同时满足两个条件的高阶函数
const plus = (...args) => {
let n = 0;
for (let i = 0; i < args.length; i++) {
n += args[i];
}
return n;
}
const mult = (...args) => {
let n = 1;
for (let i = 0; i < args.length; i++) {
n *= args[i];
}
return n;
}
const createFn = (fn) => {
let obj = {};
return (...args) => {
let keyName = args.join("");
if (keyName in obj) {
return obj[keyName];
}
obj[keyName] = fn.apply(null, args);
return obj[keyName];
}
}
let fun1 = createFn(plus);
console.log(fun1(2, 2, 2)); //输出:6
let fun2 = createFn(mult);
console.log(fun2(2, 2, 2)); //输出:8
python版
def func():
num1 = 1
def add(num):
num += num1
return num
def del_(num):
num -= num1
return num
return add, del_
print(func()[0](10))
print(func()[1](10))
public static Function<String, String> getFunc(Collection<String> items, Boolean isFunc) {
System.out.println("开始");
Iterator<String> li = items.iterator();
if(isFunc){
return new Function<String, String>() {
@Override
public String apply(String str) {
return str + li.next();
}
};
}
return (item)-> item + li.next();
}
函数式编程的递归和迭代
迭代
function add(val){
let num = val;
return function(){
if (num <10){
num++;
return function(){
return num;
}
}
return num;
}
}
每次调用num加一
递归版:
function add(val){
let num = val;
return function add2 (){
if (num <10){
num++;
const run = add2();
return num;
}
return num;
}
}
https://zhuanlan.zhihu.com/p/363757919
http://ruanyifeng.com/blog/2017/02/fp-tutorial.html
https://www.baidu,com
资料来自上面两个文档,还有很多小白自己写的demo,大佬真的讲得很细,作为学渣不敢说话