We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
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
函数式编程起源很早,最近随着 React 的热度逐渐被更多人关注,这一章我们来介绍一下。
函数式编程(functional programming)是一种编程范式,它将计算机运算视为函数运算,并且避免使用程序状态以及易变对象。其中,λ 演算(lambda calculus)为该语言最重要的基础。而且,λ 演算的函数可以接受函数当作输入(引数)和输出(传出值)。
比起指令式编程,函数式编程关心数据的映射,命令式编程关心解决问题的步骤。函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。
函数式编程特点:
纯函数 是指对于相同的输入,永远得到相同的输出,而且没有任何可观察的副作用,也不依赖外部环境的状态。
const array = [1, 2, 3, 4, 5]; const a1 = array.slice(0, 2); // array = [1,2,3,4,5] // a1 = [1,2] const a2 = array.slice(0, 2); // array = [1,2,3,4,5] // a2 = [1,2] const b1 = array.splice(0, 2); // array = [3,4,5] // b1 = [1,2] const b2 = array.splice(0, 2); // array = [5] // b2 = [3,4]
可以看到,Array.clice 是纯函数,因为同样的输入,永远得到相同的输出,并且不会影响外部变量(没有副作用)。而 Array.splice 不是纯函数,因为同样的输入,输出并不相同,而且修改了原数组(有副作用)。
柯里化 是指传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
var checkage = min => age => age > min; var checkage20 = checkage(20); checkage20(100); // true
第一步,根据参数 20,返回一个检查年龄是否大于 20 的新函数,第二步,传递参数,检查年龄是否大于 20。
函数组合定义一个组合函数来讲多个函数调用组合成一个,为了解决类似的函数嵌套问题 f(h(j(k()))) 。
f(h(j(k())))
var compose = (f, g) => x => f(g(x)); function add(a) { return a + a; } function multi(a) { return a * a; } const c = compose(add, multi); c(3); // 18
惰性函数 是“比较懒的函数”,只执行一次就不执行了,是因为缓存了上一次的结果,直接拿来用。
var t; function f(a) { if (t) return t; var e = parseInt(a, 10); alert("测试有没有重复!"); e = e * e; t = e; return t; } alert(f("3")); alert(f("3")); alert(f("4")); // 不会弹出16的,因为这是“隋性”,只计算一次
将函数当参数,把传入的函数做一个封装,然后返回这个封装函数,达到更高程度的抽象。
function hoc(fn) { console.log("begin"); const result = fn(); console.log("end"); return result; }
闭包的概念来源于 19 世纪 60 年代,在 1975 年被作为一个语言的编程特征实现,用作支持词法范围的函数是一等公民的函数式编程。
function a(x) { return function(y) { return x + y; }; } var a1 = a(1); a1(3); //4
虽然外部 a 执行完毕,栈上的帧被释放,但是堆上的作用域并不能被释放,因此 x 依旧可以被外部函数访问,这样就形成的闭包。
函数式编程的起源,是一门叫做范畴论(Category Theory)的数学分支。
彼此之间存在某种关系的概念、事物、对象等等,都构成"范畴"。随便什么东西,只要能找出它们之间的关系,就能定义一个"范畴"。
我们可以把"范畴"想象成是一个容器,里面包含两样东西。
class Category { constructor(val) { this.val = val; } addOne(x) { return x + 1; } }
上面代码中,Category 是一个类,也是一个容器,里面包含一个值(this.val)和一种变形关系(addOne)。你可能已经看出来了,这里的范畴,就是所有彼此之间相差 1 的数字。
范畴论使用函数,表达范畴之间的关系。
伴随着范畴论的发展,就发展出一整套函数的运算方法。这套方法起初只用于数学运算,后来有人将它在计算机上实现了,就变成了今天的 函数式编程 。
本质上,函数式编程只是范畴论的运算方法,跟数理逻辑、微积分、行列式是同一类东西,都是数学方法,只是碰巧它能用来写程序。
所以,为什么函数式编程要求函数必须是纯的,不能有副作用?因为它是一种数学运算,原始目的就是求值,不做其他事情,否则就无法满足函数运算法则了。
总之,在函数式编程中,函数就是一个管道(pipe)。这头进去一个值,那头就会出来一个新的值,没有其他作用。
函数不仅可以用于同一个范畴中值的转换,还可以用于将一个范畴转换成另一个范畴。这就涉及到了函子(Functor)。
函子是函数式编程里面最重要的数据类型,也是基本的运算单位和功能单位。
它首先是一种范畴,也就是说,是一个容器,包含了值和变形关系。比较特殊的是,它的变形关系可以依次作用于每一个值,将当前容器变形成另一个容器。
函子的代码实现:
// 任何具有 map 方法的数据结构,都可以当作函子的实现。 class Functor { constructor(val) { this.val = val; } map(f) { return Functor.of(f(this.val)); } } Functor.of = function(value) { return new this(value); };
上面代码中,Functor 是一个函子,它的 map 方法接受函数 f 作为参数,然后返回一个新的函子,里面包含的值是被 f 处理过的 f(this.val)。
一般约定,函子的标志就是容器具有 map 方法。该方法将容器里面的每一个值,映射到另一个容器。
new Functor(2).map(function(two) { return two + 2; }); // Functor(4) new Functor("flamethrowers").map(function(s) { return s.toUpperCase(); }); // Functor('FLAMETHROWERS') new Functor("bombs").map(_.concat(" away")).map(_.prop("length")); // Functor(10)
上面的例子说明,函数式编程里面的运算,都是通过函子完成,即运算不直接针对值,而是针对这个值的容器----函子。函子本身具有对外接口(map 方法),各种函数就是运算符,通过接口接入容器,引发容器里面的值的变形。
因此,学习函数式编程,实际上就是学习函子的各种运算。由于可以把运算方法封装在函子里面,所以又衍生出各种不同类型的函子,有多少种运算,就有多少种函子。函数式编程就变成了运用不同的函子,解决实际问题。
你可能注意到了,上面生成新的函子的时候,用了 new 命令。这实在太不像函数式编程了,因为 new 命令是面向对象编程的标志。
函数式编程一般约定,函子有一个 of 方法,用来生成新的容器。
下面就用 of 方法替换掉 new。
Functor.of = function(val) { return new Functor(val); };
然后,前面的例子就可以改成下面这样。
Functor.of(2).map(function(two) { return two + 2; }); // Functor(4)
这就更像函数式编程了。
函子接受各种函数,处理容器内部的值。这里就有一个问题,容器内部的值可能是一个空值(比如 null),而外部函数未必有处理空值的机制,如果传入空值,很可能就会出错。
Maybe 函子就是为了解决这一类问题而设计的。简单说,它的 map 方法里面设置了空值检查。
class Maybe extends Functor { constructor(value) { super(); this.val = value; } isnothing() { return !!!this.val; } map(f) { if (this.isnothing()) { // 如果没有值,不执行变形函数,直接返回一个新函子 null。 return Maybe.of(null); } else { return Maybe.of(f(this.val)); } } }
条件运算 if...else 是最常见的运算之一,函数式编程里面,使用 Either 函子表达。
Either 函子内部有两个值:左值(Left)和右值(Right)。右值是正常情况下使用的值,左值是右值不存在时使用的默认值。
class Either extends Functor { constructor(value) { super(); this.val = value; } isnothing() { return !!!this.val; } map(left, right) { if (this.isnothing()) { return Either.of(left(null)); } else { return Either.of(right(this.val)); } } }
函子里面包含的值,完全可能是函数。我们可以想象这样一种情况,一个函子的值是数值,另一个函子的值是函数。
function addTwo(x) { return x + 2; } const A = Functor.of(2); const B = Functor.of(addTwo);
上面代码中,函子 A 内部的值是 2,函子 B 内部的值是函数 addTwo。
有时,我们想让函子 B 内部的函数,可以使用函子 A 内部的值进行运算。这时就需要用到 ap 函子。
ap 是 applicative(应用)的缩写。凡是部署了 ap 方法的函子,就是 ap 函子。
class Ap extends Functor { constructor(value) { super(); this.val = value; } ap(F) { return Ap.of(this.val(F.val)); } }
注意,ap 方法的参数不是函数,而是另一个函子。
因此,前面例子可以写成下面的形式。
Ap.of(addTwo).ap(Functor.of(2)); // Ap(4)
ap 函子的意义在于,对于那些多参数的函数,就可以从多个容器之中取值,实现函子的链式操作。
function add(x) { return function(y) { return x + y; }; } Ap.of(add) .ap(Maybe.of(2)) .ap(Maybe.of(3)); // Ap(5)
上面代码中,函数 add 是柯里化以后的形式,一共需要两个参数。通过 ap 函子,我们就可以实现从两个容器之中取值。它还有另外一种写法。
Ap.of(add(2)).ap(Maybe.of(3));
函子是一个容器,可以包含任何值。函子之中再包含一个函子,也是完全合法的。但是,这样就会出现多层嵌套的函子。
Maybe.of(Maybe.of(Maybe.of({ name: "Mulburry", number: 8402 })));
上面这个函子,一共有三个 Maybe 嵌套。如果要取出内部的值,就要连续取三次 this.val。这当然很不方便,因此就出现了 Monad 函子。
Monad 函子的作用是,总是返回一个单层的函子。它有一个 flatMap 方法,与 map 方法作用相同,唯一的区别是如果生成了一个嵌套函子,它会取出后者内部的值,保证返回的永远是一个单层的容器,不会出现嵌套的情况。
class Monad extends Functor { join() { return this.val; } flatMap(f) { return this.map(f).join(); } }
上面代码中,如果函数 f 返回的是一个函子,那么 this.map(f)就会生成一个嵌套的函子。所以,join 方法保证了 flatMap 方法总是返回一个单层的函子。这意味着嵌套的函子会被铺平(flatten)。
Monad 函子的重要应用,就是实现 I/O (输入输出)操作。
I/O 是不纯的操作,普通的函数式编程没法做,这时就需要把 IO 操作写成 Monad 函子,通过它来完成。
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; }); };
上面代码中,读取文件和打印本身都是不纯的操作,但是 readFile 和 print 却是纯函数,因为它们总是返回 IO 函子。
如果 IO 函子是一个 Monad,具有 flatMap 方法,那么我们就可以像下面这样调用这两个函数。
readFile("./user.txt").flatMap(print);
这就是神奇的地方,上面的代码完成了不纯的操作,但是因为 flatMap 返回的还是一个 IO 函子,所以这个表达式是纯的。我们通过一个纯的表达式,完成带有副作用的操作,这就是 Monad 的作用。
由于返回还是 IO 函子,所以可以实现链式操作。因此,在大多数库里面,flatMap 方法被改名成 chain。
var tail = function(x) { return new IO(function() { return x[x.length - 1]; }); }; readFile("./user.txt") .flatMap(tail) .flatMap(print); // 等同于 readFile("./user.txt") .chain(tail) .chain(print);
上面代码读取了文件 user.txt,然后选取最后一行输出。
函数式编程是一个非常大的话题,这里只是简单的列举出了一些案例,希望读者看完之后能有个整体的了解。
函数式编程入门教程
The text was updated successfully, but these errors were encountered:
No branches or pull requests
函数式编程
函数式编程起源很早,最近随着 React 的热度逐渐被更多人关注,这一章我们来介绍一下。
函数式编程定义
函数式编程(functional programming)是一种编程范式,它将计算机运算视为函数运算,并且避免使用程序状态以及易变对象。其中,λ 演算(lambda calculus)为该语言最重要的基础。而且,λ 演算的函数可以接受函数当作输入(引数)和输出(传出值)。
比起指令式编程,函数式编程关心数据的映射,命令式编程关心解决问题的步骤。函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。
函数式编程特点:
函数式编程特性
纯函数
纯函数 是指对于相同的输入,永远得到相同的输出,而且没有任何可观察的副作用,也不依赖外部环境的状态。
可以看到,Array.clice 是纯函数,因为同样的输入,永远得到相同的输出,并且不会影响外部变量(没有副作用)。而 Array.splice 不是纯函数,因为同样的输入,输出并不相同,而且修改了原数组(有副作用)。
柯里化
柯里化 是指传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
第一步,根据参数 20,返回一个检查年龄是否大于 20 的新函数,第二步,传递参数,检查年龄是否大于 20。
函数组合
函数组合定义一个组合函数来讲多个函数调用组合成一个,为了解决类似的函数嵌套问题
f(h(j(k())))
。惰性函数
惰性函数 是“比较懒的函数”,只执行一次就不执行了,是因为缓存了上一次的结果,直接拿来用。
高阶函数
将函数当参数,把传入的函数做一个封装,然后返回这个封装函数,达到更高程度的抽象。
闭包
闭包的概念来源于 19 世纪 60 年代,在 1975 年被作为一个语言的编程特征实现,用作支持词法范围的函数是一等公民的函数式编程。
虽然外部 a 执行完毕,栈上的帧被释放,但是堆上的作用域并不能被释放,因此 x 依旧可以被外部函数访问,这样就形成的闭包。
函数式编程原理
函数式编程的起源,是一门叫做范畴论(Category Theory)的数学分支。
彼此之间存在某种关系的概念、事物、对象等等,都构成"范畴"。随便什么东西,只要能找出它们之间的关系,就能定义一个"范畴"。
我们可以把"范畴"想象成是一个容器,里面包含两样东西。
上面代码中,Category 是一个类,也是一个容器,里面包含一个值(this.val)和一种变形关系(addOne)。你可能已经看出来了,这里的范畴,就是所有彼此之间相差 1 的数字。
范畴论与函数式编程的关系
范畴论使用函数,表达范畴之间的关系。
伴随着范畴论的发展,就发展出一整套函数的运算方法。这套方法起初只用于数学运算,后来有人将它在计算机上实现了,就变成了今天的 函数式编程 。
本质上,函数式编程只是范畴论的运算方法,跟数理逻辑、微积分、行列式是同一类东西,都是数学方法,只是碰巧它能用来写程序。
所以,为什么函数式编程要求函数必须是纯的,不能有副作用?因为它是一种数学运算,原始目的就是求值,不做其他事情,否则就无法满足函数运算法则了。
总之,在函数式编程中,函数就是一个管道(pipe)。这头进去一个值,那头就会出来一个新的值,没有其他作用。
函子
函数不仅可以用于同一个范畴中值的转换,还可以用于将一个范畴转换成另一个范畴。这就涉及到了函子(Functor)。
函子是函数式编程里面最重要的数据类型,也是基本的运算单位和功能单位。
它首先是一种范畴,也就是说,是一个容器,包含了值和变形关系。比较特殊的是,它的变形关系可以依次作用于每一个值,将当前容器变形成另一个容器。
函子的代码实现:
上面代码中,Functor 是一个函子,它的 map 方法接受函数 f 作为参数,然后返回一个新的函子,里面包含的值是被 f 处理过的 f(this.val)。
一般约定,函子的标志就是容器具有 map 方法。该方法将容器里面的每一个值,映射到另一个容器。
上面的例子说明,函数式编程里面的运算,都是通过函子完成,即运算不直接针对值,而是针对这个值的容器----函子。函子本身具有对外接口(map 方法),各种函数就是运算符,通过接口接入容器,引发容器里面的值的变形。
因此,学习函数式编程,实际上就是学习函子的各种运算。由于可以把运算方法封装在函子里面,所以又衍生出各种不同类型的函子,有多少种运算,就有多少种函子。函数式编程就变成了运用不同的函子,解决实际问题。
Of 方法
你可能注意到了,上面生成新的函子的时候,用了 new 命令。这实在太不像函数式编程了,因为 new 命令是面向对象编程的标志。
函数式编程一般约定,函子有一个 of 方法,用来生成新的容器。
下面就用 of 方法替换掉 new。
然后,前面的例子就可以改成下面这样。
这就更像函数式编程了。
Maybe 函子
函子接受各种函数,处理容器内部的值。这里就有一个问题,容器内部的值可能是一个空值(比如 null),而外部函数未必有处理空值的机制,如果传入空值,很可能就会出错。
Maybe 函子就是为了解决这一类问题而设计的。简单说,它的 map 方法里面设置了空值检查。
Either 函子
条件运算 if...else 是最常见的运算之一,函数式编程里面,使用 Either 函子表达。
Either 函子内部有两个值:左值(Left)和右值(Right)。右值是正常情况下使用的值,左值是右值不存在时使用的默认值。
AP 函子
函子里面包含的值,完全可能是函数。我们可以想象这样一种情况,一个函子的值是数值,另一个函子的值是函数。
上面代码中,函子 A 内部的值是 2,函子 B 内部的值是函数 addTwo。
有时,我们想让函子 B 内部的函数,可以使用函子 A 内部的值进行运算。这时就需要用到 ap 函子。
ap 是 applicative(应用)的缩写。凡是部署了 ap 方法的函子,就是 ap 函子。
注意,ap 方法的参数不是函数,而是另一个函子。
因此,前面例子可以写成下面的形式。
ap 函子的意义在于,对于那些多参数的函数,就可以从多个容器之中取值,实现函子的链式操作。
上面代码中,函数 add 是柯里化以后的形式,一共需要两个参数。通过 ap 函子,我们就可以实现从两个容器之中取值。它还有另外一种写法。
Monad 函子
函子是一个容器,可以包含任何值。函子之中再包含一个函子,也是完全合法的。但是,这样就会出现多层嵌套的函子。
上面这个函子,一共有三个 Maybe 嵌套。如果要取出内部的值,就要连续取三次 this.val。这当然很不方便,因此就出现了 Monad 函子。
Monad 函子的作用是,总是返回一个单层的函子。它有一个 flatMap 方法,与 map 方法作用相同,唯一的区别是如果生成了一个嵌套函子,它会取出后者内部的值,保证返回的永远是一个单层的容器,不会出现嵌套的情况。
上面代码中,如果函数 f 返回的是一个函子,那么 this.map(f)就会生成一个嵌套的函子。所以,join 方法保证了 flatMap 方法总是返回一个单层的函子。这意味着嵌套的函子会被铺平(flatten)。
IO 函子
Monad 函子的重要应用,就是实现 I/O (输入输出)操作。
I/O 是不纯的操作,普通的函数式编程没法做,这时就需要把 IO 操作写成 Monad 函子,通过它来完成。
上面代码中,读取文件和打印本身都是不纯的操作,但是 readFile 和 print 却是纯函数,因为它们总是返回 IO 函子。
如果 IO 函子是一个 Monad,具有 flatMap 方法,那么我们就可以像下面这样调用这两个函数。
这就是神奇的地方,上面的代码完成了不纯的操作,但是因为 flatMap 返回的还是一个 IO 函子,所以这个表达式是纯的。我们通过一个纯的表达式,完成带有副作用的操作,这就是 Monad 的作用。
由于返回还是 IO 函子,所以可以实现链式操作。因此,在大多数库里面,flatMap 方法被改名成 chain。
上面代码读取了文件 user.txt,然后选取最后一行输出。
常用的函数式编程的库
小结
函数式编程是一个非常大的话题,这里只是简单的列举出了一些案例,希望读者看完之后能有个整体的了解。
参考链接
函数式编程入门教程
The text was updated successfully, but these errors were encountered: