七周七并发模型:函数式编程

Posted by 蔡华的博客 on February 8, 2018

概念

  • 命令式编程的代码由一系列改变全局状态的语句构成,而函数式编程则是将计算过程抽象成表达式求值。
  • 这些表达式由纯数学函数构成,而这些数学函数是第一类对象(我们可以像操作数值一样操作第一类对象)并且没有副作用。由于没有副作用,函数式编程可以更容易做到线程安全,因此特别适合于并发编程。

为什么在并发和并行问题时会用到函数式编程

  • 有关锁的一些规则,都是针对于线程之间共享的可变的数据——换个说法就是共享可变状态。而对于不变的数据,多线程不使用锁就可以安全地进行访问。
  • 这就是为什么在解决并发和并行问题时函数式编程会如此引人注目——它没有可变状态,所以不会遇到由共享可变状态带来的种种问题。
  • 纯粹的函数式语言中,函数都具有引用透明性——在任何调用函数的地方,都可以用函数运行的结果来替换函数的调用,而不会对程序产生副作用。这个特性也使得可以任意安排多个计算过程的求值顺序,包括让它们并行
  • 所有函数(至少是理论上)都可以同时执行。这种执行方式被称为数据流式编程(dataflow programming)。
  • PS:其实本书在第三章函数式编程部分的前两天中的内容,在我看来更多的是利用语言或者运行时本书的并发能力。后续会专门写个文章总结下C#中对应的并发功能。

写在2018.2.8的第一版总结

  • 函数式编程确实是个大话题,在七周七并发模式看到第三章的时候我卡到了函数式编程这里。然后看完了《函数式编程思维》这本书,《C#函数式程序设计》还在路上,我想这是一次很不错的机会,让我好好的学习下。

参考

从C#的角度看看

  • Map、Reduce、Filter分别对应了C# linq中的三个函数
Map = Select | Enumerable.Range(1, 10).Select(x => x + 2);
Reduce = Aggregate | Enumerable.Range(1, 10).Aggregate(0, (acc, x) => acc + x);
Filter = Where | Enumerable.Range(1, 10).Where(x => x % 2 == 0);
  • Map从下面函数式编程部分的含义看就是接收一个函数,作用于范畴中每一个值,使得范畴从A变成B。
  • Filter就是过滤,在我理解里面其实也就是一个传递给map的函数,这里的map是函子中的map。
  • Reduce,正如下图说的,它是个折叠的作用。要我说就是sum的过程。只不过也许并不是真的对数值进行累加,而是可以做其它的处理。总之细细体会下面图片中的文字吧。
  • PS:折叠这个词用的好。

来自知乎的一个回答

结论:函数式编程会把数据的结构外显,而命令式则把执行过程外显。 或者这样说:你在读函数式代码时,经常会想不清楚执行过程;而你在读命令式代码时,会经常搞不清楚当前对象有哪些属性。

函数式编程思维

image

函数式编程

原文

范畴

  • 彼此之间存在某种关系的概念、事物、对象等等,都构成”范畴”。随便什么东西,只要能找出它们之间的关系,就能定义一个”范畴”。
  • 范畴的数学模型:
    • 所有成员是一个集合
    • 变形关系是函数

容器

  • 可以把”范畴”想象成是一个容器,里面包含两样东西。
    • 值(value)
    • 值的变形关系,也就是函数。

范畴论与函数式编程的关系

  • 范畴论使用函数,表达范畴之间的关系。
  • 本质上,函数式编程只是范畴论的运算方法,跟数理逻辑、微积分、行列式是同一类东西,都是数学方法,只是碰巧它能用来写程序。
  • 为什么函数式编程要求函数必须是纯的,不能有副作用?因为它是一种数学运算,原始目的就是求值,不做其他事情,否则就无法满足函数运算法则了。
  • 总之,在函数式编程中,函数就是一个管道(pipe)。这头进去一个值,那头就会出来一个新的值,没有其他作用。

函子

  • 函子是函数式编程里面最重要的数据类型,也是基本的运算单位和功能单位。
  • ++它首先是一种范畴,也就是说,是一个容器,包含了值和变形关系++。比较特殊的是,它的变形关系可以依次作用于每一个值,将当前容器变形成另一个容器。
  • 一般约定,函子的标志就是容器具有map方法。该方法将容器里面的每一个值,映射到另一个容器。
  • 函数式编程里面的运算,都是通过函子完成,即运算不直接针对值,而是针对这个值的容器—-函子。函子本身具有对外接口(map方法),各种函数就是运算符,通过接口接入容器,引发容器里面的值的变形。

  • PS:实际上函子通过map方法接收函数来实现转换。而所有的计算本质还是这个函数对函子中的值的计算

常见的函子

of
  • new一个函子
Maybe
  • Maybe 函子就是为了解决内部值为null而设计的。简单说,它的map方法里面设置了空值检查。
Either
  • 条件运算if…else是最常见的运算之一,函数式编程里面,使用 Either 函子表达。
  • Either 函子内部有两个值:左值(Left)和右值(Right)。右值是正常情况下使用的值,左值是右值不存在时使用的默认值。
ap
  • 函子B内部的函数,可以使用函子A内部的值进行运算。这时就需要用到 ap 函子。
  • ap 是 applicative(应用)的缩写。凡是部署了ap方法的函子,就是 ap 函子。
  • ap是为了解决一个函子使用另一个函子的值的问题。
Monad
  • Monad 函子的作用是,总是返回一个单层的函子。
  • 它有一个flatMap方法,与map方法作用相同,唯一的区别是如果生成了一个嵌套函子,它会取出后者内部的值,保证返回的永远是一个单层的容器,不会出现嵌套的情况。
  • Monad 函子的重要应用,就是实现 I/O (输入输出)操作。

  • 举例说明:
class Monad extends Functor {
  join() {
    return this.val;
  }
  flatMap(f) {
    return this.map(f).join();
  }
}

// 具体事例

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('./user.txt')
.flatMap(print)

  • 上面第一段代码中如果f函数返回一个函子,map本身也是返回函子。这样就的造成了this.map(f)是函子的嵌套。
  • 从事例的角度看,readFile('./user.txt')本身是返回了一个函子IO,其中的值为函数function() { return fs.readFileSync(filename, 'utf-8');}(注意:函子的值可以是函数,而且因为这里的new实际就是上面的of,所以里面的参数应该是值),它其实是从文件中读取到的数据。而.flatMap(print)根据上面的描述它会取出后者内部的值,保证返回的永远是一个单层的容器,不会出现嵌套的情况。 。它取出的就是第一个IO函子中的值(函数),然后在将这个值作为参数传递到print函数中。只有就形成了函数链。
  • PS:以上只是我的理解,这篇文章的评论区有很多质疑,但是因为我确实对这个不了解,所以就按照原文的理解来解释了。