函数式编程入门教程

作者: 阮一峰

日期: 2017年2月22日

你可能听说过函数式编程(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)。

上图中,XY之间的变形关系是函数fYZ之间的变形关系是函数g,那么XZ之间的关系,就是gf的合成函数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)),有一个隐藏的前提,就是fg都只能接受一个参数。如果可以接受多个参数,比如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完成值的转换(ab),将它传入函子,就可以实现范畴的转换(FaFb)。

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;
  });
}

上面代码中,读取文件和打印本身都是不纯的操作,但是readFileprint却是纯函数,因为它们总是返回 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,然后选取最后一行输出。

十、参考链接

(正文完)

============================

感谢你读完了全文。下面还有一个推广,请再花一分钟阅读。

去年十月,我介绍了来自硅谷的技术学习平台优达学城(Udacity),他们推出的纳米学位

现在,他们进入中国市场快满周年了,又有一个本地化课程发布了。那就是由 Google 和 Github 合作制作的"前端开发工程师"认证课程。

这个课程完全是国际水准,讲解深入浅出,示例丰富,贴近大公司开发实践,帮助你牢牢掌握那些最实用的前端技术。

课程由硅谷工程师英语讲授,配有全套中文字幕,以及全中文的学习辅导,还有首次引入中国的同步学习小组和导师监督服务,包含一对一的代码辅导。课程通过后,还能拿到 Google、Github 参与颁发的学习认证。

这门课程今天(2月22日)就开始报名了,现在就点击这里,了解更多。我的读者报名时,请使用优惠码ruanyfFEND

最后,欢迎立即扫码,关注优达学城(微信号:youdaxue),跟踪最新的 IT 在线学习和培训资讯。

(完)

留言(76条)

柯里化 部分
add(2)(1) // 3

这个是不是应该是

addX(2)(1) // 3

2.2函数柯里化里代码片段 函数柯里化之后的执行应该是addX(2)(1);

楼主既然是以范畴论来讲函数式编程的,那就最好用Haskell语言来介绍吧,这样要简洁多了。另外,范畴论请贴完整的定义,一句话是无法说清楚范畴论的,现有这个解释非常容易误导人。文中对范畴中的态射和范畴间的函子的解释是错误的,会对初学者的后续学习引上歧路,对后续自然变换、伴随函子、极限与余极限等概念的理解造成障碍。

@choukin,@yard:

谢谢指出,已经改正。

@Parker Liu:

范畴论的正式定义太复杂了,这里只是最简化的定义。如果读者要完整了解范畴论,肯定不能依靠这个定义。

Haskell 语言我还没掌握,能基本学会了,我会再写一篇 Haskell 版本的函数式编程。

这个udacity的前端教程可以自己安排时间的吧,不用等6个月学完吧?谢谢!

用了一段时间的scala了,确实还不懂函数式编程,这个文章看了眼前一亮,但是有似懂非懂。

@阮一峰:
你这个简化定义把基本的要素给丢了,就不能表达范畴论的意义了。既然引用了,最好还是给出完整的介绍比较好。
下面是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并不是一个函子,其有两个类型变量。

"箭头表示范畴成员之间的关系,正式的名称叫做"态射"(morphism)。范畴论认为,同一个范畴的所有成员,就是不同状态的"变形"(transformation)。通过"态射",一个成员可以变形成另一个成员。"开头就有问题了. 范畴论的基础是不关心成员, 只考虑整体.

能有个demo 就好了, 我用ts 写一遍 各种问题..

Java8的Lambda编程熟悉了,把你文章一看,理论知识更加丰富一层,赞!

网上看到一种理解,我觉得更形象,符合中国人的思维。

面向对象编程的两顶帽子

其实就是理解了面向对象的根本,定义和实现的两个面,通过接口关联了起来。世界都是通过这种方式来分类呈现的。所谓易经的阴和阳,阴阳转化不过如此。

今天领悟到的,就是易经里的那个不易,不变,就是函数式,函数的不变性,一致性,函数作为描述抽象及原理的,作为第一类的函数first function,就是终极。

所有的变化,最后都通过函数串了起来。而变化的后面,就是不变,以不变应万变。函数就是相当于太极,无级就图灵机,Lambda,太极就是函数。

易有太极,始生两仪,两仪生四象,四象生八卦。

函数产生了定义及调用。又产生了参数和返回值。最后组成了对象的定义和实现,然后派生了整个计算机世界。

