上两节我们建了一个并行运算组件库,实现了一些基本的并行运算功能。到现在这个阶段,编写并行运算函数已经可以和数学代数解题相近了:我们了解了问题需求,然后从类型匹配入手逐步产生题解。下面我们再多做几个练习吧。

在上节我们介绍了asyncF,它的类型款式是这样的:asyncF(f: A => B): A => Par[B],从类型款式(type signature)分析,asyncF函数的功能是把一个普通的函数 A => B转成A => Par[B],Par[B]是一个并行运算。也就是说asyncF可以把一个输入参数A的函数变成一个同样输入参数A的并行运算。asyncF函数可以把List[A],一串A值,按照函数A => B变成List[Par[A]],即一串并行运算。

例:函数f: (a: A) => a + 10:List(1,2,3).map(asyncF(f))=List(Par(1+10),Par(2+10),Par(3+10)),这些Par是并行运算的。但它们的运算结果需要另一个函数sequence来读取。我们从以上分析可以得出sequence的类型款式:

 def sequence[A](lp: List[Par[A]]): Par[List[A]]

用sequence把List[Par[A]]转成Par[List[A]]后我们就可以用Par.map对List[A]进行操作了。List有map,我们可以再用map对A进行操作。在上一节我们做了个练习:

 def parMap[A,B](l: List[A])(f: A => B): Par[List[B]]

parMap按List[A]产生了一串并行运算的函数f。我们可以从类型匹配着手一步一步推导:

1、lp: List[Par[B]] = l.map(asyncF(f))

2、pl: Par[List[B]] = sequence(lp) >>> parMap

再做个新的习题:用并行运算方式Filter List:

 def parFilter[A](as: List[A])(f: A => Boolean): Par[List[A]]

我们还是从类型匹配着手一步步推导:

1、asyncF( a => if(f(a)) List(a) else List() )  >>> Par[List[A]]

2、lpl: List[Par[List[A]]] = as.map( asyncF( a => if(f(a)) List(a) else List()))

3、pll: Par[List[List[A]]] = sequence(lpl)

4、map(pll){ a => a.flatten } >>> Par[List{A]]

   def parFilter[A](as: List[A])(f: A => Boolean): Par[List[A]] = {
val pars: List[Par[List[A]]] = as.map(asyncF( (a: A) => if (f(a)) List(a) else List() ))
map(sequence(pars)){ a => a.flatten }
} //> parFilter: [A](as: List[A])(f: A => Boolean)ch71.Par.Par[List[A]]

测试结果:

 parFilter(List(10,29,13,3,6,48)){_ > 10}(es).get//> pool-1-thread-1
//| pool-1-thread-2
//| pool-1-thread-3
//| pool-1-thread-4
//| pool-1-thread-5
//| pool-1-thread-6
//| pool-1-thread-7
//| pool-1-thread-8
//| pool-1-thread-9
//| pool-1-thread-10
//| pool-1-thread-11
//| pool-1-thread-12
//| pool-1-thread-14
//| pool-1-thread-16
//| pool-1-thread-13
//| pool-1-thread-15
//| pool-1-thread-17
//| res0: List[Int] = List(29, 13, 48)

再做一个计算字数的练习:用并行运算方式来计算List里的文字数。我们尽量用共性的方法来通用化解答。如果文字是以List装载的活,类型就是:List[String],举个实例:List("the quick fox","is running","so fast")。我们可以分两步解决:

1、"the quick fox".split(' ').size >>> 把字符串分解成文字并计算数量

2、List(A,B,C) >>> A.size + B.size + C.size >>> 把List里的文字数积合。

这两步可以分两个函数来实现:

1. f: A => B >>> 我们需要把这个函数转成并行运算:List[Par[B]]

2. g: List[B] => B

   def generalWordCount[A,B](as: List[A])(f: A => B)(g: List[B] => B): Par[B] = {
val lp: List[Par[B]] = as.map(asyncF(f))
val pl: Par[List[B]] = sequence(lp)
map(pl)(g)
} //> generalWordCount: [A, B](as: List[A])(f: A => B)(g: List[B] => B)ch71.Par.P
//| ar[B]
def wordCount(as: List[String]): Par[Int] = {
generalWordCount(as)(_.split(' ').size)(_.sum)
} //> wordCount: (as: List[String])ch71.Par.Par[Int]
val lw = List("the quick silver fox", "is running","the one legged fog", "is hopping")
//> lw : List[String] = List(the quick silver fox, is running, the one legged
//| fog, is hopping)
wordCount(lw)(es).get //> pool-1-thread-1
//| pool-1-thread-3
//| pool-1-thread-2
//| pool-1-thread-15
//| pool-1-thread-16
//| pool-1-thread-7
//| pool-1-thread-10
//| pool-1-thread-14
//| pool-1-thread-6
//| pool-1-thread-13
//| pool-1-thread-9
//| res7: Int = 12

相信大家对泛函编程的这种数学解题模式已经有了一定的了解。

在前面我们曾经提过现在的fork实现方式如果使用固定数量线程池的话有可能造成锁死:

   val es = Executors.newFixedThreadPool(1)
val a = fork(async(40+2))
run(es)(a).get

我们再回顾一下fork的实现:

     def fork[A](pa: => Par[A]): Par[A] = {
es => {
es.submit(new Callable[A] {
def call: A = run(es)(pa).get
})
}
}

可以看出我们提交的callable内部是一个run par,这个run会再提交一个callable然后锁定get。外面的callable必须等待内部callable的get锁定完成。所以这种fork实现是需要两个线程的。如果线程池无法再为内部callable提供线程的话,那么外面的callable就会处于永远等待中形成死锁。上面的parMap函数会按照List的长度分解出同等数量的并行运算,运行时会造成死锁吗?如果线程池不是固定数量线程的话,答案就是否定的:如果并行运算数量大于线程数,那么运算会分批进行:后面的运算可以等待前面的运算完成后释放出线程后继续运行,这里重点是前面的运算始终是可以完成的,所以不会造成死锁。

我们再看看现在所有的组件函数是否足够应付所有问题,还需不需要增加一些基本组件,这也是开发一个函数库必须走的过程;这就是一个不断更新的过程。

现在有个新问题:如果一个并行运算的运行依赖另一个并行运算的结果,应该怎样解决?先看看问题的类型款式:

   def choice[A](pa: Par[Boolean])(ifTrue: Par[A], ifFalse: Par[A]): Par[A]

我们可能马上想到用map: map(pa){b => if(b) ifTrue else ifFalse}, 不过这样做的结果类型是:Par[Par[A]], 是代表我们需要新的组件函数来解决这个问题吗?我们先试着解这个题:

   def choice[A](pa: Par[Boolean])(ifTrue: Par[A], ifFalse: Par[A]): Par[A] = {
es => if(run(es)(pa).get) run(es)(ifTrue) else run(es)(ifFalse)
}

我们可以看到现在choice是个最基本组件了。为了解决一个问题就创造一个新的组件不是泛函编程的风格。应该是用一些更基本的组件组合成一个描述这个问题的函数,那才是我们要采用的风格。我们应该试着用一个函数能把Par[Par[A]]变成Par[A],可能就可以用map了:

ppa: Par[Par[A]], 如果 run(es)(ppa).get 得到 pa: Par[A], 再run(es)(pa) >>> Future[A]。 Par[A] = es => Future[A],不就解决问题了嘛:

   def join[A](ppa: Par[Par[A]]): Par[A] = {
es => {
run(es)(run(es)(ppa).get())
}
}

现在可以用map来实现choice了吧。但是,map是针对元素A来操作的,ifTrue和ifFalse都是Par[A],还无法使用map。那就先放放吧。

