JavaScript:函数式编程基本概念学习

基本概念

  • 函数式编程(Functional programming)与面向对象编程(Object-oriented programming)和过程式编程(Procedural programming)并列的编程范式。
    过程式编程毫无边界,只关心完成目标的具体操作步骤,这个很接近机器的指令式思维。
    面向对象编程,开始有边界了。第一层边界是对象,有隔离,有封装;第二层边界是环境;
    函数式编程的边界进一步缩小。第一层边界是函数,独立的,纯的函数,不依赖外界的状态。第二层边界是容器(集合),从一个集合变换到另外一集合。这两个集合是互相独立的,只是有映射关系,而且这种映射关系是单向的,一对一或者是多对一的。
  • 最主要的特征是,函数是第一等公民。所谓“一等公民”,其实就是“普通公民”。函数可以是参数,可以是返回值,可以是数组的成员等等。
  • 值的集合组成一个容器,或者叫范畴category;值的变换关系叫函数,可以一对一,多对一,但是不能一对多。(对于给定的输入,有确定的输出)。函数式编程的本质是从一个容器变换到另外一个容器:变换的函数是容器的方法,变换的成员是容器中的成员,容器中的成员个数保持不变,或者越来越少,甚至最后变成一个(reduce)。为了简单起见,函数的输入参数是一个(curry),输出也是一个(可能是函数),直到所有的参数都处理完,最后形成一条串行管道(pipe)。
    容器和成员变换关系,是两大基本元素。思路要转变到这两个焦点上面。
  • 函数式编程有两个最基本的运算:合成和柯里化。
  • 函数式编程要求是纯函数,但是现实的异步编程基本上都是不纯的函数。所以重点是想办法将不纯的函数变成纯的函数。

纯函数

纯函数的定义是,对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,也不依赖外部环境的状态。
函数f的概念就是,对于输入x 产生一个输出y = f(x)

  • 纯的容器(集合)的方法(函数)不能改变自己所属容器(集合)的成员,应该返回一个新的容器(集合),其成员是变换(映射)后的结果。
    比如数组的splice方法操作自身成员,是不纯的;slice方法返回一个映射结果的新数组,是纯的
var input = [1,2,3,4,5];
var output = [];

// Array.slice是纯函数,因为它没有副作用,对于固定的输入,输出总是固定的
// 可以,这很函数式
output = input.slice(0,3);
//=> [1,2,3]
output = input.slice(0,3);
//=> [1,2,3]

// Array.splice是不纯的,它有副作用,对于固定的输入,输出不是固定的
// 这不函数式
output = input.splice(0,3);
//=> [1,2,3]
output = input.splice(0,3);
//=> [4,5]
output = input.splice(0,3);
//=> []
  • 纯的函数不能依赖函数外部的变量(状态),函数跟外部的接口只能通过参数,并且参数要求是值传递,不能是引用传递(有共享的内存,函数就依赖外部的状态了)。
// 不纯的
var minimum = 21;

var checkAge = function(age) {
  return age >= minimum;
};


// 纯的
var checkAge = function(age) {
  var minimum = 21;
  return age >= minimum;
};

函数组合

  • 如果一个值要经过多个函数,才能变成另外一个值,就可以把所有中间步骤合并成一个函数,这叫做"函数的合成"(compose)
var compose = function(g,f) {
  return function(x) {
    return g(f(x));
  };
};

var toUpperCase = function(x) { return x.toUpperCase(); };   // f
var exclaim = function(x) { return x + '!'; };   // g
var shout = compose(exclaim, toUpperCase);  // 先f后g

console.log(shout("send in the clowns"));
//=> "SEND IN THE CLOWNS!"
  • 函数的合成还必须满足结合律。但是一般不满足交换律。实际使用中这条链可能很长,结合的个数也不限于2个,可以有多个,具体怎么结合,可以根据具体的业务来。并且链路也可能有多条,交叉,但是要求单向流动,一般不能有回路。