来源
http://www.cnblogs.com/DSharp/p/3789545.html

柯里化那里可以这样写吗?

// 柯里化之前
function add(x, y) {
return x + y;
}

add(1, 2) // 3

// 柯里化之后
function add(y) {
return function (x) {
return x + y;
};
}

add(2)(1) // 3

建议用es6的arrow function会让代码更简洁

AP和Curry和像啊

@阮一峰老师:
2.1节函数的合成列子,根据上面的图我总感觉应该是如下:
const compose = function (f, g) {
return function (x) {
return g(f(x));
};
}
还望指教!

学习前端中,很多问题都是在阮大大文章帮助下解决的,特来道谢。

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)

先mark一下,最近再看别的东西,看完别的东西,立马看这篇

2.1中为什么不是 return g(f(x));而是 return f(g(x))?

@Parker Liu 对初学者用Haskell做演示真的好么= =

阮老师好,有一点比较疑惑的地方求解答:
我注意到文中的每一个函子都是继承了原始的Functor实现,然后每当有一个新的函子时这个函数的调用就会改变。
比如: 最原始的调用方法是 Functor.of(...) 当有了a函子就变成a.of(...),有了b函子就变成b.of(...),可这与我传统的编程方式好像不太一样呀,如果按着我传统的思维,应该会像这样: Functor.of(a).a(...) Functor.of(b).b(...)
这一块该怎么理解呢? 谢谢阮老师解答~

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了?

文中第七节有一处Functor误作Function,应是笔误,望纠正。

开个脑洞,数组有map但它是对象,所以它可以算是函子吗?

“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));
};
}

@prettykernel:谢谢指出,已改正。

@cshenger:是的, Array 是函子。

@steam:两种写法都可以,函数式编程的惯例,似乎是先运行右边参数。

引用动感小前端的发言:

@Parker Liu 对初学者用Haskell做演示真的好么= =

就本文中介绍的知识来看,用到的Haskell的最基础的东西,这时Haskell的语法是最简洁的,是比较容易理解的。至于monad,会用即可,概念的理解可以先跳过去。实际上本文也没有告诉你monad究竟是什么。

另外,柯里化用下面这种方式来写更符合函数式编程的风格:
var curry = function (f) {
return function(x) {
return function(y) {
return f(x, y);
};
};
}

暂时看不懂,想知道函数式编程有什么优点,除了数学研究。。

感觉讲的有点深奥,慢慢了解吧

从文中例子看完全没看出函数式编程的好处来……
这个例子
readFile('./user.txt')
.flatMap(tail)
.flatMap(print)
直接写成print(tail(readFile('./user.txt')))难道不是更好懂?
要说链式调用,面向对象也可以轻易实现啊。。

刚刚开始scala;文章很好,多谢阮先生。

@yybbb:

我在Chrome56上试也是这样子。查看调用栈,Maybe.map调用的是Functor.map,而不是Maybe在class里面声明的map

估计是 Maybe.of(null) 返回的是 Functor 而不是 Maybe

请问这行代码能正确执行?
`(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
```

@yybbb:

class Functor {
constructor(val) {
this.val = val;
}
static of(val){
return new this(val)
}
map(f) {
return new Functor(f(this.val));
}
}

测试这样写能解决问题

引用聪明的剑圣的发言:

@阮一峰老师:
2.1节函数的合成列子,根据上面的图我总感觉应该是如下:
const compose = function (f, g) {
return function (x) {
return g(f(x));
};
}

还望指教!


恩作者写错了,f(x) -> Y, g(y) -> Z 则 gof(x) = g(f(x))

具体有什么用?目前看来除了装X还有什么用?简化代码?让人看不懂?如何维护呢?真心求解

Either 函子这块

Either extends 了 Functor

如果 Functor 是跟上文一样的东西

那么 Either 的 constructor里 应该先执行super() 把,不然constructor的 this.left this 会报未定义错误的。

感受了一番函数式编程的奥妙,3Q

我做个简单的类比,chroma电源测试系统里,他们的软件如是高度封装的指令,每一条指令写上参数就能实现一个动作,比如一个电源的动作过程,先设置电压-->频率-->输出,分别对应3条指令,SetINSRC_Vout,SetINSRC_Frequency,SetINSRC_OutputState,每条指令填写相关的电压频率和状态参数,就能实现这个电源动作过程。函数式编程我理解就是把一个通用事件的步骤都封装在一条指令里,一条指令就一个动作,根据作者的范畴论,只要沿着箭头的步骤添加相关指令就能实现这个范畴的功能,这样其实是大大简化了编程的难度