既然我们能在两个并行运算中选择,那么能在N个并行运算中选择不是能更抽象吗?

   def choiceN[A](pb: Par[Int])(choices: List[Par[A]]): Par[A]

run(es)(pb).get 得出指数(index), choices(index)就是选择的运算了:

   def choiceN[A](pb: Par[Int])(choices: List[Par[A]]): Par[A] = {
es => {
run(es)(choices(run(es)(pb).get))
}
}

从choiceN中我们可以发现一个共性模式:是一个选择函数:Int => Par[A]。再抽象一步我们把选择函数变成:A => Par[B]。这个函数就像之前接触过的flatMap函数的传入参数函数f一样的。我们先看看flatMap的类型款式:

   def flatMap[A,B](pa: Par[A])(f: A => Par[B]): Par[B]

我们只要flatMap pb 传入 A => Par[B]就可以实现choiceN了:

   def flatMap[A,B](pa: Par[A])(f: A => Par[B]): Par[B] = {
es => {
run(es)(f(run(es)(pa).get))
}
}

有了flatMap,我们可以用它来实现choice,choiceN了:

   def choiceByFlatMap[A](pb: Par[Boolean])(ifTrue: Par[A], ifFalse: Par[A]): Par[A] ={
flatMap(pb){a => if (a) ifTrue else ifFalse }
}
def choiceNByFlatMap[A](pb: Par[Int])(choices: List[Par[A]]): Par[A] = {
flatMap(pb){choices(_)}
}

在前面我们无法用map来实现choice,因为类型不匹配。加了一个join函数,又因为map元素类型不匹配,又不行。现在看来flatMap恰恰是我们需要解决choice的组件,而且flatMap能更抽象一层,连choiceN都一并解决了。值得注意的是我们在以上解决问题的过程中一再提及类型匹配,这恰恰体现了泛函编程就是函数解题的过程。

那么flatMap,join,map之间有没有什么数学关系呢?

   def joinByFlatMap[A](ppa: Par[Par[A]]): Par[A] = {
flatMap(ppa){(x: Par[A]) => x}
}
def flatMapByJoin[A,B](pa: Par[A])(f: A => Par[B]): Par[B] = {
join(map(pa)(f))
}
def mapByFlatMap[A,B](pa: Par[A])(f: A => B): Par[B] = {
flatMap(pa) { a => unit(f(a)) }
}

它们之间的确可以用数学公式来表达。