函数组合.png
compose(h, compose(g, f))
// 等同于
compose(compose(h, g), f)
// 等同于
compose(h, g, f)
  • 图上的箭头和顺序“从左向右”,但是书写和执行的顺序“从右向左”,刚好相反,这点要注意。如果非要搞成一致,那么建议画图的时候,箭头和顺序也“从右向左”。没有为什么,约定俗成罢了,习惯了就好了。
  • ABCD看成是容器(或者集合),fgh看成函数(或者映射),思维转换过来,习惯了就好了。函数(属性,比如map)是容器(类,比如Array)的方法,作用的对象是容器的成员。只关注输入输出的映射关系,不关心具体的循环遍历方式以及中间变量(比如循环序号i
  • fgh等函数只允许有一个参数,简化处理;多参数的情况就把链路拉长一点
  • 从起点到终点,容器内的成员只能保持不变或者越来越少。原因是函数的映射关系只能是一对一或者多对一,不能一对多。

Point Free

不要命名转瞬即逝的中间变量

//这不Piont free
var f = str => str.toUpperCase().split(' ');

这个函数中,我们使用了 str 作为我们的中间变量,但这个中间变量除了让代码变得长了一点以外是毫无意义的。

var compose = (f, g) => (x => f(g(x)));

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

var f = compose(split(' '), toUpperCase);

var result = f("abcd efgh");
console.log(result);
// [ 'ABCD', 'EFGH' ]

柯里化(curry)

  • 所谓"柯里化",就是把一个多参数的函数,转化为单参数函数。
  • 只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。形成一个调用链,直到处理完所有的参数。
  • 也是从左向右书写,但是从右到左执行,将最外层的参数写在最左边,最后处理。最内层的参数写在最右边,最先处理。
var add = function(x) {
  return function(y) {
    return x + y;
  };
};

var increment = add(1);
var addTen = add(10);

increment(2);
// 3

addTen(2);
// 12

函子(Functor)

  • 函子是函数式编程里面最重要的数据类型,也是基本的运算单位和功能单位。
  • 它首先是一种范畴,也就是说,是一个容器,包含了值和变形关系。比较特殊的是,它的变形关系可以依次作用于每一个值,将当前容器变形成另一个容器。
  • 任何具有map方法的数据结构,都可以当作函子的实现。
  • 一般约定,函子的标志就是容器具有map方法。该方法将容器里面的每一个值,映射到另一个容器。
  • 函数式编程里面的运算,都是通过函子完成,即运算不直接针对值,而是针对这个值的容器----函子。
  • 函子本身具有对外接口(map方法),各种函数就是运算符,通过接口接入容器,引发容器里面的值的变形。
  • 学习函数式编程,实际上就是学习函子的各种运算。函数式编程就变成了运用不同的函子,解决实际问题。
  • 函数式编程一般约定,函子有一个of方法,用来生成新的容器。
class Functor {
  constructor(val) { 
    this.val = val; 
  }

  static of(val) {
    return new Functor(val); 
  }

  map(f) {
    return new Functor(f(this.val));
  }
}

console.log(Functor.of(2).map(function (two) {
  return two + 2;
})); 
// Functor { val: 4 }

console.log(Functor.of('flamethrowers').map(function(s) {
  return s.toUpperCase();
}));
// Functor { val: 'FLAMETHROWERS' }

console.log(Functor.of('bombs').map(function(s){
    return s.concat(' away');
}).map(function(s) {
    return s.length;
}));
// Functor { val: 10 }
  • 这个of方法这里是一个静态方法,用类名来访问,替代构造方法,将new关键字隐藏起来
  • 这里的函数执行顺序是“从左到右”的,和示意图流的方向一致。这个和前面函数的合成和柯里化那部分是相反的,要注意区别。

Maybe 函子

Maybe 函子是为了处理空值而设计的。简单说,它的map方法里面设置了空值检查。

class Maybe extends Functor {
  static of(val) {
    return new Maybe(val); 
  }

  map(f) {
    return this.val ? Maybe.of(f(this.val)) : Maybe.of(null);
  }
}

try {
  console.log(Functor.of(null).map(function (s) {
    return s.toUpperCase(); // Functor没有空值检查,当输入null时抛出异常
  }));
} catch (error) {
  console.log(error.message);
  // Cannot read property 'toUpperCase' of null
}

console.log(Maybe.of(null).map(function (s) {
  return s.toUpperCase();
}));
// Maybe { val: null }

Either 函子

  • 条件运算if...else是最常见的运算之一,函数式编程里面,使用 Either 函子表达。
  • Either 函子内部有两个值:左值(Left)和右值(Right)。右值是正常情况下使用的值,左值是右值不存在时使用的默认值。
class Either {
  constructor(left, right) {
    this.left = left;
    this.right = right;
  }

  static of(left, right) {
    return new Either(left, right);
  }

  map(f) {
    return this.right ? 
      Either.of(this.left, f(this.right)) :
      Either.of(f(this.left), this.right);
  }
}

// Either用来提供默认值
var addOne = function (x) {
  return x + 1;
};
console.log(Either.of(5, 6).map(addOne));
// Either { left: 5, right: 7 }
console.log(Either.of(1, null).map(addOne));
// Either { left: 2, right: null }
  • Either 函子的另一个用途是代替try...catch,使用左值表示错误。一般来说,所有可能出错的运算,都可以返回一个 Either 函子。
function parseJSON(json) {
  try {
    return Either.of(null, JSON.parse(json));
  } catch (e: Error) {
    return Either.of(e, null);
  }
}

ap 函子

  • 容器中的成员是函数
  • 这些函数是curry函数
  • 包含ap方法
  • ap方法的参数不是函数,而是另一个函子
  • ap 函子的意义在于,对于那些多参数的函数,就可以从多个容器之中取值,实现函子的链式操作。
  • 取ap函子中的函数,参数从其他的函子中取,最后得到一个结果的集合。这个集合不是ap函子,也不是参数函子,而是结果的集合。
class Ap {
  constructor(val) { 
    this.val = val; 
  }

  static of(val) {
    return new Ap(val); 
  }

  map(f) {
    return new Ap(f(this.val));
  }

  ap(F) {
    return Ap.of(this.val(F.val));
  }
}

class Maybe {
  constructor(val) { 
    this.val = val; 
  }

  static of(val) {
    return new Maybe(val); 
  }

  map(f) {
    return this.val ? Maybe.of(f(this.val)) : Maybe.of(null);
  }
}

function add(x) {
  return function (y) {
    return x + y;
  };
}

var reslut = Ap.of(add).ap(Maybe.of(2)).ap(Maybe.of(3));
console.log(reslut);
// Ap { val: 5 }  

Monad 函子

  • Monad 函子的作用是,总是返回一个单层的函子。
  • 它有一个flatMap方法,与map方法作用相同,唯一的区别是如果生成了一个嵌套函子,它会取出后者内部的值,保证返回的永远是一个单层的容器,不会出现嵌套的情况。
  • flatMap方法也叫chain方法,就是比普通的map多一个取值操作
  • Monad 函子的重要应用,就是实现 I/O (输入输出)操作。
class Monad extends Functor {
  join() {
    return this.val;
  }
  flatMap(f) {
    return this.map(f).join();
  }
}
  • Monad,英文单词翻译成中文是“单子”。作用是实现输入输出都是“类”的函数调用,最后形成一个链式调用。直观的感觉就是“类”(或者叫容器)中的方法,全部返回“类”本身,那么就可以一直点点点下去,将很多函数调用写在一行,成为“一条链”
  • 从本质上讲,输入输出都是“类”的函数是没有意义的。值到值的变换才是函数的本质。借助Monad这个概念,实现了函数链式调用。首先取出传过来的“类”参数中的值,经过函数运算,得到结果,然后把这个值再“封装”到“类”中,返回这个类。

图解 Monad这篇文章很好地描述了这个过程,好好看看。

IO 操作

  • I/O 是不纯的操作,普通的函数式编程没法做,这时就需要把 IO 操作写成Monad函子,通过它来完成。
  • 读取文件和打印本身都是不纯的操作,但是readFileprint却是纯函数,因为它们总是返回 IO 函子。
var fs = require('fs');

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

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

参考文章

函数式编程入门教程
这篇博客写的很好,对于函数式编程概念的理解很有帮助,强烈推荐

JS函数式编程指南
这个都写成书了,应该好好看看,写得很好,强烈推荐

JavaScript函数式编程(一)
JavaScript函数式编程(二)
JavaScript函数式编程(三)
很用心写的一系列文章,值得看看

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,099评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,473评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,229评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,570评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,427评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,335评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,737评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,392评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,693评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,730评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,512评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,349评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,750评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,017评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,290评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,706评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,904评论 2 335

推荐阅读更多精彩内容