引言&前言

本书主要探索函数式编程[1](FP)的核心思想。在此过程中,作者不会执着于使用大量复杂的概念来进行诠释,这也是本书的特别之处。

这并不是说,各种复杂繁琐的概念是无意义的,更不是说,函数式编程者滥用了它们。一旦你完全掌握了轻量的函数式编程内容,你将会/但愿会想要对函数式编程的各种概念进行更正式更系统的学习。

单子是自函子范畴上的一个幺半群

本书 尽可能少用晦涩难懂的专业术语。我们将尝试以更实用的方法来探讨函数式编程,而非纯粹的学术角度。毫无疑问,肯定会有专业术语。但是我将会小心谨慎的引入这些术语并解释为何它们如此重要。

第1章:为什么使用函数式编程?

置信度

信任是什么意思?信任是指你通过读代码,不仅是跑代码,就能理解这段代码能干什么事,而不只是停留在它可能是干什么的层面。

通过读代码就能对我们的程序更有信心,我相信函数式编程技术的基础构成,是本着这种心态设计的

交流渠道

代码的主要作用是方便人与人交流。

我们的大部分时间其实都是在维护别人的代码(或自己的老代码),只有少部分时间是在敲新代码。

FP可以做到当我们读取一段代码时,我们将花费更少的时间来进行定位。我们的重点将在于如何组建所有已知的“乐高片段”,而不是这些“乐高片段”是什么意思。

可读性曲线

image.png

接受

一项技术你怎么称呼它不重要,重要的是理解它是什么并且它是怎么工作的。这并不是说共享术语不重要,它无疑可以简化经验丰富的专业人士之间的交流。但对学习者来说,它有点分散人的注意力。

形式主义总是阻碍我们前进。

你不需要它

YAGNI - You Ain’t Gonna Need It

你掌握了函数式编程并不意味着你一定得用它。此外,解决问题的方法很多,即使你掌握了更精炼的方法,能对维护和可扩展性更”经得起未来的考验”,但更轻量的函数式编程模式可能更适合该场景。

总结

这就是 JavaScript 轻量级函数式编程。我们的目标是学会与代码交流,而不是在符号或术语的大山下被压的喘不过气。希望这本书能开启你的旅程!

第2章:函数基础

什么是函数

简要的数学回顾

image.png

在数学中,函数总是获取一些输入值,然后给出一个输出值。你能听到一个函数式编程的术语叫做“态射”:这是一个优雅的方式来描述一组值和另一组值的映射关系,就像一个函数的输入值与输出值之间的关联关系。

函数 vs 程序

程序就是一个任意的功能集合。它或许有许多个输入值,或许没有。它或许有一个输出值( return 值),或许没有。
而函数则是接收输入值,并明确地 return 值。

函数输入

arguments 是你输入的值(实参), parameters 是函数中的命名变量(形参),用于接收函数的输入值

输入计数

这里有一个特殊的术语:Arity。Arity 指的是一个函数声明的形参数量。

随着输入而变化的函数

警告: 要对方便的诱惑有警惕之心。因为你可以通过这种方式设计一个函数,即使可以立即使用,但这个设计的长期成本可能会让你后悔。

函数输出

是否需要避免函数有可重构的多个输出?或许将这个函数分为两个或更多个更小的单用途函数。有时会需要这么做,有时可能不需要,但你应该至少考虑一下。

提前return

我认为在许多可读性的问题上,是因为我们不仅使用 return 返回不同的值,更把它作为一个流控制结构——在某些情况下可以提前退出一个函数的执行。我们显然有更好的方法来编写流控制( if 逻辑等),也有办法使输出路径更加明显

我不是说,你只能有一个 return,或你不应该提早 return,我只是认为在定义函数时,最好不要用 return 来实现流控制,这样会创造更多的隐含意义。尝试找出最明确的表达逻辑的方式,这往往是最好的办法

return 的输出

  • 修改全局变量
  • 副作用修改了引用的对象

隐式函数输出在函数式编程中有一个特殊的名称:副作用。当然,没有副作用的函数也有一个特殊的名称:纯函数。

函数功能

将其他函数视为值的函数是高阶函数的定义。函数式编程者们应该学会这样写!

保持作用域

我们将在本书的后续中大量使用闭包。如果抛开整个编程来说,它可能是所有函数式编程中最重要的基础。

句法

什么是名称?

匿名函数通常显示为:(anonymous function)

在 ES6 中,匿名表达式可以通过名称引用来获得名称。

  1. var x = function(){};
  2. x.name; // x

我有许多个理由可以解释命名函数比匿名函数更可取。事实上,我甚至认为匿名函数都是不可取的。相比命名函数,他们没有任何优势。

没有 function 的函数