阮老师,不知道你会不会看到这条留言,你的有些文章很长,我建议在页面加上一个目录导航,这样便于阅读。

道理我都懂,就是有点懵。还是喜欢面向对象,好理解:)

谢谢您。谢谢您做出的贡献

范畴的wiki定义翻译的似乎有欠缺,就字面意思而言,是不是应该翻译成:"范畴就是一种以箭头连接的物体所构成的代数结构。"更合适一些呢?

transformation这个是“变换”的意思,线性代数里面我们就学过,线性变换linear transformation。

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); //右值无效的情况下左值作为默认值始终不变

@shinefine 说:
人家有人家的Either定义,你自己把定义改了而已,这就能说别人错了?

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));
会报错吧

不知道博主还能否看到这条评论

引用sheng的发言:

@shinefine 说:
人家有人家的Either定义,你自己把定义改了而已,这就能说别人错了?

@sheng
下面这个函数是我对 IsOdd()的定义:
function IsOdd(n){
return n%2 ==0
}

照你的逻辑,你不能说我对Odd 定义错了


倒数第二个例子, 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调用,
但是作者你的用意不是要将文件内容打印在控制台上吗, 你文章里那样写是做不到的呀, 只能产生链试调用吧。

还是看的有点懵……

看完还是不明白,为什么要用函数式编程,应用场景中到底遇到了什么问题会决定去用函数式编程

看到2.2有个疑惑,什么是纯的,什么是不纯的,完全没有给出解释

引用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并不是一个函子,其有两个类型变量。


"函子只是Cat范畴中的态射,即Cat范畴(其对象是范畴)中的范畴之间的态射。" 谢谢指正,
读到"函子是一种范畴,是一个容器"就感觉有问题,直接略过到下面来看评论,发现果然写的有问题。

三、函子 以后的部分没看明白,是函数式编程的理论规定还是什么?

最好在文中写明IO单子的定义,然后把文中“new IO”改为“IO.of”。

IO单子示例部分“print”定义中“console.log(x)”似应为“console.log(x())”。

关于monad的作用论述,似乎应着重于monad.chain与applicative.ap的不同:前者总是返回源函子的、无变化的核,而后者返回值则不保证源函子核的不变性。

可以推荐一本相关方面的Js书籍吗?

阮老师,在那个组合f和g的例子中,按照您画的图,应该是return g(f(x)),因为是先执行f

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();
});

已经看到解决方法了~

似懂非懂的。mark一下。补充知识再看看

看第一遍的时候,不要看实现过程,就看使用就行了。

没有调用到函子里面的of方法啊,我是在最新的chrome控制台运行的

阮老师,我下面的例子函数是不可以交换的,是不是我的函数不满足交换律,不能函数式

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)

阮老师小哥哥的文章,太好了,打开了我函数式编程认知的大门,让我受益匪浅。
阮老师小哥哥有空私下传授以下我吗?

以前看了很多文章,都似懂非懂的,这篇让我茅塞顿开,非常感谢阮老师

@yybbb:

这里是因为Functor.of函数中实例化的是一个Functor对象,所以不会调用Maybe的map方法,而是调用Functor的map,所以会报错。

第二次看 似乎看懂了些了

IO IO IO IO 的代码呢???留一半不写全?????
show me the code

前面看着还行,往后就开始一头雾水 ~

我算是发现了,每一篇博文都需要看几遍才能吸收。写的很精彩

当年看了一头雾水, 时隔多年再次阅读, 竟然看明白了.

这篇文章有很多问题评论也有提到一些,建议直接看参考连接。
推荐第一个连接,最好带着思考读,有什么疑问去翻翻Github里的Issues也会有收获。

呵呵。。什么范畴论,无非是扯虎皮拉大旗。

理论上通过函数,就可以从范畴的一个成员,算出其他所有成员。
这个说法在数学意义上显然是错误的。一个简单的反例是离散范畴,所有 object 都只有到自己的恒等态射。容易验证它是满足范畴的定义的。

我要发表看法

«-必填

«-必填,不公开

«-我信任你,不会填写广告链接