图片 1

函数式编程

这是完结篇了,前两篇文章在这里:

常用的编程思想有一下几类:
1、面向过程编程,最初级的,想到哪写到哪;
2、面向对象编程,以事物为中心的编程思想,把共有的属性和方法封装到一个类里;
3、面向切面编程,统计一个函数执行的时间;
4、函数式编程,提纯无关于业务的纯函数,函数嵌套让函数更强大。(react中大量使用)

现在大公司的编程方式有:

JavaScript函数式编程(一)

一、函数式编程思维及核心概念

1.oop(面向对象编程);

2.aop(面向切面编程);

3.函数式编程(JavaScript Functional Programming);

JavaScript函数式编程(二)

1、函数作为“一等公民”,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。

范畴论Category Theory

在第二篇文章里,我们介绍了 MaybeEitherIO 等几种常见的
Functor,或许很多看完第二篇文章的人都会有疑惑:

2、map和reduce是最常见的函数式编程方法
var arr = [11, 22, 33];
arr.map(function(value, index, array){
    console.log(value, index, array);
})
//11  0  [11, 22, 33]
//22  1  [11, 22, 33]
//33  2  [11, 22, 33]

这里面.map本身就是一个函数

  1. 函数式编程是范畴论的数学分支是一门很复杂的数学,认为世界上所有概念体系都可以抽象出一个个范畴
  2. 彼此之间存在某种关系概念、事物、对象等等,都构成范畴。任何事物只要找出他们之间的关系,就能定义
  3. 箭头表示范畴成员之间的关系,正式的名称叫做“态射”(morphism)。范畴论认为,同一个范畴的所有成员,就是不同状态的“变形”(transformation)。通过“态射”,一个成员可以变形成另一个成员

『这些东西有什么卵用?』

3、对于函数式编程,里面的纯函数,对于同样的输入,一定会有同样的输出,永远不依赖于外部状态。
var xs = [1,2,3,4,5];
var result1 = xs.slice(0, 3);
console.log('xs:', xs);
console.log('result1:', result1);
// xs:  [1, 2, 3, 4, 5]
//result1:  [1, 2, 3]

var result2 = xs.splice(0, 3);
console.log('xs', xs);
console.log('result2', result2);
// xs:  [4, 5]
//result2:  [1, 2, 3]

可以发现Array.slice是一个纯函数,没有副作用,对于固定的输入总有固定的输出。

纯函数可以记忆(同样的输入总有同样的输出),不和外界有任何关系,抽象代码方便单元测试

函数式编程5大特点

事实上,如果只是为了学习编写函数式、副作用小的代码的话,看完第一篇文章就足够了。第二篇文章和这里的第三篇着重于的是一些函数式理论的实践,是的,这些很难(但并非不可能)应用到实际的生产中,因为很多轮子都已经造好了并且很好用了。比如现在在前端大规模使用的
Promise 这种异步调用规范,其实就是一种
Monad(等下会讲到);现在日趋成熟的 Redux 作为一种 Flux
的变种实现,核心理念也是状态机和函数式编程。

4、函数的柯里化函数,函数接受一堆参数,返回一个新函数,继续接收参数能够处理逻辑。

柯里化之前:

var addFoo = (x, y) => x+y;
addFoo(1, 2); //3

柯里化之后:

var addFoo = x => ( y => x+y);
addFoo(1)(2);   //3

此处只是对柯里化概念的解释,其实一般简单的处理是没必要柯里化的。

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

  1. 函数是第一等公民
  2. 只用表达式,不用语句
  3. 没有副作用
  4. 不修改状态
  5. 引用透明(函数运行只靠参数)

一、Monad

关于 Monad
的介绍和教程在网络上已经层出不穷了,很多文章都写得比我下面的更好,所以我在这里只是用一种更简单易懂的方式介绍
Monad,当然简单易懂带来的坏处就是不严谨,所以见谅/w

如果你对 Promise 这种规范有了解的话,应该记得 Promise
里一个很惊艳的特性:

doSomething()
    .then(result => {
        // 你可以return一个Promise链!
        return fetch('url').then(result => parseBody(result));
    })
    .then(result => {
        // 这里的result是上面那个Promise的终值
    })

doSomething()
    .then(result => {
        // 也可以直接return一个具体的值!
        return 123;
    })
    .then(result => {
        // result === 123
    })

对于 Promise
的一个回调函数来说,它既可以直接返回一个值,也可以返回一个新的
Promise,但对于他们后续的回调函数来说,这二者都是等价的,这就很巧妙地解决了
nodejs 里被诟病已久的嵌套地狱。

