Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

函数式编程瞎扯编 #7

Open
lhyt opened this issue Jan 28, 2018 · 0 comments
Open

函数式编程瞎扯编 #7

lhyt opened this issue Jan 28, 2018 · 0 comments
Assignees

Comments

@lhyt
Copy link
Owner

lhyt commented Jan 28, 2018

0.前言

本文并不是完全按照严格意义的函数式编程来讲,主要是抽取一些思想和js结合,以达到写出更有水平的代码的目的。(比较扯,接下来学完redux准备重写)

1.概念

1.1介绍

在计算机科学里,函数式编程是一种编程范式,它将计算描述为表达式求值并避免了状态和数据改变。
函数式编程(FP)思想像数学计算,比如数学中有一个函数为f(x)=x+1,那么f(0)=1,输出结果只和输入有关。函数可以看作是一个管道,一边进去另外一边输出结果(自变量进去,因变量出来)。数学函数的特点是对于每一个自变量,存在唯一的因变量与之对应,形成一一对应的映射关系(数学函数的单值性)。只要编程函数是纯函数,编程函数就可像数学的函数一样进行函数式编程。

function add(a,b){
return a+b
}

好处?

  • 语义清晰
  • 复用性高
  • 维护性好
  • 副作用少

副作用?(除了返回值之外,它可以与外部条件进行交互。如new Date())
数学中的函数,如果是一个值需要经过多个函数才能得到结果:f('test')='a',g('a')='result',那么可以写作一个复合函数的形式:componse('test') = g(f('test'))='result'
同样的,写作js程序就是:

function f(x){
if(x=='test')  return 'a'
}
function g(x){
if(x=='a')return 'result'
}
function componse(x){
return g(f(x))
}
componse('test')

componse是复合函数,输入如果是'test',输出结果就是'result'

1.2纯函数的条件

  • 每个函数必须有输入参数(类似数学函数自变量)
  • 每个函数必须有返回值(类似因变量)
  • 同一个参数调用函数时,返回值一致。
  • 返回值仅由其输入值决定,并且不产生副作用

这样子,就可以“看起来好像数学的函数”,进行函数式编程了

