泛函编程(20)-泛函库设计-Further Into Parallelism
上两节我们建了一个并行运算组件库,实现了一些基本的并行运算功能。到现在这个阶段,编写并行运算函数已经可以和数学代数解题相近了:我们了解了问题需求,然后从类型匹配入手逐步产生题解。下面我们再多做几个练习吧。
在上节我们介绍了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的更多相关文章
- 泛函编程(30)-泛函IO:Free Monad-Monad生产线
在上节我们介绍了Trampoline.它主要是为了解决堆栈溢出(StackOverflow)错误而设计的.Trampoline类型是一种数据结构,它的设计思路是以heap换stack:对应传统递归算法 ...
- 泛函编程(5)-数据结构(Functional Data Structures)
编程即是编制对数据进行运算的过程.特殊的运算必须用特定的数据结构来支持有效运算.如果没有数据结构的支持,我们就只能为每条数据申明一个内存地址了,然后使用这些地址来操作这些数据,也就是我们熟悉的申明变量 ...
- 泛函编程(23)-泛函数据类型-Monad
简单来说:Monad就是泛函编程中最概括通用的数据模型(高阶数据类型).它不但涵盖了所有基础类型(primitive types)的泛函行为及操作,而且任何高阶类或者自定义类一旦具备Monad特性就可 ...
- 泛函编程(28)-粗俗浅解:Functor, Applicative, Monad
经过了一段时间的泛函编程讨论,始终没能实实在在的明确到底泛函编程有什么区别和特点:我是指在现实编程的情况下所谓的泛函编程到底如何特别.我们已经习惯了传统的行令式编程(imperative progra ...
- 泛函编程(27)-泛函编程模式-Monad Transformer
经过了一段时间的学习,我们了解了一系列泛函数据类型.我们知道,在所有编程语言中,数据类型是支持软件编程的基础.同样,泛函数据类型Foldable,Monoid,Functor,Applicative, ...
- 泛函编程(25)-泛函数据类型-Monad-Applicative
上两期我们讨论了Monad.我们说Monad是个最有概括性(抽象性)的泛函数据类型,它可以覆盖绝大多数数据类型.任何数据类型只要能实现flatMap+unit这组Monad最基本组件函数就可以变成Mo ...
- 泛函编程(4)-深入Scala函数类
既然是泛函编程,多了解一下函数自然是免不了的了: 方法(Method)不等于函数(Function) 方法不是函数但可以转化成函数:可以手工转换或者由编译器(compiler)在适当的情况下自动转换. ...
- 泛函编程(24)-泛函数据类型-Monad, monadic programming
在上一节我们介绍了Monad.我们知道Monad是一个高度概括的抽象模型.好像创造Monad的目的是为了抽取各种数据类型的共性组件函数汇集成一套组件库从而避免重复编码.这些能对什么是Monad提供一个 ...
- 泛函编程(9)-异常处理-Option
Option是一种新的数据类型.形象的来描述:Option就是一种特殊的List,都是把数据放在一个管子里:然后在管子内部对数据进行各种操作.所以Option的数据操作与List很相似.不同的是Opt ...
随机推荐
- EF: Returns multi table from procedure
原文:https://msdn.microsoft.com/en-us/data/jj691402.aspx
- Oracle 11g EM安全证书问题无法访问的解决办法
OS: Windows Server 2012 Oracle: 11g R2 上一篇 Oracle 11g EM删除重建的方法 通过命令的方式重建了EM,启动也成功 emctl status dbco ...
- pidgin修改来消息字体大小
vi ~/.gtkrc-2.0写入如下内容设置自己想要的字体大小 style "imhtml-fix"{ font_name = "Sans 14"} w ...
- CDN 和 DNS
原文地址:http://www.cnblogs.com/xitang/p/3575255.html 相信有很多的朋友会被这几个名词绕的有些头大,很多朋友觉得智能DNS跟双线加速.CDN加速是类似的技术 ...
- [转]非OpenVZ下利用谷歌TCP-BBR协议单边加速你的VPS
前段时间谷歌推出了新的 TCP-BBR 开源算法,可以起到单边加速 TCP 连接的效果,也就是不用客户端的配合,用来替代收费的锐速再合适不过,毕竟开源免费.TCP-BBR 的目的是要尽量跑满带宽,并且 ...
- Android代码截屏
本文来源:http://myhpu2008.iteye.com/blog/999779 这种方法应该只能对当前Activity本身进行截屏,因而你只能在你应用程序中参照该代码对其应用程序本身截屏. i ...
- Jenkins+Maven+SVN快速搭建持续集成环境(转)
Jenkins是一个可扩展的持续集成引擎,Jenkins非常易于安装和配置,简单易用,下面看看我们是如何几分钟就快速搭建一个持续集成环境吧. 假设我们目前已经有2个maven项目:entities(J ...
- 【转】How to resolve ORA-19706 error when select from dblink
文章转自:http://www.dbform.com/html/2012/1846.html
- Flex Error #2156问题
出现这个问题是因为应用程序使用了特殊端口,修改端口就可以解决. 特殊端口列表 A security change has been made in Adobe Flash Player 9.0.115 ...
- Linux高级编程--05.文件读写
缓冲I/O和非缓冲I/O 文件读写主要牵涉到了如下五个操作:打开.关闭.读.写.定位.在Linux系统中,提供了两套API, 一套是C标准API:fopen.fclose.fread.fwrite.f ...