事实上,Promise 就是一种 Monad,是的,可能你天天要写一大堆
Promise,可直到现在才知道天天用的这个东西竟然是个听起来很高大上的函数式概念。

下面我们来实际实现一个 Monad,如果你不想看的话,只要记住 『Promise
就是一种 Monad』
这句话然后直接跳过这一章就好了。

我们来写一个函数 cat,这个函数的作用和 Linux 命令行下的 cat
一样,读取一个文件,然后打出这个文件的内容,这里 IO
的实现请参考上一篇文章:

import fs from 'fs';
import _ from 'lodash';

var map = _.curry((f, x) => x.map(f));
var compose = _.flowRight;

var readFile = function(filename) {
    return new IO(_ => fs.readFileSync(filename, 'utf-8'));
};

var print = function(x) {
    return new IO(_ => {
        console.log(x);
        return x;
    });
}

var cat = compose(map(print), readFile);

cat("file")
//=> IO(IO("file的内容"))

由于这里涉及到两个 IO:读取文件和打印,所以最后结果就是我们得到了两层 IO,想要运行它,只能调用:

cat("file").__value().__value();
//=> 读取文件并打印到控制台

很尴尬对吧,如果我们涉及到 100 个 IO 操作,那么难道要连续写 100
个 __value() 吗?

当然不能这样不优雅,我们来实现一个 join 方法,它的作用就是剥开一层
Functor,把里面的东西暴露给我们:

var join = x => x.join();
IO.prototype.join = function() {
  return this.__value ? IO.of(null) : this.__value();
}

// 试试看
var foo = IO.of(IO.of('123'));

foo.join();
//=> IO('123')

有了 join 方法之后,就稍微优雅那么一点儿了:

var cat = compose(join, map(print), readFile);
cat("file").__value();
//=> 读取文件并打印到控制台

join 方法可以把 Functor 拍平(flatten),我们一般把具有这种能力的
Functor 称之为 Monad。

这里只是非常简单地移除了一层 Functor 的包装,但作为优雅的程序员,我们不可能总是在
map 之后手动调用 join 来剥离多余的包装,否则代码会长得像这样:

var doSomething = compose(join, map(f), join, map(g), join, map(h));

所以我们需要一个叫 chain 的方法来实现我们期望的链式调用,它会在调用 map 之后自动调用 join 来去除多余的包装,这也是
Monad 的一大特性:

var chain = _.curry((f, functor) => functor.chain(f));
IO.prototype.chain = function(f) {
  return this.map(f).join();
}

// 现在可以这样调用了
var doSomething = compose(chain(f), chain(g), chain(h));

// 当然,也可以这样
someMonad.chain(f).chain(g).chain(h)

// 写成这样是不是很熟悉呢?
readFile('file')
    .chain(x => new IO(_ => {
        console.log(x);
        return x;
    }))
    .chain(x => new IO(_ => {
        // 对x做一些事情,然后返回
    }))

哈哈,你可能看出来了,chain 不就类似 Promise 中的 then
吗?是的,它们行为上确实是一致的(then
会稍微多一些逻辑,它会记录嵌套的层数以及区别 Promise
和普通返回值),Promise 也确实是一种函数式的思想。

(我本来想在下面用 Promise
为例写一些例子,但估计能看到这里的人应该都能熟练地写各种 Promise
链了,所以就不写了0w0)

总之就是,Monad
让我们避开了嵌套地狱,可以轻松地进行深度嵌套的函数式编程,比如IO和其它异步任务。

5、函数组合

纯函数以及如何把它柯里化写出洋葱代码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(firtst, reverse);
last([1, 2, 3, 4, 5]);  //5

其实这里compose函数就是first函数和reverse函数组合在一起拼接成的一个组合函数,先求数组的逆序函数,再得出数组的第一个值。从最里层一层一层往外层剥开。
函数组合相当于把一页一页的洋葱贴在一起。

compose(f, compose(g,h))
compose(compose(f, g), h)
compose(f, g, h)

专业术语

二、函数式编程的应用

好了,关于函数式编程的一些基础理论的介绍就到此为止了,如果想了解更多的话其实建议去学习
Haskell 或者 Lisp
这样比较正统的函数式语言。下面我们来回答一个问题:函数式编程在实际应用中到底有啥用咧?

1、React

React
现在已经随处可见了,要问它为什么流行,可能有人会说它『性能好』、『酷炫』、『第三方组件丰富』、『新颖』等等,但这些都不是最关键的,最关键是
React 给前端开发带来了全新的理念:函数式和状态机。

我们来看看 React 怎么写一个『纯组件』吧:

var Text = props => (
    <div style={props.style}>{props.text}</div>
)

咦这不就是纯函数吗?对于任意的 text
输入,都会产生唯一的固定输出,只不过这个输出是一个 virtual DOM
的元素罢了。配合状态机,就大大简化了前端开发的复杂度:

state => virtual DOM => 真实 DOM

在 Redux 中更是可以把核心逻辑抽象成一个纯函数 reducer:

reducer(currentState, action) => newState

关于
React+Redux(或者其它FLUX架构)就不在这里介绍太多了,有兴趣的可以参考相关的教程。

2、Rxjs

Rxjs 从诞生以来一直都不温不火,但它函数响应式编程(Functional Reactive
Programming,FRP)的理念非常先进,虽然或许对于大部分应用环境来说,外部输入事件并不是太频繁,并不需要引入一个如此庞大的
FRP 体系,但我们也可以了解一下它有哪些优秀的特性。

在 Rxjs 中,所有的外部输入(用户输入、网络请求等等)都被视作一种
『事件流』:

--- 用户点击了按钮 --> 网络请求成功 --> 用户键盘输入 --> 某个定时事件发生 --> ......

举个最简单的例子,下面这段代码会监听点击事件,每 2
次点击事件产生一次事件响应:

var clicks = Rx.Observable
    .fromEvent(document, 'click')
    .bufferCount(2)
    .subscribe(x => console.log(x)); // 打印出前2次点击事件

其中 bufferCount 对于事件流的作用是这样的:

图片 1

是不是很神奇呢?Rxjs
非常适合游戏、编辑器这种外部输入极多的应用,比如有的游戏可能有『搓大招』这个功能,即监听用户一系列连续的键盘、鼠标输入,比如上上下下左右左右BABA,不用事件流的思想的话,实现会非常困难且不优雅,但用
Rxjs 的话,就只是维护一个定长队列的问题而已:

var inputs = [];
var clicks = Rx.Observable
    .fromEvent(document, 'keydown')
    .scan((acc, cur) => {
        acc.push(cur.keyCode);
        var start = acc.length - 12 < 0 ? 0 : acc.length - 12;
        return acc.slice(start);
    }, inputs)
    .filter(x => x.join(',') == [38, 38, 40, 40, 37, 39, 37, 39, 66, 65, 66, 65].join(','))// 上上下下左右左右BABA,这里用了比较奇技淫巧的数组对比方法
    .subscribe(x => console.log('!!!!!!ACE!!!!!!'));

当然,Rxjs
的作用远不止于此,但可以从这个范例里看出函数响应式编程的一些优良的特性。

3、Cycle.js

Cycle.js 是一个基于 Rxjs 的框架,它是一个彻彻底底的 FRP 理念的框架,和
React 一样支持 virtual DOM、JSX
语法,但现在似乎还没有看到大型的应用经验。

本质的讲,它就是在 Rxjs 的基础上加入了对 virtual
DOM、容器和组件的支持,比如下面就是一个简单的『开关』按钮:

import xs from 'xstream';
import {run} from '@cycle/xstream-run';
import {makeDOMDriver} from '@cycle/dom';
import {html} from 'snabbdom-jsx';

function main(sources) {
  const sinks = {
    DOM: sources.DOM.select('input').events('click')
      .map(ev => ev.target.checked)
      .startWith(false)
      .map(toggled =>
        <div>
          <input type="checkbox" /> Toggle me
          <p>{toggled ? 'ON' : 'off'}</p>
        </div>
      )
  };
  return sinks;
}

const drivers = {
  DOM: makeDOMDriver('#app')
};

run(main, drivers);

当然,Cycle.js
这种『侵入式』的框架适用性不是太广,因为使用它就意味着应用中必须全部或者大部分都要围绕它的理念设计,这对于大规模应用来说反而是负担。

6、Point Free

把一些对象自带的方法转化成纯函数,不要命名转瞬即逝的中间变量。
const f = str => str.toUpperCase().split(' ');
这个函数中,使用了str作为中间变量,但这个中间变量除了让代码变得长一点以外,毫无意义。

var toUpperCase = word.toUpperCase();
var split = x =>(str => str.split(x));

var compose = (f, g) => (x => f(g(x)));
var f = compose(split(' '), toUpperCase);

f("abc  def");   //ABCDEF

这种风格能够帮助我们减少不必要的命名,让代码保持简洁和通用。

  1. 纯函数
  2. 函数的柯里化
  3. 函数组合
  4. Point Free
  5. 声明式命令式代码
  6. 惰性求值

三、总结