const greet = (name) => \`Hello, ${name}\`//纯函数
greet('world')

const greet = () => 'Hello, world'//不是纯函数
greet()
greet('hi')

2.高阶函数

以函数为参数,返回另一个函数。
比如上面所说的componse函数,改为函数式编程的话,就是

var componse = (x)=>(x=='test'?'a':undefined)=='a'?'result':undefined

以函数f的输出作为g的输入

再举一个在一个大的函数里面,包含函数处理参数的例子:
过滤一个数组,使得数组只有字符串:

[0, '1', null].filter(function(x){
		return Object(x) instanceof String
})

把参数、过滤函数都作为一个大的函数的参数,写作函数式编程,就可以这样:

var filter = (predicate, arr) => arr.filter(predicate)
var isStr = (x) => Object(x) instanceof String
filter(isStr, [0, '1', null]) //'1'

不仅如此,部分还可以带上的初始值。这是一个这样的函数,f是函数,arg是f接受的原始值参数集合,otherArgs是后面传进来的参数集合,最后返回的结果是原始值和后面传进来的值作为f的参数,返回结果:

var fp = function(f,...args){
 	return function(...otherArgs){
 		return f(...args,...otherArgs)
 	}
 }
fp((x,y,z)=>x+y+z,2)(1,120)//123,2 是原始值
fp((x,y,z)=>x+y+z,1,2)(1)//4   ,1和2是原始值
fp((x,y,z)=>x+y+z,2,3)(1,120)//6,因为只接受3个参数,最后的120没有参与计算

2.1数组的方法

通过高阶函数,数组中的每一个元素可以被应用到一个函数上,并返回新的数组。
比如,一个数组的排序函数,两个参数:数组和排序函数,我们就可以写作:

var arrsort = function(f,arr){
	return arr.sort(f)
}
//es6:
var arrsort = (f,arr)=>arr.sort(f)
arrsort((a,b)=>a-b,[1,5,4,3])//[1,3,4,5]

尝试用reduce实现一个Array.map:

function myMap(f, arr) {
  return arr.reduce(function(result, x) {
    result.push(f(x));
    return result;
  }, []);
}
myMap((x)=>x+1,[1,2,3,4])//[2,3,4,5]

2.3闭包

闭包的现象:函数中的函数、外作用域可以访问内作用域的变量,闭包的结果:内存泄露(泄漏一点点内存没所谓)。中间缓存的变量,其实也是一种闭包的结果。代码块执行完成后,函数 add1 的状态也被保存

var add = x => y => x + y
var add1 = add(1) //缓存x=1,return的是1+y
add1(3) // 4

回头看前面的例子,

var filter = (predicate, arr) => arr.filter(predicate)
var isStr = (x) => Object(x) instanceof String
filter(isStr, [0, '1', null]) //'1'

前面的缓存var isStr = (x) => Object(x) instanceof String这个函数,也是通过闭包,使得在这个作用域能够捕获访问函数的局部变量,即使执行已经从定义它的块中移出。它们允许在声明变量的块执行完成之后保持对作用域的引用,不会被垃圾回收

2.4链式调用

我们计算1+2+3+4时候,如果用var add = (a,b)=>a+b这个函数,则是add(add(add(1, 2), 3), 4),这样子显然可读性和维护性很差,所以我们可以优化一下:

var _ = {
	sum:0,
  add(x) {  
    this.sum += x;  
    return this;  
 } 
};  
_.add(1).add(2).add(3).add(4).sum 

是不是发现有点像jQuery和promise呢?
jQuery实例对象,执行完他的方法后返回的还是jQuery实例对象,所以还可以继续执行jQuery实例的方法。promise解决了回调地狱的问题,promise的then也是一样的道理,返回的是promise对象,所以可以继续使用他本身的方法(比如then)
于是,我们马上来自己造一个promise看看:

function MyPromise(fn){
                var count = 0;
                var that = this
                this.list = [];//存放后面的then
                this.then = function(callback){
                    this.list.push(callback);
                    return this;
                }
                this.oresolve = function(){//resolve一个就算一个,直到then运行完
                    if(count == that.list.length) {
                    	return;
                    }
                    that.list\[count++\](that.oresolve);
                }
                setTimeout(()=>{//模仿异步编程
                    fn(that.oresolve);
                },0);
            }
//试一下:
function a(oresolve){
                setTimeout(function(){
                    console.log("1");
                    oresolve();
                }, 1000);
            }           
            function b(oresolve){
                setTimeout(function(){
                    console.log("2");
                    oresolve();
                }, 1000);
            }
new MyPromise(a).then(b)//1、2

虽然这不是严格意义的函数式编程,但是这种链式调用是一种类似的思想,像一条管道一样,一边进一边出,而且代码易于维护

3.柯里化

将多元函数转换为 一元函数的过程。因为闭包的存在,可以使得柯里化更加方便。
比如sum = (a, b) => a + b柯里化后sum = (a) => (b) => a + b,柯里化后sum(1)(2)==3,如果设置一个中间变量sum1=sum(1)(保持对sum(1)的结果的引用),那么sum1(3)==4,有一种电子计算机的感觉吧,按下1+1,然后输出个2,再按+1输出3

3.1手动给函数加上柯里化的能力

直接在函数的原型对象上加上一个curry的方法

Function.prototype.curry = function() {
       var defaultArgs = [...arguments];
        var that = this;
        return function() {
            return that.apply(this,defaultArgs.concat([...arguments]));    
            }
        }

于是我们就可以用一下试试看

 function add(a, b) {//定义一个加法函数测试一下
            return a + b;
        }
        var add1 = add.curry(1);//实际上是1+b
        console.log(add1(5)); // 6
        console.log(add1(2)); // 3
        var add2= add.curry(1,2)
        add2() //3
        add2(1)//3,参数是两个,第三个参数不参与

3.2自动柯里化

lodash 里面有一个curry函数 _.curry(f),如果给定的参数数量少于需要的参数,则返回一个函数,该函数将获得其余的参数。当函数得到足够参数时,就会有返回值。
我们已经自己写出了一个function原型上的curry方法,那也可以写出类似效果的自动柯里化,比如上面的就可以有这样的效果:add.curry(1,2)()//3,add.curry(1)(2)//3
自动柯里化后的结果:

var curryAdd = function (){
	return arguments.length<add.length?add.curry(...arguments):add(...arguments)
}
curryAdd(1)//一个函数add.curry(1)
curryAdd(1)(2)//3
curryAdd(1,2)//3
curryAdd(1,2,3)//3

4.从代理模式到函数记忆

4.1 代理模式

是一个对象找一个替代对象,以便对原对象进行访问,在我们不愿意或者不想对原对象进行直接操作时候,我们可以使用代理,让它帮原对象进行一系列的操作。

var Dinner = function(name){//定义晚餐类
   this.name = name;
};
Dinner.prototype.getName = function() {//晚餐类被取名字的方法
   return this.name;
};
// 定义一个代理
var proxy = {
   cookDinner: function(dinner) {
       boss.cookDinner(dinner.getName())
   }
};
// 定义一个boss对象
var boss = {
   cookDinner: function(name) {
       console.log('已经做好了晚餐:' + name);
   }
};
proxy.cookDinner(new Dinner('黄焖鸡米饭')); //已经做好了晚餐:黄焖鸡米饭

这里boss会做饭,代理也会做饭,但是boss并不希望自己做饭,所以交给代理做饭。

4.2 缓存代理

缓存代理:函数第一次遇到某个参数,并算出了返回值,之后如果遇到相同参数就直接返回缓存结果,无需执行计算过程。

var add = (a,b)=>a+b
var proxyAdd = (function() {
   var cache = {};//缓存
   return function() {
       var args = Array.prototype.join.call(arguments, ',');
       if(args in cache) {
           return cache[args];//直接返回缓存结果
       }
       return caches[args] = add.apply(this, arguments);//将第一次遇到的k-v组合缓存
   }
})();
console.time('第一次')
proxyAdd(1, 2); 
console.timeEnd('第一次')
console.time('第2次')
proxyAdd(1, 2); 
console.timeEnd('第2次')

测试了几次:(复制函数定义部分+console部分粘贴运行)

  • 第一次: 1.021ms
    第2次: 0.074ms
  • 第一次: 0.376ms
    第2次: 0.060ms
  • 第一次: 0.180ms
    第2次: 0.041ms
    显然第二次是比较快的
    ※如果是已经定义了函数,多次运行console部分,时间就是不稳定的(就像平时js在浏览器中的运行时间也是不稳定的)

4.3函数记忆

类似于缓存代理,我们先定义一个记忆函数

var memorize = function(f,hasher){
		//用闭包缓存
		var mem = function(name){
			var cache = mem.cache
			var key = "" + (hasher?hasher.apply(this.arguments):name)
			if (!cache[key]) {
				cache[key] = f.apply(this,arguments)
			}
			return cache[key]
		}
		mem.cache = {}
		return mem
	}
var add = (a,b)=>a+b;
var memorizedAdd = memorize(add,function(){
		var args = Array.prototype.slice.call(arguments)
		return JSON.stringify(args)
	})
memorizedAdd(1,2)//3
memorizedAdd(1,2)//第二次不用计算就可以知道结果

有的人就说了,console.time居然慢了,其实对于简单计算,并不需要函数记忆,用了反而更加慢。
可是对于递归的经典问题斐波那契数列,他的作用就来了

//常规的斐波那契数列递归
var count = 0
	var fob = function(n){
		count ++
		return n<2?n:fob(n-1) + fob(n-2)
	}
	fob(10)
	console.log('执行了'+count+'次')//177

但是,如果用到了函数记忆,运行次数将会大大减小

var count = 0
	var fob = function(n){
		count ++
		return n<2?n:fob(n-1) + fob(n-2)
	}
	var fob = memorize(fob)//用上函数记忆
	fob(10)
	console.log('执行了'+count+'次')//11

常规的递归,次数多是因为,第一个数+第二个数=第三个数,第四个数=第三个数+第二个数=第一个数+第二个数+第二个数......一直下去,而我们已知的就是前面两个数,如果没有缓存,他们将会使劲往下找,直到找到前面两个数。如果用上函数记忆,他们每一次运行后的结果都会被记住,而不是往下递归寻找前面两个数了。

5.从函数记忆到递归

5.1递归

一般,我们要想在控制台打印1-10,都会用循环,循环i从0到10:

for(var i = 0;i<10;i++){
console.log(i)
}

在函数式编程中,我们经常和递归离不开:

(function f(i){
	if (i>10) {
		return 
	}
	console.log(i)
	f(i+1)
})(0)

我们知道,函数调用会在内存形成一个调用帧,保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用记录上方,还会形成一个B的调用记录。等到B运行结束,将结果返回到A,B的调用记录才会消失。如果函数B内部还调用函数C,那就还有一个C的调用记录栈,以此类推。所有的调用记录,就形成一个调用栈。所以,普通的递归容易导致栈的溢出,而且效率不如普通的循环,所以需要尾递归进行优化。

5.2尾递归

尾调用,就是某个函数的最后一步是调用另一个函数。那么显然,尾递归就是调用的那个函数就是自己。
由于是函数的最后一步,所以不需要保留外层函数的调用记录,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用记录,取代外层函数的调用记录就可以了。
如果所有函数都是尾调用,那么完全可以做到每次执行时,调用记录只有一项,这将大大节省内存。这就是尾递归的意义。
对比:
正常递归(非尾递归,尾部调用非自身)

function f(n){ 
return g(f(n-1)) 
} 

用数学公式的角度就是f(x)=g(f(x-1))
假设函数内部的关系用g表示(加减乘除幂对数赋值等等),对于函数f的n次递归,正常的递归就是f(n)=g(f(n-1))=g(g(f(n-2)))=…=g(g(g(…..g(f(1))))),里面要嵌套n个g,复杂度O(n)
尾递归(尾部调用自身,没毛病,return就是f自己)

function f(n){ 
return f(n-1 
} 
f(n)=f(n-1==f(1) 

复杂度就是O(1)

那么对于阶乘递归问题,一般的递归:

var s = 1
var mult = (x)=>{
	s*=x--
	if(x==1){
		return s
	}
	return mult(x)
}

进行优化后:var mult = (n,s)=>n===1?s:mult(n-1,n*s)

对于常规的递归的斐波那契var fob=(n)=>n<=1?n: fob(n-1)+fob(n-2),如果经过了尾递归,则可以写作:
var fob = (n)=>(f=(a,b,s)=>s<n?f(b,a+b,s+1):a)(1,1,1)
将中间的计算结果(sum和第二项)缓存到参数中。实际上用起来的话,尾递归优化过的函数,可以计算的范围就比常规递归大的多了.

把计算结果放在第二个参数以后缓存,是递归优化成尾递归的一种常见方法。当然,正常的情况能用for就用for,递归的话看需求,也不一定要用上的,但是知道有这种思想是特别重要的。

6.真-函数式编程

严格意义上的函数式编程,和上面讲的还是有区别的。我这里为了方便理解,就没有太过于遵守这个规则,实际上为了追求这种思想而不是完全照搬,来增加代码的简洁性。

  • 用const定义,即是无变量。
  • 条件表达式condition ? expr1 : expr2代替if-else
  • 禁用循环,用递归代替
  • 不能使用prototype和this,完全脱离JS中的面向对象编程。
  • 不要使用with语句。
  • 用===,避免==的自动类型转换
    ....
    还有好多好多,涉及到范畴这些的,我就不过多了解了
@lhyt lhyt changed the title 函数式编程 函数式编程瞎扯编 Apr 11, 2018
@lhyt lhyt self-assigned this May 1, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant