howieyi

No pains, No gains!

View project on GitHub

函数式编程基础概念

范畴论认为,同一个范畴的所有成员,就是不同状态的“变形”(transformation)。通过“态射”,一个成员可以变成另一个成员。

  • 所有成员是一个集合
  • 变性关系是函数

函数式编程不是用函数来编程,也不是传统的面向过程编程。主旨在于将复杂的函数复合成简单的函数(计算理论,或者递归论,或者拉姆达演算)。运算过程尽量写成一些列嵌套的函数调用。(函数是编程适合写库,实现业务还是得通过面向对象和面相过程)

  • 1.函数是一等公民。所谓”第一等公民”(first class),指的是函数 与其他数据类型一样,处于平等地位,可以赋值给其他变量,也 可以作为参数,传入另一个函数,或者作为别的函数的返回值。
  • 2.不可改变量。在函数式编程中,我们通常理解的变量在函数式 编程中也被函数代替了:在函数式编程中变量仅仅代表某个表达 式。这里所说的’变量’是不能被修改的。所有的变量只能被赋一次 初值
  • 3.map & reduce 他们是最常用的函数式编程的方法。
  • 特点总结
    • 函数是”第一等公民”
    • 只用”表达式”,不用”语句”
    • 没有”副作用”
    • 不修改状态
    • 引用透明(函数运行只靠参数)

核心概念

  • 纯函数
    • 对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,也不依赖外部环境的状态
    • 纯函数不仅可以有效降低系统的复杂度,还有很多很棒的特性,比如可缓存性
    var arr = [1,2,3,4,5];

    // Array.slice 是纯函数,因为它没有副作用,对于固定输入,输出总是固定
    arr.slice(0, 1); // 不该改变xs
    arr.splice(0, 1); // 改变xs
// 优点
import _ from 'lodash';
var sin = _.memorize(x => Math.sin(x));

// 第一次计算的时候会稍微慢一点
var a = sin(1);

// 第二次有了缓存,速度极快
var b = sin(1);
// 缺点
var min = 18;

// 不纯的,有外部依赖 min
// checkAge 不仅取决于 age,还有外部依赖变量 min
var checkAge = age => age > min;

// 纯的,这很函数式
// 纯的 checkAge 把关键数字 18 硬编码在函数内部,扩展性比较差,
// 柯里化函数式,代码更优雅
var checkAge = age => age > 18;
  • 纯度和幂等性

    • 幂等性是指执行无数次后还具有相同的效果,同一的参数运行一次函数应该与连续两次结果一致。幂等性在函数式编程中与纯度相关,但有不一致。
    • Math.abs(Math.abs(-42)): 同样的函数运行多少次都与第一次结果一致
  • 函数的柯里化:传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数

var checkAge = min => (age => age > min);
var checkAge18 = checkAge(18);
checkAge18(20);

<!-- 编译后相当于=> -->
var checkAge = function(min){
    return function(age){
        return age > min;
    }
}
function add(x, y){
    return x + y;
}
add(1, 2);

<!-- 柯里化后 -->
function addX(y){
    return function(x){
        return x + y;
    };
}
addX(2)(1);
import {curry} from 'lodash';

var match = curry((reg, str) => str.match(reg));
var filter = curry((f, arr) => arr.filter(f));
var haveSpace = match(/\s+/g);

haveSpace('fsasdasd');
haveSpace('sdasd sdasdafsdfs');

filter(haveSpace, ['abcdsdasd', 'hells sdasdas']);
filter(haveSpace)(['abcdsdasd', 'hells sdasdas']);

事实上柯里化是一种“预加载”函数的方法,通过传递较少的参数, 得到一个已经记住了这些参数的新函数,某种意义上讲,这是一种 对参数的“缓存”,是一种非常高效的编写函数的方法

  • 函数组合: 纯函数以及如何把它柯里化写出的洋葱代码 h(g(f(x))),为了解决函数嵌套的问题,我们需要用到“函数组合”
const compose = (f, g) => (x => f(g(x)));
var first = arr => arr[0];
var reverse = arr => arr.reverse();
var last = compose(first, reverse);

last([1,2,3,4,5]);
  • Point Free: 把一些对象自带的方法转化成纯函数,不要命名转瞬即逝的中间变量。这种风格能够帮我们减少不必要的命名,让代码保持简洁和通用。
// str 作为中间变量让代码变长了,并且毫无意义
const f = str => str.toUpperCase().split(' ');

<!-- 转化后 -->
// 柯里化
var toUpperCase = word => word.toUpperCase();
var split = x => (str => str.split(x));

// 组合
var f = compose(split(' '), toUpperCase);

f('sasd sdasda');
  • 声明式与命令式代码
    • 命令式代码:就是我们通过编写一条又一条指令去让计算机执行一些动作,这其中一般都会涉及到很多繁杂的细节。
    • 声明式:就要优雅很多了,我们通过写表达式的方式来声明我们想干什么,而不是通过一步一步的指示。
// 命令式 -> 偏执行过程
let CEOs = [];
for(var i = 0; i < companies.length; i++){
    CEOs.push(companies[i].CEO);
}

//声明式 -> 定义方法
let CEOs = companies.map(c => c.CEO);
  • 函数式编程总结
    • 函数式编程的一个明显的好处就是这种声明式的代码,对于无副作用的纯函数,我们完全可以不考虑函数内部是如何实现的,专注于编写业务代码。优化代码时,目光只需要集中在这些稳定坚固的函数内部即可。\
    • 相反,不纯的函数式的代码会产生副作用或者依赖外部系统环境,使用它们的时候总是要考虑这些不干净的副作用。在复杂的系统中,这对于程序员的心智来说是极大的负担
    • 函数式编程全是兄弟,面相对象是父子关系

惰性求值、惰性函数、惰性链(懒、下次不想干活)

  • 在指令式语言中以下代码会按顺序执行,由于每个函数都有可能改动或者依赖于其外部的状态,因此必须顺序执行
  • function somewhatLongOperation1(){somewhatLongOperation1}
  • new LazyChain([2,1,3])…
// 惰性函数
// 一次调用,直接修改了函数体
function ajax(){
    if(XMLHttpRequest){
        ajax = function(){
            return new XMLHttpRequest();
        }
    }else{
        ajax = function(){
            return new ActiveXObject('Microsoft.XMLHTTP');
        }
    }
}
// 惰性链,惰性求值
// 前面记录状态值,可用堆栈记录,end集中计算
new LazyChain([2,1,3]).add().xx().end()

高阶函数: 函数当参数,把传入的函数做一个封装,然后返回这个封装函数,达到更高程度的抽象。

  • 它是一等公民
  • 它以一个函数作为参数
  • 把传入的函数做一个封装,以这个封装的函数作为返回结果,达到更高程度的封装
// 命令式
var add = function(a,b){
    return a + b;
};
function math(func, array){
    return func(array[0], array[1]);
}
math(add,[1, 2]); // 3

尾调用优化

  • 函数内部的最后一个动作是函数调用
  • 该调用的返回值,直接返回给函数。
  • 函数调用自身,称为递归。递归需要保存大量的调用记录,很容易发生栈溢出错误
  • 如果尾调用自身,就称为尾递归。如果使用尾递归优化,将递归变为循环,那么只需要保存一个调用记录,这样就不会发生栈溢出错误了
// 不是尾递归,无法优化
function factorial(n){
    if(n === 1) return 1;

    return n * factorial(n - 1);
}

// 尾递归
function factorial(n, total){
    if(n === 1) return 1;

    return factorial(n - 1, n * total);
}
function sum(n){
    if(n === 1) return 1;
    return n + sum(n - 1);
}

<!--
普通递归时,内存需要记录调用的堆栈所出的深度和位置信息。
在最底层计算返回值,再根据记录的信息,跳回上一层级计算,然后再跳回更高一层,依次运行,直到最外层的调用函数。在cpu计算和内存会消耗很多,而且当深度过大时,会出现堆栈溢出
-->
sum(5)
(5 + sum(4))
(5 + (4 + sum(3)))
(5 + (4 + (3 + sum(2))))
(5 + (4 + (3 + (2 + sum(1))))) (5 + (4 + (3 + (2 + 1))))
(5 + (4 + (3 + 3)))
(5 + (4 + 6))
(5 + 10)
15
<!--
整个计算过程是线性的,调用一次sum(x, total)后,会进入下一个栈,相关的数据信息和 跟随进入,不再放在堆栈上保存。当计算完最后的值之后,直接返回到最上层的 sum(5,0)。这能有效的防止堆栈溢出。
ES6 强制使用尾递归
在ECMAScript 6,我们将迎来尾递归优化,通过尾递归优化,javascript代码在解释成机器 码的时候,将会向while看起,也就是说,同时拥有数学表达能力和while的效能。
-->
function sum(x, total){
    if(x === 1) return 1;
    return sun(n - 1, x + total);
}


sum(5, 0)
sum(4, 5)
sum(3, 9)
sum(2, 12)
sum(1, 14)
15

闭包(拿到不该拿到的值)

<!-- 如下例子,虽然外层的 makePowerFn 函数执行完毕,栈上的调用 帧被释放,但是堆上的作用域并不被释放,因此 power 依旧可以 被 powerFn 函数访问,这样就形成了闭包 -->

function makePowerFn(power){
    function powerFn(base) {
        return Math.pow(base, power);
    }
    return powerFn;
}
var square = makePowerFn(2);
square(3); // 9