既然是完结篇,那我们来总结一下这三篇文章究竟讲了些啥?

第一篇文章里,介绍了纯函数、柯里化、Point
Free、声明式代码和命令式代码的区别,你可能忘记得差不多了,但只要记住『函数对于外部状态的依赖是造成系统复杂性大大提高的主要原因』以及『让函数尽可能地纯净』就行了。

第二篇文章,或许是最没有也或许是最有干货的一篇,里面介绍了『容器』的概念和
MaybeEitherIO 这三个强大的
Functor。是的,大多数人或许都没有机会在生产环境中自己去实现这样的玩具级
Functor,但通过了解它们的特性会让你产生对于函数式编程的意识。

软件工程上讲『没有银弹』,函数式编程同样也不是万能的,它与烂大街的 OOP
一样,只是一种编程范式而已。很多实际应用中是很难用函数式去表达的,选择
OOP
亦或是其它编程范式或许会更简单。但我们要注意到函数式编程的核心理念,如果说
OOP
降低复杂度是靠良好的封装、继承、多态以及接口定义的话,那么函数式编程就是通过纯函数以及它们的组合、柯里化、Functor
等技术来降低系统复杂度,而 React、Rxjs、Cycle.js
正是这种理念的代言人,这可能是大势所趋,也或许是昙花一现,但不妨碍我们去多掌握一种编程范式嘛0w0

7、惰性函数

惰性函数,只在第一次执行,第一次执行后再调用得到的结果都是一样的。对于浏览器来说,节省了时间和资源。
一个简单的例子如下:

function test(a) {
    if (a == 1) {
        test = function () {
            return "ok"
        }
        return "ok";
    }else{
        test = function (argument) {
            return "no";
        }
        return "no";
    }
}
test(1)  // "ok"
test(10)   //"ok"

当第一次执行test(1)后,函数的返回值固定返回“ok”了,所以不管传的参数是多少,都是返回“ok”。
最典型的一个应用就是ajax中:

function createXHR(){
     var xhr=null;
     if(typeof XMLHttpRequest!='undefined'){
          xhr=new XMLHttpRequest();
         createXHR=function(){
               return XMLHttpRequest();  //直接返回一个懒函数,这样不必在往下走
          }
      }else{
          try{
               xhr=new ActiveXObject("Msxml2.XMLHTTP");
              createXHR=function(){
                    return new ActiveXObject("Msxml2.XMLHTTP");
               }
          }catche(e){
               try{
                    xhr =new ActiveXObject("Microsoft.XMLHTTP");
                    createXHR=function(){
                         return new ActiveXObject("Microsoft.XMLHTTP");
                    }
               }catch(e){
                    createXHR=function(){
                         return null
                    }
               }        
         }
     }
}

纯函数

对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,也不依赖外部环境的状态。

var xs=[1,2,3,4,5];
//Array.slice是纯函数,因为它没有副作用,对于固定的输入,输出总是固定的
xs.slice(0,3); //[1,2,3]
xs.slice(0,3);  //[1,2,3]
xs.splice(0,3);   //[1,2,3]
xs.splice(0,3);  //[4,5]

import _ from 'lodash';
var sin=_.memorize(x=>Math.sin(x));
var a=sin(1); //第一次计算的时候会稍慢一点
var b=sin(1); //第二次有了缓存,速度极快
//纯函数不仅可以有效降低系统的复杂度,还有很多很棒的特性,比如可缓存性
//惰性函数

不纯

var min=18;
var checkage=function(age){
    return age>min; //依赖于外部的min,导致不纯
}
8、高阶函数

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

var add = function(a, b){
      return a+b;
}
function math(func, arr){
      return func(arr[0], arr[1]);
}

math(add, [12, 21, 31]);   //33

函数的柯里化

传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

用柯里化来改造上面的不纯函数

var checkage=min=>(age=>age>min);
var checkage18=checkage(18);
checkage18(20);
9、尾递归
function sum(n){
      if (n === 1){
            return 1;
      }
      return n + sum(n-1);
}

sum(4)
求值过程如下:
sum(4)
(4 + sum(3))
(4 + (3 + sum(2)))
(4 + (3 + (2 + sum(1))))
(4 + (3 + (2 + 1)))
(4 + (3 + 3))
(4 + 6)
10

普通递归时,内存需要记录调用的堆栈所处的深度和位置信息,在最底层计算返回值,再根据记录的信息,跳回上一层级计算,然后再跳回更高一层,依次运行,直到最外层的调用函数,cpu计算和内存消耗很多,而且当深度过大时,会出现堆栈溢出

通过尾递归优化后的代码:

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

sum(5, 0)   //10
求值过程如下:
sum(5, 0)
sum(4, 5)
sum(3, 9)
sum(2, 12)
sum(1, 14)
1+14
15

整个过程是现行的,调用一次(x,
total)后,会进入下一个栈,相关数据信息会跟随进入,不会放入堆栈保存。当计算完最后的值之后,直接返回到最上层的sum(5,
0)。这能有效的防止堆栈溢出。
尾部递归的的性能要高于传统纯函数的递归。

Point Free

  1. 把一些对象自带的方法转化成纯函数,不要命名转瞬即逝的中间变量
  2. 这个函数中,我们使用了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("abcd efgh");

这种风格能够帮助我们减少不必要的命名,让代码保持简洁和通用

10、函数式编程中的闭包
function makePowerFn(power){
      function powerFn(base){
            return Math.pow(base, power); 
      }
      return powerFn;
}
var square = makePowerFn(2);
square(3);  //9

函数式编程其实是函数的种种技巧的拼接,但是函数式编程会充盈着大量的闭包,使用完需要释放,防止内存泄漏。

声明式与命令式代码

命令式代码的意思就是,我们通过编写一条又一条指令去让计算机执行一些动作,这其中一般都会涉及到很多繁杂的细节。而声明式就要优雅很多了,我们通过写表达式的方式来声明我们想干什么,而不是通过一步一步的指示。

//命令式
let CEOs=[];
for(var i=0;i<companies.length;i++){
    CEOs.push(companies[i].CEO);
}
//声明式
let CEOs=companies.map(c=>c.CEO);

优缺点

函数式编程的一个明显的好处就是这种声明式的代码,对于无副作用的纯函数,我们完全可以不考虑函数内部是如何实现的,专注于编写业务代码。优化代码时,目光只需要集中在这些稳定坚固的函数内部即可。

相反,不纯的函数式的代码会产生副作用或者依赖外部系统环境,使用他们的时候总是要考虑这些不干净的副作用。在复杂的系统中,这对于程序员的心智来说是极大的负担。

二、比较流行的函数式编程库

  • RxJS
  • Cycle.js
  • Underscore.js
  • lodash
  • Ramda

惰性求值

function fn(){
    if(IE){//IE时
        fn=a;
    }else{//chrome时
        fn=b;
    }
    return fn;
}

第一次执行时会走if,然后fn重新赋值,第二次执行fn时,直接赋值不用判断,提高执行效率

1、RxJS

RxJS(Reactive Extensions for
JavaScript,JavaScript的响应式扩展),其函数响应式编程理念非常先进,虽然或许对于大部分应用环境来说,外部输入时间并不是太频繁,并不需要引入一个如此庞大的FRP(Functional
Reactive
Programming,函数响应式编程)体系,但我们也可以了解一下它的优秀特性。

在RxJS中,所有的外部输入(用户输入、网络请求等等)都被看做是一种“事件流”
用户点击了按钮——>网络请求成功——>用户键盘输入——>某个定时事件发生,这种事件流特别适合处理游戏,上上下下,举个简单例子,下面这段代码会监听点击事件,每2次点击事件会产生一次事件响应:

var clicks = Rx.Observable
      .fromEvent(document, 'click')
      .bufferCount(2)
      .subscribe(x => console.log(x)); //打印出前2次点击事件

高阶函数

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

//命令式
var add=function(a,b){
    return a+b;
};
funtion math(func,array){
    return func(array[0],array[1]);
}
math(add,[1,2]); //3
2、Cycle.js

Cycle.js
是一个基于RxJS的框架,它是一个彻底的FRP理念的框架,和React一样支持virtual
DOM,JSX语法,但现在似乎还没有看到大型应用经验。

本质的讲,它是在RxJS的基础上加入了对virtual
DOM,容器和组件的支持,比如下面就有一个简单的“开关”按钮:

function main(source){
      const sinks = {
            DOM: sources.DOM.select('input').events('click')
                  .map(ev => ev.target.checked)
                  .startWith(false)
                  .map(toggled => 
                  <div>
                        <input type="checkbox" />Toggle me
                        <p>{toggled ? 'ON' : 'OFF'}</p>
                  </div>
            )
      };
      return sinks;
}
const drivers = {
      DOM: makeDOMDriver('#app')
}

run(main, drivers);

尾调用优化

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

//不是尾递归,无法优化
function factorial(n){
    if(n===1) return 1;
    return n*factorial(n-1);
}
//尾递归
function factorial(n,total){
    if(n===1) return total;
    return factorial(n-1,n*total);
}//ES6强制使用尾递归

