你可能听说过函数式编程(Functional programming),甚至已经使用了一段时间。
但是,你能说清楚,它到底是什么吗?
网上搜索一下,你会轻松找到好多答案。
- 与面向对象编程(Object-oriented programming)和过程式编程(Procedural programming)并列的编程范式。
- 最主要的特征是,函数是第一等公民。
- 强调将计算过程分解成可复用的函数,典型例子就是
map
方法和reduce
方法组合而成 MapReduce 算法。- 只有纯的、没有副作用的函数,才是合格的函数。
上面这些说法都对,但还不够,都没有回答下面这个更深层的问题。
为什么要这样做?
这就是,本文要解答的问题。我会通过最简单的语言,帮你理解函数式编程,并且学会它那些基本写法。
需要声明的是,我不是专家,而是一个初学者,最近两年才真正开始学习函数式编程。一直苦于看不懂各种资料,立志要写一篇清晰易懂的教程。下面的内容肯定不够严密,甚至可能包含错误,但是我发现,像下面这样解释,初学者最容易懂。
另外,本文比较长,阅读时请保持耐心。结尾还有 Udacity 的《前端工程师认证课程》的推广,非常感谢他们对本文的赞助。
一、范畴论
函数式编程的起源,是一门叫做范畴论(Category Theory)的数学分支。
理解函数式编程的关键,就是理解范畴论。它是一门很复杂的数学,认为世界上所有的概念体系,都可以抽象成一个个的"范畴"(category)。
1.1 范畴的概念
什么是范畴呢?
维基百科的一句话定义如下。
"范畴就是使用箭头连接的物体。"(In mathematics, a category is an algebraic structure that comprises "objects" that are linked by "arrows". )
也就是说,彼此之间存在某种关系的概念、事物、对象等等,都构成"范畴"。随便什么东西,只要能找出它们之间的关系,就能定义一个"范畴"。
上图中,各个点与它们之间的箭头,就构成一个范畴。
箭头表示范畴成员之间的关系,正式的名称叫做"态射"(morphism)。范畴论认为,同一个范畴的所有成员,就是不同状态的"变形"(transformation)。通过"态射",一个成员可以变形成另一个成员。
1.2 数学模型
既然"范畴"是满足某种变形关系的所有对象,就可以总结出它的数学模型。
- 所有成员是一个集合
- 变形关系是函数
也就是说,范畴论是集合论更上层的抽象,简单的理解就是"集合 + 函数"。
理论上通过函数,就可以从范畴的一个成员,算出其他所有成员。
1.3 范畴与容器
我们可以把"范畴"想象成是一个容器,里面包含两样东西。
- 值(value)
- 值的变形关系,也就是函数。
下面我们使用代码,定义一个简单的范畴。
class Category { constructor(val) { this.val = val; } addOne(x) { return x + 1; } }
上面代码中,Category
是一个类,也是一个容器,里面包含一个值(this.val
)和一种变形关系(addOne
)。你可能已经看出来了,这里的范畴,就是所有彼此之间相差1
的数字。
注意,本文后面的部分,凡是提到"容器"的地方,全部都是指"范畴"。
1.4 范畴论与函数式编程的关系
范畴论使用函数,表达范畴之间的关系。
伴随着范畴论的发展,就发展出一整套函数的运算方法。这套方法起初只用于数学运算,后来有人将它在计算机上实现了,就变成了今天的"函数式编程"。
本质上,函数式编程只是范畴论的运算方法,跟数理逻辑、微积分、行列式是同一类东西,都是数学方法,只是碰巧它能用来写程序。
所以,你明白了吗,为什么函数式编程要求函数必须是纯的,不能有副作用?因为它是一种数学运算,原始目的就是求值,不做其他事情,否则就无法满足函数运算法则了。
总之,在函数式编程中,函数就是一个管道(pipe)。这头进去一个值,那头就会出来一个新的值,没有其他作用。
二、函数的合成与柯里化
函数式编程有两个最基本的运算:合成和柯里化。
2.1 函数的合成
如果一个值要经过多个函数,才能变成另外一个值,就可以把所有中间步骤合并成一个函数,这叫做"函数的合成"(compose)。
上图中,X
和Y
之间的变形关系是函数f
,Y
和Z
之间的变形关系是函数g
,那么X
和Z
之间的关系,就是g
和f
的合成函数g·f
。
下面就是代码实现了,我使用的是 JavaScript 语言。注意,本文所有示例代码都是简化过的,完整的 Demo 请看《参考链接》部分。
合成两个函数的简单代码如下。
const compose = function (f, g) { return function (x) { return f(g(x)); }; }
函数的合成还必须满足结合律。
compose(f, compose(g, h)) // 等同于 compose(compose(f, g), h) // 等同于 compose(f, g, h)
合成也是函数必须是纯的一个原因。因为一个不纯的函数,怎么跟其他函数合成?怎么保证各种合成以后,它会达到预期的行为?
前面说过,函数就像数据的管道(pipe)。那么,函数合成就是将这些管道连了起来,让数据一口气从多个管道中穿过。
2.2 柯里化
f(x)
和g(x)
合成为f(g(x))
,有一个隐藏的前提,就是f
和g
都只能接受一个参数。如果可以接受多个参数,比如f(x, y)
和g(a, b, c)
,函数合成就非常麻烦。
这时就需要函数柯里化了。所谓"柯里化",就是把一个多参数的函数,转化为单参数函数。
// 柯里化之前 function add(x, y) { return x + y; } add(1, 2) // 3 // 柯里化之后 function addX(y) { return function (x) { return x + y; }; } addX(2)(1) // 3
有了柯里化以后,我们就能做到,所有函数只接受一个参数。后文的内容除非另有说明,都默认函数只有一个参数,就是所要处理的那个值。
三、函子
函数不仅可以用于同一个范畴之中值的转换,还可以用于将一个范畴转成另一个范畴。这就涉及到了函子(Functor)。
3.1 函子的概念
函子是函数式编程里面最重要的数据类型,也是基本的运算单位和功能单位。
它首先是一种范畴,也就是说,是一个容器,包含了值和变形关系。比较特殊的是,它的变形关系可以依次作用于每一个值,将当前容器变形成另一个容器。
上图中,左侧的圆圈就是一个函子,表示人名的范畴。外部传入函数f
,会转成右边表示早餐的范畴。
下面是一张更一般的图。
上图中,函数f
完成值的转换(a
到b
),将它传入函子,就可以实现范畴的转换(Fa
到Fb
)。
3.2 函子的代码实现
任何具有map
方法的数据结构,都可以当作函子的实现。
class Functor { constructor(val) { this.val = val; } map(f) { return new Functor(f(this.val)); } }
上面代码中,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
方法),各种函数就是运算符,通过接口接入容器,引发容器里面的值的变形。
因此,学习函数式编程,实际上就是学习函子的各种运算。由于可以把运算方法封装在函子里面,所以又衍生出各种不同类型的函子,有多少种运算,就有多少种函子。函数式编程就变成了运用不同的函子,解决实际问题。
四、of 方法
你可能注意到了,上面生成新的函子的时候,用了new
命令。这实在太不像函数式编程了,因为new
命令是面向对象编程的标志。
函数式编程一般约定,函子有一个of
方法,用来生成新的容器。
下面就用of
方法替换掉new
。
Functor.of = function(val) { return new Functor(val); };
然后,前面的例子就可以改成下面这样。
Functor.of(2).map(function (two) { return two + 2; }); // Functor(4)
这就更像函数式编程了。
五、Maybe 函子
函子接受各种函数,处理容器内部的值。这里就有一个问题,容器内部的值可能是一个空值(比如null
),而外部函数未必有处理空值的机制,如果传入空值,很可能就会出错。
Functor.of(null).map(function (s) { return s.toUpperCase(); }); // TypeError
上面代码中,函子里面的值是null
,结果小写变成大写的时候就出错了。
Maybe 函子就是为了解决这一类问题而设计的。简单说,它的map
方法里面设置了空值检查。
class Maybe extends Functor { map(f) { return this.val ? Maybe.of(f(this.val)) : Maybe.of(null); } }
有了 Maybe 函子,处理空值就不会出错了。
Maybe.of(null).map(function (s) { return s.toUpperCase(); }); // Maybe(null)
六、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, null);
上面代码中,如果右值有值,就使用右值,否则使用左值。通过这种方式,Either 函子表达了条件运算。
Either 函子的常见用途是提供默认值。下面是一个例子。
Either .of({address: 'xxx'}, currentUser.address) .map(updateField);
上面代码中,如果用户没有提供地址,Either 函子就会使用左值的默认地址。
Either 函子的另一个用途是代替try...catch
,使用左值表示错误。
function parseJSON(json) { try { return Either.of(null, JSON.parse(json)); } catch (e: Error) { return Either.of(e, null); } }
上面代码中,左值为空,就表示没有出错,否则左值会包含一个错误对象e
。一般来说,所有可能出错的运算,都可以返回一个 Either 函子。
七、ap 函子
函子里面包含的值,完全可能是函数。我们可以想象这样一种情况,一个函子的值是数值,另一个函子的值是函数。
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 { 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));
八、Monad 函子
函子是一个容器,可以包含任何值。函子之中再包含一个函子,也是完全合法的。但是,这样就会出现多层嵌套的函子。
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)。
九、IO 操作
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
,然后选取最后一行输出。
十、参考链接
- JS 函数式编程指南
- Taking Things Out of Context: Functors in JavaScript
- Functor.js
- Maybe, Either & Try Functors in ES6
- Why Category Theory Matters
(正文完)
============================
感谢你读完了全文。下面还有一个推广,请再花一分钟阅读。
去年十月,我介绍了来自硅谷的技术学习平台优达学城(Udacity),他们推出的纳米学位。
现在,他们进入中国市场快满周年了,又有一个本地化课程发布了。那就是由 Google 和 Github 合作制作的"前端开发工程师"认证课程。
这个课程完全是国际水准,讲解深入浅出,示例丰富,贴近大公司开发实践,帮助你牢牢掌握那些最实用的前端技术。
课程由硅谷工程师英语讲授,配有全套中文字幕,以及全中文的学习辅导,还有首次引入中国的同步学习小组和导师监督服务,包含一对一的代码辅导。课程通过后,还能拿到 Google、Github 参与颁发的学习认证。
这门课程今天(2月22日)就开始报名了,现在就点击这里,了解更多。我的读者报名时,请使用优惠码ruanyfFEND
。
最后,欢迎立即扫码,关注优达学城(微信号:youdaxue),跟踪最新的 IT 在线学习和培训资讯。
(完)
choukin 说:
柯里化 部分
add(2)(1) // 3
这个是不是应该是
addX(2)(1) // 3
2017年2月22日 09:53 | # | 引用
yard 说:
2.2函数柯里化里代码片段 函数柯里化之后的执行应该是addX(2)(1);
2017年2月22日 09:53 | # | 引用
Parker Liu 说:
楼主既然是以范畴论来讲函数式编程的,那就最好用Haskell语言来介绍吧,这样要简洁多了。另外,范畴论请贴完整的定义,一句话是无法说清楚范畴论的,现有这个解释非常容易误导人。文中对范畴中的态射和范畴间的函子的解释是错误的,会对初学者的后续学习引上歧路,对后续自然变换、伴随函子、极限与余极限等概念的理解造成障碍。
2017年2月22日 09:55 | # | 引用
阮一峰 说:
@choukin,@yard:
谢谢指出,已经改正。
@Parker Liu:
范畴论的正式定义太复杂了,这里只是最简化的定义。如果读者要完整了解范畴论,肯定不能依靠这个定义。
Haskell 语言我还没掌握,能基本学会了,我会再写一篇 Haskell 版本的函数式编程。
2017年2月22日 10:49 | # | 引用
Darcy Xu 说:
这个udacity的前端教程可以自己安排时间的吧,不用等6个月学完吧?谢谢!
2017年2月22日 11:41 | # | 引用
leselie 说:
用了一段时间的scala了,确实还不懂函数式编程,这个文章看了眼前一亮,但是有似懂非懂。
2017年2月22日 11:53 | # | 引用
Parker Liu 说:
@阮一峰:
你这个简化定义把基本的要素给丢了,就不能表达范畴论的意义了。既然引用了,最好还是给出完整的介绍比较好。
下面是wikipedia上范畴论的简要介绍:
Category theory formalizes mathematical structure and its concepts in terms of a collection of objects and of arrows (also called morphisms). A category has two basic properties: the ability to compose the arrows associatively and the existence of an identity arrow for each object.
另外,你这句话“通过"态射",一个成员可以变形成另一个成员”是错误的,非常容易误导初学者。范畴中的态射恰恰是保持了范畴中对象的某些不变的性质,是保持某些性质不变的变换。
同样的,函子也具有类似的不变性,因此具有如下非常重要的定律,而这是你没有提到的。
F id = id
F (f . g) = F f . F g
你在文中提到函子是一种范畴,是一个容器,这是完全错误的。函子只是Cat范畴中的态射,即Cat范畴(其对象是范畴)中的范畴之间的态射。从C++来看,可以大致将Option的Option理解为函子。函子可以看成是类型构造子,但必须满足上面提到的函子的定律。
另外Either并不是一个函子,其有两个类型变量。
2017年2月22日 13:03 | # | 引用
anonymous 说:
"箭头表示范畴成员之间的关系,正式的名称叫做"态射"(morphism)。范畴论认为,同一个范畴的所有成员,就是不同状态的"变形"(transformation)。通过"态射",一个成员可以变形成另一个成员。"开头就有问题了. 范畴论的基础是不关心成员, 只考虑整体.
2017年2月22日 13:29 | # | 引用
viola 说:
能有个demo 就好了, 我用ts 写一遍 各种问题..
2017年2月22日 15:42 | # | 引用
jackpowell 说:
Java8的Lambda编程熟悉了,把你文章一看,理论知识更加丰富一层,赞!
2017年2月22日 17:52 | # | 引用
堂号清靖 说:
网上看到一种理解,我觉得更形象,符合中国人的思维。
面向对象编程的两顶帽子
其实就是理解了面向对象的根本,定义和实现的两个面,通过接口关联了起来。世界都是通过这种方式来分类呈现的。所谓易经的阴和阳,阴阳转化不过如此。
今天领悟到的,就是易经里的那个不易,不变,就是函数式,函数的不变性,一致性,函数作为描述抽象及原理的,作为第一类的函数first function,就是终极。
所有的变化,最后都通过函数串了起来。而变化的后面,就是不变,以不变应万变。函数就是相当于太极,无级就图灵机,Lambda,太极就是函数。
易有太极,始生两仪,两仪生四象,四象生八卦。
函数产生了定义及调用。又产生了参数和返回值。最后组成了对象的定义和实现,然后派生了整个计算机世界。
来源
http://www.cnblogs.com/DSharp/p/3789545.html
2017年2月22日 18:38 | # | 引用
郭勃生 说:
柯里化那里可以这样写吗?
// 柯里化之前
function add(x, y) {
return x + y;
}
add(1, 2) // 3
// 柯里化之后
function add(y) {
return function (x) {
return x + y;
};
}
add(2)(1) // 3
2017年2月22日 22:35 | # | 引用
Steve 说:
建议用es6的arrow function会让代码更简洁
2017年2月23日 00:54 | # | 引用
龙傲天 说:
AP和Curry和像啊
2017年2月23日 08:33 | # | 引用
聪明的剑圣 说:
@阮一峰老师:
2.1节函数的合成列子,根据上面的图我总感觉应该是如下:
const compose = function (f, g) {
return function (x) {
return g(f(x));
};
}
还望指教!
2017年2月23日 11:02 | # | 引用
攻城狮狮狮 说:
学习前端中,很多问题都是在阮大大文章帮助下解决的,特来道谢。
2017年2月23日 19:34 | # | 引用
yybbb 说:
Maybe.of(null) 还是找不到toUpperCase() 继承没有使用重写的方法....
> node
> class Functor {
... constructor(val) {
..... this.val = val;
..... }
...
... map(f) {
... return new Functor(f(this.val));
... }
... }
[Function: Functor]
> Functor.of = function(val) {
... return new Functor(val);
... };
[Function]
> class Maybe extends Functor {
... map(f) {
..... return this.val ? Maybe.of(f(this.val)) : Maybe.of(null);
..... }
... }
[Function: Maybe]
> Maybe.of(null).map(function (s) {
... return s.toUpperCase();
... });
TypeError: Cannot read property 'toUpperCase' of null
at repl:2:9
at Functor.map (repl:7:20)
at repl:1:16
at ContextifyScript.Script.runInThisContext (vm.js:23:33)
at REPLServer.defaultEval (repl.js:334:29)
at bound (domain.js:280:14)
at REPLServer.runBound [as eval] (domain.js:293:12)
at REPLServer.onLine (repl.js:531:10)
at emitOne (events.js:101:20)
at REPLServer.emit (events.js:189:7)
2017年2月23日 22:19 | # | 引用
jose 说:
先mark一下,最近再看别的东西,看完别的东西,立马看这篇
2017年2月23日 22:30 | # | 引用
刘昴星 说:
2.1中为什么不是 return g(f(x));而是 return f(g(x))?
2017年2月23日 22:46 | # | 引用
动感小前端 说:
@Parker Liu 对初学者用Haskell做演示真的好么= =
2017年2月24日 09:48 | # | 引用
lemonleo 说:
阮老师好,有一点比较疑惑的地方求解答:
我注意到文中的每一个函子都是继承了原始的Functor实现,然后每当有一个新的函子时这个函数的调用就会改变。
比如: 最原始的调用方法是 Functor.of(...) 当有了a函子就变成a.of(...),有了b函子就变成b.of(...),可这与我传统的编程方式好像不太一样呀,如果按着我传统的思维,应该会像这样: Functor.of(a).a(...) Functor.of(b).b(...)
这一块该怎么理解呢? 谢谢阮老师解答~
2017年2月24日 10:59 | # | 引用
函数编程挺好玩 说:
readFile('./user.txt')
.flatMap(print)
var readFile = function(filename) {
return new IO(function() {
return fs.readFileSync(filename, 'utf-8');
});
};
readFile的返回值是一个IO的函子,这个函子的val是那个读取文件的函数(function() { return fs.readFileSync(filename, 'utf-8');})再调用flatMap的时候,是不是把这个函数作为参数传给print了?
2017年2月24日 15:18 | # | 引用
prettykernel 说:
文中第七节有一处Functor误作Function,应是笔误,望纠正。
2017年2月24日 15:29 | # | 引用
cshenger 说:
开个脑洞,数组有map但它是对象,所以它可以算是函子吗?
2017年2月24日 17:41 | # | 引用
steam 说:
“2.1 函数的合成” 中,下面这段代码是不是错了?
const compose = function (f, g) {
return function (x) {
return f(g(x));
};
}
应该是下面这样?g 函数是不是应该在 f 函数外边?
const compose = function (f, g) {
return function (x) {
return g(f(x));
};
}
2017年2月24日 17:49 | # | 引用
阮一峰 说:
@prettykernel:谢谢指出,已改正。
@cshenger:是的, Array 是函子。
@steam:两种写法都可以,函数式编程的惯例,似乎是先运行右边参数。
2017年2月24日 23:02 | # | 引用
Parker Liu 说:
就本文中介绍的知识来看,用到的Haskell的最基础的东西,这时Haskell的语法是最简洁的,是比较容易理解的。至于monad,会用即可,概念的理解可以先跳过去。实际上本文也没有告诉你monad究竟是什么。
另外,柯里化用下面这种方式来写更符合函数式编程的风格:
var curry = function (f) {
return function(x) {
return function(y) {
return f(x, y);
};
};
}
2017年2月25日 11:30 | # | 引用
暗夜 说:
暂时看不懂,想知道函数式编程有什么优点,除了数学研究。。
2017年2月26日 16:13 | # | 引用
dcxy0 说:
感觉讲的有点深奥,慢慢了解吧
2017年2月27日 16:07 | # | 引用
Jackslow 说:
从文中例子看完全没看出函数式编程的好处来……
这个例子
readFile('./user.txt')
.flatMap(tail)
.flatMap(print)
直接写成print(tail(readFile('./user.txt')))难道不是更好懂?
要说链式调用,面向对象也可以轻易实现啊。。
2017年2月28日 09:18 | # | 引用
ipc 说:
刚刚开始scala;文章很好,多谢阮先生。
2017年2月28日 10:33 | # | 引用
Jimgo 说:
@yybbb:
我在Chrome56上试也是这样子。查看调用栈,Maybe.map调用的是Functor.map,而不是Maybe在class里面声明的map
估计是 Maybe.of(null) 返回的是 Functor 而不是 Maybe
2017年2月28日 20:12 | # | 引用
kyle 说:
请问这行代码能正确执行?
`(new Functor('bombs')).map(_.concat(' away')).map(_.prop('length'));`
- 我的环境下执行顺序是,
1.require('lodash')
2.执行代码
运行结果是:
```
return new Functor(f(this.val));
^
TypeError: f is not a function
```
2017年2月28日 23:14 | # | 引用
Jimgo 说:
@yybbb:
class Functor {
constructor(val) {
this.val = val;
}
static of(val){
return new this(val)
}
map(f) {
return new Functor(f(this.val));
}
}
测试这样写能解决问题
2017年3月 1日 23:40 | # | 引用
Alvin Qiu 说:
恩作者写错了,f(x) -> Y, g(y) -> Z 则 gof(x) = g(f(x))
2017年3月 3日 10:00 | # | 引用
sdlfjsalk 说:
具体有什么用?目前看来除了装X还有什么用?简化代码?让人看不懂?如何维护呢?真心求解
2017年3月 3日 15:51 | # | 引用
Kyoloro 说:
Either 函子这块
Either extends 了 Functor
如果 Functor 是跟上文一样的东西
那么 Either 的 constructor里 应该先执行super() 把,不然constructor的 this.left this 会报未定义错误的。
2017年3月 4日 23:45 | # | 引用
ablexie 说:
感受了一番函数式编程的奥妙,3Q
2017年3月 8日 15:39 | # | 引用
jisen.zhong 说:
我做个简单的类比,chroma电源测试系统里,他们的软件如是高度封装的指令,每一条指令写上参数就能实现一个动作,比如一个电源的动作过程,先设置电压-->频率-->输出,分别对应3条指令,SetINSRC_Vout,SetINSRC_Frequency,SetINSRC_OutputState,每条指令填写相关的电压频率和状态参数,就能实现这个电源动作过程。函数式编程我理解就是把一个通用事件的步骤都封装在一条指令里,一条指令就一个动作,根据作者的范畴论,只要沿着箭头的步骤添加相关指令就能实现这个范畴的功能,这样其实是大大简化了编程的难度
2017年3月13日 16:36 | # | 引用
leewi9 说:
阮老师,不知道你会不会看到这条留言,你的有些文章很长,我建议在页面加上一个目录导航,这样便于阅读。
2017年3月13日 21:31 | # | 引用
大头 说:
道理我都懂,就是有点懵。还是喜欢面向对象,好理解:)
2017年3月15日 16:02 | # | 引用
JuAn 说:
谢谢您。谢谢您做出的贡献
2017年4月27日 10:28 | # | 引用
farheart 说:
范畴的wiki定义翻译的似乎有欠缺,就字面意思而言,是不是应该翻译成:"范畴就是一种以箭头连接的物体所构成的代数结构。"更合适一些呢?
2017年4月28日 09:07 | # | 引用
爱挑错的人 说:
transformation这个是“变换”的意思,线性代数里面我们就学过,线性变换linear transformation。
2017年5月 3日 11:41 | # | 引用
shinefine 说:
Either 函子定义错了,左值作为默认值,无论传递什么函数给它,它自身都不会改变。
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(this.left, this.right); //因为不做任何操作,这里也可以直接返回this
}
}
Either.of = function (left, right) {
return new Either(left, right);
};
Either用法 示例:
Either.of("not valid number”, 6).map(addOne);
// Either("not valid number”, 7);
Either.of("not valid number”, null).map(addOne);
// Either("not valid number”, null); //右值无效的情况下左值作为默认值始终不变
2017年5月26日 10:34 | # | 引用
sheng 说:
@shinefine 说:
人家有人家的Either定义,你自己把定义改了而已,这就能说别人错了?
2017年6月17日 08:56 | # | 引用
wanghuai 说:
class Ap extends Functor {
ap(F) {
return Ap.of(this.val(F.val));
}
}
这个ap函数的实现是不是应该写成
class Ap extends Functor {
ap(F) {
return Ap.of(F.map(this.val));
}
}
如果按照上面的写法
Ap.of(add).ap(Maybe.of(null)).ap(Maybe.of(null));
会报错吧
不知道博主还能否看到这条评论
2017年8月23日 16:08 | # | 引用
shinefine 说:
@sheng
下面这个函数是我对 IsOdd()的定义:
function IsOdd(n){
return n%2 ==0
}
照你的逻辑,你不能说我对Odd 定义错了
2017年9月 1日 15:17 | # | 引用
Imcodingcc 说:
倒数第二个例子, readFile('./readme.txt').flatMap(print)
这句话并不会真正的去输出文件信息, 而是要将传入IO函子的函数执行了才会,也就是在function定义后面加上()
但是Monad这个类型函子真正用意从上面的文章中我看懂了。
下面这部分是我写的
var fs = require('fs');
var {IO} = require('./IO.js')
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;
}()//这个括号这里是改动的部分);
}
var a = readFile('webpack.config.js')
.flatMap(print)
这样最终是把文件内容输出了, 而且a这个变量是指向一个IO函子的, 也可以继续写chain调用,
但是作者你的用意不是要将文件内容打印在控制台上吗, 你文章里那样写是做不到的呀, 只能产生链试调用吧。
2017年9月24日 12:39 | # | 引用
XX 说:
还是看的有点懵……
2017年9月26日 10:44 | # | 引用
WW 说:
看完还是不明白,为什么要用函数式编程,应用场景中到底遇到了什么问题会决定去用函数式编程
2017年10月21日 10:56 | # | 引用
qinky 说:
看到2.2有个疑惑,什么是纯的,什么是不纯的,完全没有给出解释
2017年11月10日 17:04 | # | 引用
HYN 说:
"函子只是Cat范畴中的态射,即Cat范畴(其对象是范畴)中的范畴之间的态射。" 谢谢指正,
读到"函子是一种范畴,是一个容器"就感觉有问题,直接略过到下面来看评论,发现果然写的有问题。
2017年12月12日 17:42 | # | 引用
Andy 说:
三、函子 以后的部分没看明白,是函数式编程的理论规定还是什么?
2018年1月 7日 22:17 | # | 引用
象道 说:
最好在文中写明IO单子的定义,然后把文中“new IO”改为“IO.of”。
2018年1月21日 03:45 | # | 引用
可嘉 说:
IO单子示例部分“print”定义中“console.log(x)”似应为“console.log(x())”。
2018年1月21日 04:19 | # | 引用
象道 说:
关于monad的作用论述,似乎应着重于monad.chain与applicative.ap的不同:前者总是返回源函子的、无变化的核,而后者返回值则不保证源函子核的不变性。
2018年1月21日 21:45 | # | 引用
殷敏峰 说:
2018年1月31日 10:56 | # | 引用
liuxuan 说:
阮老师,在那个组合f和g的例子中,按照您画的图,应该是return g(f(x)),因为是先执行f
2018年3月22日 10:28 | # | 引用
悬崖上的宗介 说:
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();
});
2018年5月30日 10:43 | # | 引用
悬崖上的宗介 说:
已经看到解决方法了~
2018年5月30日 10:57 | # | 引用
keshin 说:
似懂非懂的。mark一下。补充知识再看看
2018年7月20日 11:05 | # | 引用
channg 说:
看第一遍的时候,不要看实现过程,就看使用就行了。
2018年7月20日 16:34 | # | 引用
张顺 说:
没有调用到函子里面的of方法啊,我是在最新的chrome控制台运行的
2018年9月14日 10:43 | # | 引用
Jason zeng 说:
阮老师,我下面的例子函数是不可以交换的,是不是我的函数不满足交换律,不能函数式
const compose = function (f, g) {
return function (x) {
return f(g(x));
};
}
const add = function (c) {
return c+1;
}
const mult = function(t) {
return t * 2;
}
const comp1 = compose(add,mult)
const comp2 = compose(mult,add)
2018年10月25日 11:18 | # | 引用
黄紫琪 说:
阮老师小哥哥的文章,太好了,打开了我函数式编程认知的大门,让我受益匪浅。
阮老师小哥哥有空私下传授以下我吗?
2018年10月31日 09:57 | # | 引用
疯狂的骑士 说:
以前看了很多文章,都似懂非懂的,这篇让我茅塞顿开,非常感谢阮老师
2018年11月20日 23:55 | # | 引用
瓦蕾 说:
@yybbb:
这里是因为Functor.of函数中实例化的是一个Functor对象,所以不会调用Maybe的map方法,而是调用Functor的map,所以会报错。
2019年5月29日 11:45 | # | 引用
fyh 说:
第二次看 似乎看懂了些了
2019年9月 4日 15:03 | # | 引用
cccc 说:
IO IO IO IO 的代码呢???留一半不写全?????
show me the code
2020年4月13日 22:16 | # | 引用
Jerry 说:
前面看着还行,往后就开始一头雾水 ~
2021年6月15日 10:12 | # | 引用
神乐 说:
我算是发现了,每一篇博文都需要看几遍才能吸收。写的很精彩
2021年7月30日 10:50 | # | 引用
Evan 说:
当年看了一头雾水, 时隔多年再次阅读, 竟然看明白了.
2021年8月27日 15:18 | # | 引用
L. 说:
这篇文章有很多问题评论也有提到一些,建议直接看参考连接。
推荐第一个连接,最好带着思考读,有什么疑问去翻翻Github里的Issues也会有收获。
2021年8月31日 22:13 | # | 引用
黄其泽 说:
呵呵。。什么范畴论,无非是扯虎皮拉大旗。
2021年10月26日 23:18 | # | 引用
zhishiluguo 说:
理论上通过函数,就可以从范畴的一个成员,算出其他所有成员。
这个说法在数学意义上显然是错误的。一个简单的反例是离散范畴,所有 object 都只有到自己的恒等态射。容易验证它是满足范畴的定义的。
2024年1月15日 23:03 | # | 引用