虽然我不喜欢在我的应用程序中使用 =>,但我们将在本书的其余部分多次使用它,特别是当我们介绍典型的函数式编程实战时,它能简化、优化代码片段中的空间。不过,增强或减弱代码的可读性也取决你自己做的决定。

来说说 This ?

this 因为各种原因,不符合函数式编程的原则。其中一个明显的问题是隐式 this 共享。

从我的角度来看,问题不在于使用对象来进行操作,而是我们试图使用隐式输入取代显式输入。当我戴上名为函数式编程的帽子时,我应该把 this 放回衣架上。

总结

函数是强大的。

现在,让我们清楚地理解什么是函数:它不仅仅是一个语句或者操作的集合,而且需要一个或多个输入(理想情况下只需一个!)和一个输出。

函数内部的函数可以取到闭包外部变量,并记住它们以备日后使用。这是所有程序设计中最重要的概念之一,也是函数式编程的基础。

要警惕匿名函数,特别是 => 箭头函数。虽然在编程时用起来很方便,但是会对增加代码阅读的负担。我们学习函数式编程的全部理由是为了书写更具可读性的代码,所以不要赶时髦去用匿名函数。

别用 this 敏感的函数。这不需要理由。

第3章:管理函数的输入

立即传参和稍后传参

  1. function ajax(url,data,callback) {
  2. // ..
  3. }

想象一个场景,你要发起多个已知 URL 的 API 请求,但这些请求的数据和处理响应信息的回调函数要稍后才能知道。

  1. function getPerson(data,cb) {
  2. ajax( "http://some.api/person", data, cb );
  3. }
  4. function getOrder(data,cb) {
  5. ajax( "http://some.api/order", data, cb );
  6. }

手动指定这些外层函数当然是完全有可能的,但这可能会变得冗长乏味,特别是不同的预设实参还会变化的时候。

用一句话来说明发生的事情:getOrder(data,cb)ajax(url,data,cb) 函数的偏函数(partially-applied functions)。
**
让我们定义一个 partial(..) 实用函数:

  1. function partial(fn,...presetArgs) {
  2. return function partiallyApplied(...laterArgs){
  3. return fn( ...presetArgs, ...laterArgs );
  4. };
  5. }

bind(..)

var getPerson = ajax.bind( null, "http://some.api/person" );

那个 null 只会给我带来无尽的烦恼。

将实参顺序颠倒

  1. function reverseArgs(fn) {
  2. return function argsReversed(...args){
  3. return fn( ...args.reverse() );
  4. };
  5. }

一次传一个

柯里化(currying)技术, 该技术将一个期望接收多个实参的函数拆解成连续的链式函数(chained functions),每个链式函数接收单一实参(实参个数:1)并返回另一个接收下一个实参的函数

  1. function curry(fn,arity = fn.length) {
  2. return (function nextCurried(prevArgs){
  3. return function curried(nextArg){
  4. var args = prevArgs.concat( [nextArg] );
  5. if (args.length >= arity) {
  6. return fn( ...args );
  7. }
  8. else {
  9. return nextCurried( args );
  10. }
  11. };
  12. })( [] );
  13. }

我们用 curry(..) 函数来实现此前的 ajax(..) 例子:

  1. var curriedAjax = curry( ajax );
  2. var personFetcher = curriedAjax( "http://some.api/person" );
  3. var getCurrentUser = personFetcher( { user: CURRENT_USER_ID } );
  4. getCurrentUser( function foundUser(user){ /* .. */ } );

柯里化和偏应用有什么用?

柯里化风格(sum(1)(2)(3))还是偏应用风格(partial(sum,1,2)(3)

  • 使用柯里化和偏应用可以将指定分离实参的时机和地方独立开来(遍及代码的每一处),而传统函数调用则需要预先确定所有实参
  • 当函数只有一个形参时,我们能够比较容易地组合它们


如何柯里化多个实参?

严格柯里化:一次只能传一个参数
松散柯里化: 允许你传入超过形参数量

  1. function looseCurry(fn,arity = fn.length) {
  2. return (function nextCurried(prevArgs){
  3. return function curried(...nextArgs){
  4. var args = prevArgs.concat( nextArgs );
  5. if (args.length >= arity) {
  6. return fn( ...args );
  7. }
  8. else {
  9. return nextCurried( args );
  10. }
  11. };
  12. })( [] );
  13. }

反柯里化

将类似 f(1)(2)(3) 的函数变回类似 g(1,2,3) 的函数

  1. function uncurry(fn) {
  2. return function uncurried(...args){
  3. var ret = fn;
  4. for (let i = 0; i < args.length; i++) {
  5. ret = ret( args[i] );
  6. }
  7. return ret;
  8. };
  9. }
  10. // ES6 箭头函数形式
  11. var uncurry =
  12. fn =>
  13. (...args) => {
  14. var ret = fn;
  15. for (let i = 0; i < args.length; i++) {
  16. ret = ret( args[i] );
  17. }
  18. return ret;
  19. };

请不要以为 uncurry(curry(f))f 函数的行为完全一样,如果你少传了实参,就会得到一个仍然在等待传入更多实参的部分柯里化函数

只要一个实参

实用函数

  1. function unary(fn) {
  2. return function onlyOneArg(arg){
  3. return fn( arg );
  4. };
  5. }
  6. // ES6 箭头函数形式
  7. var unary =
  8. fn =>
  9. arg =>
  10. fn( arg );

传一个返回一个

  1. function identity(v) {
  2. return v;
  3. }
  4. // ES6 箭头函数形式
  5. var identity =
  6. v =>
  7. v;
  8. var words = " Now is the time for all... ".split( /\s|\b/ );
  9. words;
  10. // ["","Now","is","the","time","for","all","...",""]
  11. words.filter( identity );

恒定参数

  1. function constant(v) {
  2. return function value(){
  3. return v;
  4. };
  5. }
  6. // or the ES6 => form
  7. var constant =
  8. v =>
  9. () =>
  10. v;

仔细思考下面,命名函数调用比匿名声明更易读和统一

  1. p1.then( foo ).then( () => p2 ).then( bar );
  2. // 对比:
  3. p1.then( foo ).then( constant( p2 ) ).then( bar );

扩展在参数中的妙用

自动扩展数组到参数

  1. function spreadArgs(fn) {
  2. return function spreadFn(argsArr) {
  3. return fn( ...argsArr );
  4. };
  5. }
  6. // ES6 箭头函数的形式:
  7. var spreadArgs =
  8. fn =>
  9. argsArr =>
  10. fn( ...argsArr );

相反的方法,聚合参数

  1. function gatherArgs(fn) {
  2. return function gatheredFn(...argsArr) {
  3. return fn( argsArr );
  4. };
  5. }
  6. // ES6 箭头函数形式
  7. var gatherArgs =
  8. fn =>
  9. (...argsArr) =>
  10. fn( argsArr );

参数顺序的那些事儿

一般的参数都是有序数组这种,对于参数多的情况下难以通过下标控制

使用对象解构编程命名参数,来让代码更易读

  1. function partialProps(fn,presetArgsObj) {
  2. return function partiallyApplied(laterArgsObj){
  3. return fn( Object.assign( {}, presetArgsObj, laterArgsObj ) );
  4. };
  5. }
  6. function curryProps(fn,arity = 1) {
  7. return (function nextCurried(prevArgsObj){
  8. return function curried(nextArgObj = {}){
  9. var [key] = Object.keys( nextArgObj );
  10. var allArgsObj = Object.assign( {}, prevArgsObj, { [key]: nextArgObj[key] } );
  11. if (Object.keys( allArgsObj ).length >= arity) {
  12. return fn( allArgsObj );
  13. }
  14. else {
  15. return nextCurried( allArgsObj );
  16. }
  17. };
  18. })( {} );
  19. }

使用方式

  1. function foo({ x, y, z } = {}) {
  2. console.log( `x:${x} y:${y} z:${z}` );
  3. }
  4. var f1 = curryProps( foo, 3 );
  5. var f2 = partialProps( foo, { y: 2 } );
  6. f1( {y: 2} )( {x: 1} )( {z: 3} );
  7. // x:1 y:2 z:3
  8. f2( { z: 3, x: 1 } );
  9. // x:1 y:2 z:3

们不用再为参数顺序而烦恼了!👍!

属性扩展

对于不能改变函数签名的多参函数要咋整?

使用如下黑科技:

  1. function spreadArgProps(
  2. fn,
  3. propOrder =
  4. fn.toString()
  5. .replace( /^(?:(?:function.*\(([^]*?)\))|(?:([^\(\)]+?)\s*=>)|(?:\(([^]*?)\)\s*=>))[^]+$/, "$1$2$3" )
  6. .split( /\s*,\s*/ )
  7. .map( v => v.replace( /[=\s].*$/, "" ) )
  8. ) {
  9. return function spreadFn(argsObj) {
  10. return fn( ...propOrder.map( k => argsObj[k] ) );
  11. };
  12. }

使用方式:

  1. function bar(x,y,z) {
  2. console.log( `x:${x} y:${y} z:${z}` );
  3. }
  4. var f3 = curryProps( spreadArgProps( bar ), 3 );
  5. var f4 = partialProps( spreadArgProps( bar ), { y: 2 } );
  6. f3( {y: 2} )( {x: 1} )( {z: 3} );
  7. // x:1 y:2 z:3
  8. f4( { z: 3, x: 1 } );
  9. // x:1 y:2 z:3

👍!

无形参风格

not取否工具函数

  1. function not(predicate) {
  2. return function negated(...args){
  3. return !predicate( ...args );
  4. };
  5. }
  6. // ES6 箭头函数形式
  7. var not =
  8. predicate =>
  9. (...args) =>
  10. !predicate( ...args );

when条件工具函数

  1. function when(predicate,fn) {
  2. return function conditional(...args){
  3. if (predicate( ...args )) {
  4. return fn( ...args );
  5. }
  6. };
  7. }

综合例子:

  1. function output(msg) {
  2. console.log( msg );
  3. }
  4. function isShortEnough(str) {
  5. return str.length <= 5;
  6. }
  7. var isLongEnough = not( isShortEnough );
  8. var printIf = uncurry( partialRight( when, output ) );
  9. var msg1 = "Hello";
  10. var msg2 = msg1 + " World";
  11. printIf( isShortEnough, msg1 ); // Hello
  12. printIf( isShortEnough, msg2 );
  13. printIf( isLongEnough, msg1 );
  14. printIf( isLongEnough, msg2 ); // Hello World

无形参就是尽量将函数作为一等变量传递,实际参数发生落地的地方尽量往后推延

总结

  • 偏应用是用来减少函数的参数数量 —— 一个函数期望接收的实参数量 —— 的技术,它减少参数数量的方式是创建一个预设了部分实参的新函数。
  • 柯里化是偏应用的一种特殊形式,其参数数量降低为 1,这种形式包含一串连续的链式函数调用,每个调用接收一个实参。当这些链式调用指定了所有实参时,原函数就会拿到收集好的实参并执行。你同样可以将柯里化还原。其它类似 unary(..)identity(..) 以及 constant(..) 的重要函数操作,是函数式编程基础工具库的一部分。
  • 无形参是一种书写代码的风格,这种风格移除了非必需的形参映射实参逻辑,其目的在于提高代码的可读性和可理解性。

第 4 章:组合函数

一个函数式编程者,会将他们程序中的每一个函数当成一小块简单的乐高部件。他们能一眼辨别出蓝色的 2x2 方块,并准确地知道它是如何工作的、能用它做些什么。当构建一个更大、更复杂的乐高模型时,当每一次需要下一块部件的时候,他们能够准确地从备用部件中找到这些部件并拿过来使用。

输出到输入

image.png

wordsUsed <— unique <— words <— text

制造机器

image.png

  1. function compose2(fn2,fn1) {
  2. return function composed(origValue){
  3. return fn2( fn1( origValue ) );
  4. };
  5. }
  6. // ES6 箭头函数形式写法
  7. var compose2 =
  8. (fn2,fn1) =>
  9. origValue =>
  10. fn2( fn1( origValue ) );

组合的变体

**var** letters = compose2( words, unique );

重点是,函数的组合不总是单向的。有时候我们将灰方块放到蓝方块上,有时我们又会将蓝方块放到最上面。
假如糖果工厂尝试将包装好的糖果放入搅拌和冷却巧克力的机器,那他们最好要小心点了。

通用组合

image.png

“自动组合各种小机器,包装成一个更大更好的机器“

  1. function compose(...fns) {
  2. return function composed(result){
  3. // 拷贝一份保存函数的数组
  4. var list = fns.slice();
  5. while (list.length > 0) {
  6. // 将最后一个函数从列表尾部拿出
  7. // 并执行它
  8. result = list.pop()( result );
  9. }
  10. return result;
  11. };
  12. }
  13. // ES6 箭头函数形式写法
  14. var compose =
  15. (...fns) =>
  16. result => {
  17. var list = fns.slice();
  18. while (list.length > 0) {
  19. // 将最后一个函数从列表尾部拿出
  20. // 并执行它
  21. result = list.pop()( result );
  22. }
  23. return result;
  24. };

不同的实现

  • compose的饥渴模式
  1. function compose(...fns) {
  2. return function composed(result){
  3. return fns.reverse().reduce( function reducer(result,fn){
  4. return fn( result );
  5. }, result );
  6. };
  7. }
  8. // ES6 箭头函数形式写法
  9. var compose = (...fns) =>
  10. result =>
  11. fns.reverse().reduce(
  12. (result,fn) =>
  13. fn( result )
  14. , result
  15. );
  • compose的懒惰模式
  1. function compose(...fns) {
  2. return fns.reverse().reduce( function reducer(fn1,fn2){
  3. return function composed(...args){
  4. return fn2( fn1( ...args ) );
  5. };
  6. } );
  7. }
  8. // ES6 箭头函数形式写法
  9. var compose =
  10. (...fns) =>
  11. fns.reverse().reduce( (fn1,fn2) =>
  12. (...args) =>
  13. fn2( fn1( ...args ) )
  14. );