普通递归时,内存需要记录调用的堆栈所出的深度和位置信息。在最低层计算返回值,再根据记录的信息,跳会上一层级计算,然后再跳回到更高一层,依次运行,直到最外层的调用函数。在cpu计算和内存会消耗很多,而且当深度过大时,会出现堆栈溢出

function sum(x){
    if(x===1) return 1;
    return x+sum(x-1);
}
sum(5);  //15  递归

function sum(x,total){
    if(x===1) return x+total;
    return sum(x-1,x+total);
}
sum(5,0); 
sum(4,5);
sum(3,9);
sum(2,12);
sum(1,14);
15 //尾递归,每次执行之后,函数重新传入参数,直到结束

整个计算过程是线性的,调用一次sum(x,total)后,会进入下一个栈,相关的数据信息和跟随进入,不再放在堆栈上保存。当计算完最后的值之后,直接返回到最上层的sum(5,0).这能有效的防止堆栈溢出。
在ECMAScript6,我们将迎来尾递归优化,通过尾递归优化,javascript代码在解释成机器码的时候,将会向while看起,也就是说,同时拥有数学表达能力和while的效能。

3、Underscore.js

Underscore是一个JavaScript工具库,它提供了一整套函数式编程的实用功能。但没有扩展任何JavaScript内置对象。它解决了这样的问题——“如果我面对一个空白HTML页面,并希望立即开始工作,我需要什么?”,它弥补了jQuery没有实现的功能,同时又是backbone必不可少的部分。

underscore提供了100多个函数,包括常用的map、filter、invoke等等,还有一些辅助函数,如:函数绑定,JavaScript模板功能,创建快速索引,以及强类型相等测试等。

闭包

自己领会

4、lodash.js

loadash是一个具有一致接口、模块化、高性能等特性的JavaScript工具库,是underscore的fork,最初目的也是“一致的跨浏览器行为,并改善性能”。

lodash采用延迟计算,意味着我们的链式方法在显式或者隐式的value()调用之前是不会执行的,因此lodash可以进行shortcut(捷径)fusion(融合)这样的优化,通过合并链式大大降低迭代的次数,从而大大提升其执行性能。

就如同jQuery在全部函数前加全局的”$“一样,lodash使用全局的”_”来提供对工具的快速访问。

一个深层次查找属性值的示例:

var _ = require('lodash');

// Fetch the name of the first pet from each owner
var ownerArr = [{
    "owner": "Colin",
    "pets": [{"name":"dog1"}, {"name": "dog2"}]
}, {
    "owner": "John",
    "pets": [{"name":"dog3"}, {"name": "dog4"}]
}];

// Array's map method.
ownerArr.map(function(owner){
   return owner.pets[0].name;
});

// Lodash
_.map(ownerArr, 'pets[0].name');

_.map 方法是对原生 map
方法的改进,使用字符串处理深层次嵌套属性的方式代替回调函数那些冗余的代码。

函数式编程比较火热的库

  • Rxjs //截流与仿抖
  • cyclejs
  • lodashjs
  • underscorejs //开始学最佳的库
  • ramadajs

需要学习

5、Ramda

Ramda是一个非常优秀的工具库,跟同类比更函数式,主要体现在一下原则:

  • 1、Ramda里面提供的额函数全部是柯里化的意味着函数没有默认参数可选,从而减轻认知函数的难度。(即:所有方法都支持柯里化,所有多参数的函数,默认都可以单参数使用。)
    如下例子:

var R = require('ramda');

var square = n => n * n;

// 写法一
R.map(square, [4, 8])

// 写法二
R.map(square)([4, 8])
// 或者
var mapSquare = R.map(square);
mapSquare([4, 8]);

上面代码中,写法一是多参数版本,写法二是柯里化以后的单参数版本。Ramda
都支持,并且推荐使用第二种写法。

  • 2、Ramda推崇point
    free,简单的说就是使用简单函数组合实现一个复杂功能,而不是单独写一个函数操作临时变量。

  • 3、Ramda有个非常好的参数占位符R._ ,大大减轻了函数在point
    free过程中参数位置的问题。

和underscore、lodash比较,Ramda要干净很多。

范畴与容器

  1. 我们可以把“范畴”想象成是一个容器,里面包含两样东西。值(value)、值的变形关系,也就是函数。
  2. 范畴论使用函数,表达范畴之间的关系。
  3. 伴随着范畴论的发展,就发展出一整套函数的运算方法。这套方法起初只用于数学运算,后来有人将它在计算机上实现了,就变成了今天的“函数式编程”。
  4. 本质上,函数式编程只是范畴论的运算方法,跟数理逻辑、微积分、行列式式同一类东西,都是数学方法,只是碰巧他能用来写程序。为什么函数式编程要求函数必须是纯的,不能有副作用?因为它是一种数学运算,原始目的就是求值,不做其他事情,否则就无法满足函数运算法则了。

函子是函数式编程里面最重要的数据类型,也是基本的运算单位和功能单位。它首先是一种范畴,也就是说,是一个容器,包含了值和变形关系。比较特殊的是,它的变形关系可以依次作用于每一个值,将当前容器变形成另一个容器。

三、函数式编程的实际应用场景

  • 易调试、热部署、并发
  • 单元测试

容器、Functor(函子)

  1. $(…)返回的对象并不是一个原生的DOM对象,而是对于原生对象的一种封装,这在某种意义上就是一个”容器”(但它并不函数式)
  2. Functor(函子)遵守一些特定规则的容器类型
  3. Functor是一个对于函数调用的抽象,我们赋予容器自己去调用函数的能力。把东西装进一个容器,只留出一个接口map给容器外的函数,map一个函数时,我们让容器自己来运行这个函数,这样容器就可以自由地选择何时何地如何操作这个函数,以致于拥有惰性求值、错误处理、一步调用等非常牛掰的特性

var Container=function(x){
    this.__value=x;
}
//函数式编程一般约定,函子有一个of方法
Container.of=x=>new Container(x);
//Container.of('abcd);
//一般约定,函子的标志就是容器具有map方法。该方法将容器里面的每一个值,映射到另一个容器。
Container.prototype.map=function(f){
    return Container.of(f(this.__value));
}
Container.of(3)
    .map(x=>x+1)                //Container(4)
    .map(x=>'Result is '+x);        //Container('Result is 4')
1、易调试、热部署、并发

① 函数式编程中的每个符号都是const的,于是没有什么函数是有副作用的额。


函数式编程不需要考虑“死锁”(deadlock),因为它不修改变量,所以根本不存在“锁”线程的问题。


函数式编程中的所有状态都是传给函数的参数,而参数都是存储在栈上的,这一特性,让软件的热部署变得十分简单。只要比较一下正在运行的代码和新的代码获得一个diff,然后用这个diff更新现有的代码,新代码的热部署就完成了。

Maybe 函子

函子接受各种函数,处理容器内部的值,这里就有一个问题,容器内部的值可能是一个空值(比如null),而外部函数未必有处理空值的机制,如果传入空值,很可能就会出错。

Functor.of(null).map(function(s){
    return s.toUpperCase();
});
//TypeError
class Maybe extends Functor{
    map(f){
        return this.val?Maybe.of(f(this.val)):Maybe.of(null);
    }
}
Maybe.of(null).map(function(s){
    return s.toUpperCase();
});
//Maybe(null) //报错,未定义

var Maybe=function(x){
    this.__value=x;
}
Maybe.of=function(x){
    return new Maybe(x);
}
Maybe.prototype.map=function(f){
    return this.isNothing()?Maybe.of(null):Maybe.of(f(this.__value));
}
Maybe.prototype.isNothing=function(){
    return (this.__value===null||this.__value===undefined);
}
Maybe(null) //不会报错了
//新的容器我们称为Maybe
2、单元测试


严格函数式编程的每一个符号都是对直接量或者表达式结果的引用,没有函数产生副作用。

②这是单元测试者的梦中仙境(wet
dream),对被测试程序中的每个函数,你只需在意其参数,而不必考虑函数调用顺序,不用谨慎地设置外部状态,所有要做的就是传递代表了边际情况的参数。

Either 函子

条件运算if…else
是常见的运算之一,函数式编程里面,使用Either函子表达。Either函子内部有两个值:左值(left)和右值(right)。右值是正常情况下使用的值,左值是右值不存在时使用的默认值。

class Either extends Functor{
    constructor(left,right){
        this.left=left;
        this.right=right;
    }
    map(f){
        //右值存在变右值,否则变左值
        return this.right?Either.of(this.left,f(this.right)):Either.of(f(this.left),this.right);
    }
}
Either.of=function(left,right){
    return new Either(left,right);
}

var addOne=function(x){
    return x+1;
}
Either.of(5,6).map(addOne); //Either(5,7);
Either.of(1,null).map(addOne);  //Either(2);
Either
    //右值中有address这个属性,则覆盖原来的xxx,否则使用默认的xxx
    .of({address:'xxx'},currentUser.address)    .map(updateField);

es5写法

错误处理、Either