泛函编程(20)-泛函库设计-Further Into Parallelism的更多相关文章

  1. 泛函编程(30)-泛函IO:Free Monad-Monad生产线

    在上节我们介绍了Trampoline.它主要是为了解决堆栈溢出(StackOverflow)错误而设计的.Trampoline类型是一种数据结构,它的设计思路是以heap换stack:对应传统递归算法 ...

  2. 泛函编程(5)-数据结构(Functional Data Structures)

    编程即是编制对数据进行运算的过程.特殊的运算必须用特定的数据结构来支持有效运算.如果没有数据结构的支持,我们就只能为每条数据申明一个内存地址了,然后使用这些地址来操作这些数据,也就是我们熟悉的申明变量 ...

  3. 泛函编程(23)-泛函数据类型-Monad

    简单来说:Monad就是泛函编程中最概括通用的数据模型(高阶数据类型).它不但涵盖了所有基础类型(primitive types)的泛函行为及操作,而且任何高阶类或者自定义类一旦具备Monad特性就可 ...

  4. 泛函编程(28)-粗俗浅解:Functor, Applicative, Monad

    经过了一段时间的泛函编程讨论,始终没能实实在在的明确到底泛函编程有什么区别和特点:我是指在现实编程的情况下所谓的泛函编程到底如何特别.我们已经习惯了传统的行令式编程(imperative progra ...

  5. 泛函编程(27)-泛函编程模式-Monad Transformer

    经过了一段时间的学习,我们了解了一系列泛函数据类型.我们知道,在所有编程语言中,数据类型是支持软件编程的基础.同样,泛函数据类型Foldable,Monoid,Functor,Applicative, ...

  6. 泛函编程(25)-泛函数据类型-Monad-Applicative

    上两期我们讨论了Monad.我们说Monad是个最有概括性(抽象性)的泛函数据类型,它可以覆盖绝大多数数据类型.任何数据类型只要能实现flatMap+unit这组Monad最基本组件函数就可以变成Mo ...

  7. 泛函编程(4)-深入Scala函数类

    既然是泛函编程,多了解一下函数自然是免不了的了: 方法(Method)不等于函数(Function) 方法不是函数但可以转化成函数:可以手工转换或者由编译器(compiler)在适当的情况下自动转换. ...

  8. 泛函编程(24)-泛函数据类型-Monad, monadic programming

    在上一节我们介绍了Monad.我们知道Monad是一个高度概括的抽象模型.好像创造Monad的目的是为了抽取各种数据类型的共性组件函数汇集成一套组件库从而避免重复编码.这些能对什么是Monad提供一个 ...

  9. 泛函编程(9)-异常处理-Option

    Option是一种新的数据类型.形象的来描述:Option就是一种特殊的List,都是把数据放在一个管子里:然后在管子内部对数据进行各种操作.所以Option的数据操作与List很相似.不同的是Opt ...

随机推荐

  1. IT战略规划咨询

    目录 1IT战略规划微咨询简介 2IT战略的意义 3服务模式 4IT战略规划焦点问题 5IT战略规划步骤 6服务提供方微咨询网 7微咨询价值 8微咨询服务方式 9IT工作规划与IT战略规... IT战 ...

  2. 纯CSS3实现3D特效的iPhone 6动画

    iPhone 6发布不久,屌丝怎能买得起,不过作为程序员,今天看到一个用纯CSS3绘制的iPhone 6,由于CSS3特性的运用,带有点3D的动画特效,大家可以先来看看在线演示效果. 在线演示    ...

  3. fedora Server 21 安装 Opera 29.0.1795.47

    最新文章:Virson's Blog 安装源: yum localinstall --nogpgcheck http://mirror.yandex.ru/fedora/russianfedora/r ...

  4. 3种归并操作js代码

    /**良哥的*/ function merge(a, b) { var aLen = a.length, bLen = b.length, maxLen = Math.max(aLen, bLen), ...

  5. mysql 显示行号,以及分组排序

    建表: CREATE TABLE `my_tb` ( `id` ) NOT NULL AUTO_INCREMENT, `parent_code` ) DEFAULT NULL, `code` ) DE ...

  6. ionic介绍

    ionic介绍 Ionic是一个前端的框架,帮助开发者使用HTML5, CSS3和JavaScript做出原生应用. The beautiful, open source front-end fram ...

  7. 安卓开发笔记——深入Activity

    在上一篇文章<安卓开发笔记——重识Activity >中,我们了解了Activity生命周期的执行顺序和一些基本的数据保存操作,但如果只知道这些是对于我们的开发需求来说是远远不够的,今天我 ...

  8. 【Java设计模式】单例模式

    ### 1. 概述> 单例模式是确保某一个类中有且只有一个实例. ----------### 2. 饿汉式单例``` javapublic class SingletonInstance { p ...

  9. 【原】Configuring Oracle Data Guard In Physical Standby Database

    作者:david_zhang@sh [转载时请以超链接形式标明文章] http://www.cnblogs.com/david-zhang-index/p/5042640.html参照文档:https ...

  10. 轻量级IOC框架:Ninject (下)

    一,创建依赖链(Chains of Dependency) 当我们向Ninject请求创建一个类型时,Ninject会去检查该类型和其他类型之间的耦合关系.如果有额外的依赖,Ninject也会解析它们 ...