var Left=function(x){
    this.__value=x;
}
var Rigth=function(x){
    this.__value=x;
}
Left.of=function(x){
    return new Left(x);
}
Right.of=function(x){
    return new Right(x);
}
Left.prototype.map=functin(f){
    return this;
}
Right.prototype.map=function(f){
    return Right.of(f(this.__value));
}

Left和Right唯一的区别就在于map方法的实现,Right.map的行为和我们之前提到的map函数一样。但是Left.map就很不同了:它不会对容器做任何事情,只是很简单地把这个容器拿进来又扔出去。这个特性意味着,Left可以用来传递一个错误消息。

var getAge = user => user.age ? Right.of(user.age):Left.of("Error");;
getAge({name:'stark',age:'21'}).map(age=>'Age is '+age);
//Right('Age is 21');
getAge({name:'stark'}).map({age=>'Age is '+age});
//Left('Error');

Left
可以让调用链中任意一环的错误立即返回到调用链的尾部,这给我们错误处理带来了很大的方便,再也不用一层又一层的Try/catch

四、总结

函数式编程不应该被视作灵丹妙药,相反,它应该被视为我们现有工具的一个很自然的补充——它带来了更高的可组合性,灵活性以及容错性。现代的JavaScript库已经开始尝试拥抱函数式编程的概念以获取这些优势。比如,Redux作为一种Flux的变种实现,核心理念也是状态机和函数式编程。

如果说面向对象编程降低复杂度是靠良好的封装、继承、多态以及接口的定义的话,那么函数式编程就是通过纯函数以及他们的组合、柯里化、Functor(函子)等技术来降低系统复杂度,而React、Rxjs、Cycle.js正是这种理念的代言。

AP因子

函子里面包含的值,完全可能是函数。我们可以想象这样一种情况,一个函子的值是数值,另一个函子的值是函子。

class Ap extends Functor{
    ap(F){
        return Ap.of(this.val(F.val));
    }
}
Ap.of(addTwo).ap(Functor.of(2));

实例

function Functor(val){
    this.__val=val;
}
Functor.of=function(val){
    return new Functor(val);
}
Functor.prototype.map=function(fn){
    return Functor.of(fn(this.__val));
}
function addTwo(x){
    return x+2;
}
function Ap(val){
    Functor.call(this,val);
}
Ap.of=function(val){
    return new Ap(val);
}
var __proto=Object.create(Functor.prototype);
__proto.constructor=Ap.prototype.constructor;
Ap.prototype=__proto;
Ap.prototype.ap=function(F){
    return Ap.of(this.__val(F.__val));
}

const A=Functor.of(2);
const B=Ap.of(addTwo);
console.log(B.ap(A));  //4
//console.log(B.ap(A).ap(A));此时会报错,B.ap(A)其中A不是个函数

IO

真正的程序总要去接触肮脏的世界

function readLoaclStorage(){
    return window.localStorage;
}

Io跟前面那几个Functor不同的地方在于,他的__value是一个函数。它把不纯的操作(比如IO、网络请求、DOM)包裹到一个函数内,从而延迟这个操作的执行。所以我们认为,IO包含的是被包裹的操作的返回值。

IO其实也算是惰性求值

IO负责了调用链积累了很多很多不纯的操作,带来的复杂性和不可维护性。

import _ from 'lodash';
var compose=_.flowRight;
var IO=function(f){
    this._value=f;
}
IO.of=x=>new IO(_=>x);
IO.prototype.map=function(f){
    return new IO(compose(f,this.__value));
}

Monad

Monad就是一种设计模式,表示将一个运算过程,通过函数拆解成互相连接的多个步骤。你只要提供下一步运算所需的函数,整个运算就会自动进行下去。

Promise就是一种Monad

Monad糖我们避开了嵌套地狱,可以轻松地进行深度嵌套的函数式编程,比如IO和其它异步任务

Maybe.of(
    Maybe.of(
        Maybe.of({name:'Mulburry',number:99})
    )
)
class Monad extends Functor{
    join(){
        return this.val;
    }
    flatMap(f){
        return this.map(f).join();
    }
}

Monad
函子的作用是,总是返回一个单层的函子。它有一个flatMap方法,与map方法作用相同,唯一的区别是如果生成了一个嵌套函子,他会取出后这内部的值,保证返回的永远是一个单层的容器,不会出现嵌套的情况。

如果函数f返回的是一个函子,那么this.map(f)就会生成一个嵌套的函子。所以,join方法保证了flatMap方法总是返回一个单层的函子。这意味着嵌套的汉子会被铺平(flatMap)

发表评论

电子邮件地址不会被公开。 必填项已用*标注