标签:编程 scala functional programmi
IO处理可以说是计算机技术的核心。不是吗?使用计算机的目的就是希望它对输入数据进行运算后向我们输出计算结果。所谓Stream IO简单来说就是对一串按序相同类型的输入数据进行处理后输出计算结果。输入数据源可能是一串键盘字符、鼠标位置坐标、文件字符行、数据库纪录等。如何实现泛函模式的Stream IO处理则是泛函编程不可或缺的技术。
首先,我们先看一段较熟悉的IO程序:
import java.io._ def linesGt4k(fileName: String): IO[Boolean] = IO { val src = io.Source.fromFile(fileName) try { var count = 0 val lines: Iterator[String] = src.getLines while (count <= 4000 && lines.hasNext) { lines.next count += 1 } count > 4000 } finally src.close } //> linesGt4k: (fileName: String)fpinscala.iomonad.IO[Boolean]
以上例子里有几项是值得提倡的:使用完文件后及时关闭,防止资源流露、没有一次性将整个文件载入内存而是逐行读取文件内容,节省内存资源。虽然整个过程是包嵌在IO类型内,但操作代码直接产生副作用。很明显,起码IO处理过程是由非纯代码组成的,无法实现函数组合,既是无法实现泛函编程的通过重复使用组件灵活组合功能的特点了。可以相像,我们在泛函Stream IO编程中将会通过许多细小组件的各式组合来实现多样性的IO计算功能。
实际上我们想使用以下款式的表达式:
object examples { //假设我们已经获取了这个Stream[String] val lines: Stream[String] = sys.error("defined elsewhere!") //无论40k或者其它数量都很容易得取。只要换个数字就行了 val lgt40k = lines.zipWithIndex.exists(_._2 + 1 >= 40000) //把空行过滤掉 val lgt40k2 = lines.filter(! _.trim.isEmpty).zipWithIndex.exists(_._2 + 1 >= 40000) //在40k行内检查是否存在连续11行第一个字母组合为abracadabra val lgt40k3 = lines.take(40000).map(_.head).indexOfSlice("abracadabra".toList) }
不过,这个Stream[String]就不是表面上那么容易得到的了。我们先把它放一放。
我们现在可以先分析一下泛函Stream IO编程原理。泛函编程的精髓就是把一个程序分解成许多纯代码组件,然后通过各种搭配组合来实现程序整体功能。那么对于Stream IO计算我们希望能先实现那些纯代码的基本组件然后把它们组合起来。我们可以把Stream IO处理过程想象成连成一串的电视信号处理盒子:每个盒子具备一项信号转换或者过滤功能,我们将一个盒子的输出端子接入另一个盒子的输入端子使信号可以经过一连串的处理过程最终输出我们要求的信号。我们可以用一个IO处理过程代表一个信号转换盒子。它的款式是这样的;Process[I,O]。最终的IO程序就是一连串Process[I,O]。当然,第一个Process[I,O]的输入端必须连接一个Stream,而最后一个则接在一个实体的设备。我们先不管这两头,先从Process[I,O]的功能着手,使其能够连成一串并把输入类型I转变成输出类型O。
Process[I,O]的类型款式如下:
trait Process[I,O]{} case class Halt[I,O]() extends Process[I,O] case class Emit[I,O](head: O, tail: Process[I,O] = Halt[I,O]()) extends Process[I,O] case class Await[I,O](rcvfn: Option[I] => Process[I,O]) extends Process[I,O]
每个Process[I,O]都可能处于三种状态之一:
1、Halt() 停止处理IO,退出。
2、Emit(head: O,tail: Process[I,O] = Halt[I,O]()) 输出类型O元素head,进入下一状态tail,默认输出head后完成退出。
3、Await(rcvfn: Option[I] => Process[I,O]) 等待一个类型I元素输入,处理IO,返回Process类型结果
可以看出,Await状态代表了某个Process的功能。Emit只是输出该Process对IO处理的结果。
注意:虽然Process[I,O]的功能是把Stream[I]转变成Stream[O],但它绝不是Stream[I] => Stream[O]类型的函数,而是在以上三种状态中游走的状态机器(State Machine)。
以下代码例子可以作为示范:
trait Process[I,O] { def apply(sin: Stream[I]): Stream[O] = this match { case Halt() => Stream() //返回空的Stream case Emit(out,next) => out #:: next(sin) //先输出out,跟着处理剔除out的Stream[I]输入 case Await(iproc) => sin match { case h #:: t_stream => iproc(Some(h))(t_stream) //如果sin不为空,接受输入首元素后返回状态为处理剔除首元素的Stream[I]输入 case xs => iproc(None)(xs) //如果sin为空则返回处理空输入状态 } } }
按照讨论题目,以上例子中Stream[I]被转变成Stream[O],而实现方式则是按照具体状态来确定输出。
为了实现函数组合(functional composition),我们必须想办法把两个Process像接水管一样连接起来:一头的输出是另一头的输入(function fusion):
def |>[O2](p2: Process[O,O2]): Process[I,O2] = //p2的输入类型是this的输出O,最终输出为p2的输出O2 p2 match { case Halt() => Halt() //下面的动作停了,整个管道都停了 case Emit(out,next) => Emit(out, this |> next) //如果正在输出就先输出然后再连接剩下的数据 case Await(iproc) => this match { //如果下游正在等待输入元素,那么就要看上游是什么情况了 case Halt() => Halt() //如果上游停顿那么整个管道都停 case Emit(out,next) => next |> iproc(Some(out)) //上游正在输出,下游收到后进入新状态 case Await(rcvfn) => Await((oi: Option[I]) => rcvfn(oi) |> p2) //假如上游收到输入元素,立即转入新状态再继续连接 } }
另外,可以把两个Process的处理过程连接起来:一个Process处理完后接着处理另一个Process:
def ++(p2: Process[I,O]): Process[I,O] = //完成了this后接着再运算p2 this match { case Halt() => p2 //上一个Process完成后接着运算p2 case Emit(out,next) => Emit(out, next ++ p2) //等上游完成所有输出后再运算p2 case Await(iproc) => Await(iproc andThen (_ ++ p2)) //等上游处理完输入后再运算p2 }
最基本的一些组件map,flatMap:
def map[O2](f: O => O2): Process[I,O2] = //map Process的输出O this match { case Halt() => Halt() //没什么可以map的 case Emit(out,next) => Emit(f(out),next map f) //先map输入元素,再处理剩下的 case Await(iproc) => Await(iproc andThen (_ map f)) //处理完输入元素后再进行map } def flatMap[O2](f: O => Process[I,O2]): Process[I,O2] = //只处理输出端O this match { case Halt() => Halt() case Emit(out,next) => f(out) ++ next.flatMap(f) //先处理头元素再flatMap剩下的 case Await(iproc) => Await(iproc andThen (_ flatMap f)) //处理完输入后再flatMap剩下的 }
我们再试试把一串元素喂入Process:
def feed(ins: Seq[I]): Process[I,O] = { @annotation.tailrec def go(ins: Seq[I], curProcess: Process[I,O]): Process[I,O] = //尾递归算法 curProcess match { case Halt() => Halt() case Emit(out,next) => Emit(out, next.feed(ins)) //正在输出。就等完成输出后再喂剩下的 case Await(iproc) => { if (ins.isEmpty) curProcess //完成了输入元素串,可以返回结果了 else go(ins.tail,iproc(Some(ins.head))) //吃下首元素然后再继续 } } go(ins,this) }
def repeat: Process[I,O] = { //永远重复下去 def go(p: Process[I,O]): Process[I,O] = //p代表当前更新状态 p match { case Halt() => go(this) //不要停,重新再来 case Emit(out,next) => Emit(out, go(next)) //完成输出后继续go case Await(iproc) => Await { //注意{}里是partialfunction。iproc是个函数,而partialfunction是function的子类,因而可以这样写 case None => iproc(None) //没有输入元素,继续等 case Some(i) => go(iproc(Some(i))) //处理输入元素后转入新状态然后继续 } } go(this) } def repeatN(n: Int): Process[I,O] = { //重复n次 def go(n: Int, curProcess: Process[I,O]): Process[I,O] = curProcess match { case Halt() => if (n <= 0) Halt() //n次后真停 else go(n-1, curProcess) //算一次重复 case Emit(out,next) => Emit(out, go(n,next)) //虽然状态更新了,但未完成流程。还不算一次重复 case Await(iproc) => Await { case None => iproc(None) //继续等 case Some(i) => go(n,iproc(Some(i))) //更新了状态,但未完成流程,不算一次重复 } } go(n,this) }
我们可以定义它的PartialFunction:
{ case 0: "Zero"
case 10: "Ten" }
由于Await(iproc)中的iproc >>> Option[I] => Process[I,O], PartialFunction是Function的子类所以我们可以写成:
Await {
case None => ???
case Some(i) => ???
}
下面是一组Process的基本方法和组件:
object Process { case class Halt[I,O]() extends Process[I,O] case class Emit[I,O](head: O, tail: Process[I,O] = Halt[I,O]()) extends Process[I,O] case class Await[I,O](rcvfn: Option[I] => Process[I,O]) extends Process[I,O] def emit[I,O](out: O, next: Process[I,O] = Halt[I,O]()) = Emit(out, next) def await[I,O](iproc: I => Process[I,O], fallback: Process[I,O] = Halt[I,O]): Process[I,O] = Await { case Some(i) => iproc(i) //使用基本类型I case None => fallback //定义了没有输入元素时应该怎么处理 } }
def liftOnce[I,O](f: I => O): Process[I,O] = //给我一个I=>O,我返回Process[I,O] Await { case Some(i) => emit(f(i)) //等到一个输入元素I。把它升成一个状态为输出的Process case None => Halt() } def repeatLift[I,O](f: I => O): Process[I,O] = liftOnce(f).repeat def lift[I,O](f: I => O): Process[I,O] = //不同实现方式的repeatLift Await { case Some(i) => emit(f(i), lift(f)) case None => Halt() }
def filter[I](f: I => Boolean): Process[I,I] = //对输入I进行过滤,不转变I, 所以结果是: Process[I,I] Await[I,I] { //用PartialFunction来分解两种输入参数值面对的情况 case None => Halt[I,I]() //没有输入,停止 case Some(i) if(f(i)) => Emit[I,I](i) }.repeat //重复过滤所有输入元素 def take[I](n: Int): Process[I,I] = //可以中途退出 if (n <= 0) Halt[I,I]() else Await[I,I] { //进行输入、输出这种IO操作 case None => Halt[I,I]() //没有输入就完成退出 case Some(i) => Emit[I,I](i,take[I](n-1)) //输出通过过滤的,继续过滤剩下的输入元素 } def takeWhile[I](f: I => Boolean): Process[I,I] = //可以中途退出 Await[I,I] { case None => Halt[I,I]() //没有输入就完成退出 case Some(i) if(f(i)) => Emit[I,I](i, takeWhile[I](f)) } def sendAsIs[I]: Process[I,I] = lift(identity) //直接输出任何输入元素 def drop[I](n: Int): Process[I,I] = //必须浏览所有输入元素。不可中途退出 if (n <= 0) sendAsIs[I] else Await[I,I](i => drop[I](n-1)) //收取输入元素,直接扔掉,继续n-1循环 def dropWhile[I](f: I => Boolean): Process[I,I] = //必须浏览所有输入元素。不可中途退出 await(i => if (f(i)) dropWhile[I](f) //注意用await, 不是Await else emit(i, sendAsIs[I])) //输出这个元素后继续循环输入元素
Await {
case None => ???
case Some(i) =>
}
来分别处理可能出现的输入参数值。
我们先尝试些简单的算法:
def count[I]: Process[I,Int] = //读取输入元素次数 //读入任何东西都转成数字1.0 |> 读一个加一个 |> 读入一个就转成一个Int lift((i: I) => 1.0 ) |> sum |> lift(_.toInt) //每一个输入元素都会走完整个管道 def count2[I]: Process[I,Int] = { //递归实现方式 def go(c: Int): Process[I,Int] = await((i: I) => emit(c+1, go(c+1))) go(0) } def mean: Process[Double,Double] = { def go(s: Double, c: Double): Process[Double,Double] = await((d: Double) => emit((s+d)/(c+1), go(s+d,c+1))) go(0.0,0.0) } //以上的内部函数go都体现了一些共同点:有一个起始值,然后维护状态。我们可以分解出一个新的函数 def loop[S,I,O](z: S)(f: (I,S) => (O,S)): Process[I,O] = await((i: I) => f(i,z) match { case (o,s2) => emit(o, loop(s2)(f)) }) //用loop来实现上面的函数 def sum2: Process[Double,Double] = loop(0.0)((i:Double,s) => (s+i,s+1)) def count3[I]: Process[I,Int] = loop(0)((_: I, c) => (c+1, c+1))
def any: Process[Boolean, Boolean] = //检查是否收到过true值。即使收到true还是会继续收取输入直至完成读取 loop(false)((b: Boolean, s) => ( b || s, b || s)) def exists[I](f: I => Boolean): Process[I,Boolean] = //不能中途退出 lift(f) |> any //重复检查输入然后确定是否true. 一旦遇到true永远返回true def echo[I]: Process[I,I] = await(i => emit(i)) def skip[I,O]: Process[I,O] = await(i => Halt()) def ignore[I,O]: Process[I,O] = skip.repeat
def filter(f: O => Boolean): Process[I,O] = //过滤输出元素 this |> Process.filter(f) //this的输出接到下一个Process的输入端然后过滤它的输入元素
def feedOne[I,O](oi: Option[I])(p: Process[I,O]): Process[I,O] = //把一个元素输入p p match { case Halt() => p //无法输入,它还是它 case Emit(out,next) => Emit(out, feedOne(oi)(next)) //正在输出。输出完当前元素再开始喂入 case Await(iproc) => iproc(oi) //直接喂入 } def zip[I,O,O2](p1: Process[I,O], p2: Process[I,O2]): Process[I,(O,O2)] = //同一串输入元素同时喂入p1,p2。合并输出2tuple (p1,p2) match { case (Halt(), _) => Halt() case (_, Halt()) => Halt() case (Emit(h1,t1), Emit(h2,t2)) => Emit((h1,h2), zip(t1,t2)) case (Await(iproc), _) => Await((oi: Option[I]) => zip(iproc(oi), feedOne(oi)(p2))) case (_, Await(iproc)) => Await((oi: Option[I]) => zip(feedOne(oi)(p1), iproc(oi))) } val mean2 = zip[Double,Double,Int](sum,count) |> lift {case (s,c) => s/c}
def zip[O2](p2: Process[I,O2]): Process[I,(O,O2)] = Process.zip(this,p2) def zipWithIndex: Process[I,(O,Int)] = this zip (count map {_ + 1}) //zip从0开始
现在我们肯定可以使用这样的表达式:
count |> exists {_ > 40000}。
当然我们还没有开始讨论这个管道两头的数据源。因为我们要分开独立讨论它。
下面是以上示范代码汇总:
trait Process[I,O] { import Process._ def apply(sin: Stream[I]): Stream[O] = this match { case Halt() => Stream() //返回空的Stream case Emit(out,next) => out #:: next(sin) //先输出out,跟着处理剔除out的Stream[I]输入 case Await(iproc) => sin match { case h #:: t_stream => iproc(Some(h))(t_stream) //如果sin不为空,接受输入首元素后返回状态为处理剔除首元素的Stream[I]输入 case xs => iproc(None)(xs) //如果sin为空则返回处理空输入状态 } } def |>[O2](p2: Process[O,O2]): Process[I,O2] = //p2的输入类型是this的输出O,最终输出为p2的输出O2 p2 match { case Halt() => Halt() //下面的动作停了,整个管道都停了 case Emit(out,next) => Emit(out, this |> next) //如果正在输出就先输出然后再连接剩下的数据 case Await(iproc) => this match { //如果下游正在等待输入元素,那么就要看上游是什么情况了 case Halt() => Halt() //如果上游停顿那么整个管道都停 case Emit(out,next) => next |> iproc(Some(out)) //上游正在输出,下游收到后进入新状态 case Await(rcvfn) => Await((oi: Option[I]) => rcvfn(oi) |> p2) //假如上游收到输入元素,立即转入新状态再继续连接 } } def ++(p2: Process[I,O]): Process[I,O] = //完成了this后接着再运算p2 this match { case Halt() => p2 //上一个Process完成后接着运算p2 case Emit(out,next) => Emit(out, next ++ p2) //等上游完成所有输出后再运算p2 case Await(iproc) => Await(iproc andThen (_ ++ p2)) //等上游处理完输入后再运算p2 } def map[O2](f: O => O2): Process[I,O2] = //map Process的输出O this match { case Halt() => Halt() //没什么可以map的 case Emit(out,next) => Emit(f(out),next map f) //先map输入元素,再处理剩下的 case Await(iproc) => Await(iproc andThen (_ map f)) //处理完输入元素后再进行map } def flatMap[O2](f: O => Process[I,O2]): Process[I,O2] = //只处理输出端O this match { case Halt() => Halt() case Emit(out,next) => f(out) ++ next.flatMap(f) //先处理头元素再flatMap剩下的 case Await(iproc) => Await(iproc andThen (_ flatMap f)) //处理完输入后再flatMap剩下的 } def feed(ins: Seq[I]): Process[I,O] = { @annotation.tailrec def go(ins: Seq[I], curProcess: Process[I,O]): Process[I,O] = //尾递归算法 curProcess match { case Halt() => Halt() case Emit(out,next) => Emit(out, next.feed(ins)) //正在输出。就等完成输出后再喂剩下的 case Await(iproc) => { if (ins.isEmpty) curProcess //完成了输入元素串,可以返回结果了 else go(ins.tail,iproc(Some(ins.head))) //吃下首元素然后再继续 } } go(ins,this) } def repeat: Process[I,O] = { //永远重复下去 def go(p: Process[I,O]): Process[I,O] = //p代表当前更新状态 p match { case Halt() => go(this) //不要停,重新再来 case Emit(out,next) => Emit(out, go(next)) //完成输出后继续go case Await(iproc) => Await { //注意{}里是partialfunction。iproc是个函数,而partialfunction是function的子类,因而可以这样写 case None => iproc(None) //没有输入元素,继续等 case Some(i) => go(iproc(Some(i))) //处理输入元素后转入新状态然后继续 } } go(this) } def repeatN(n: Int): Process[I,O] = { //重复n次 def go(n: Int, curProcess: Process[I,O]): Process[I,O] = curProcess match { case Halt() => if (n <= 0) Halt() //n次后真停 else go(n-1, curProcess) //算一次重复 case Emit(out,next) => Emit(out, go(n,next)) //虽然状态更新了,但未完成流程。还不算一次重复 case Await(iproc) => Await { case None => iproc(None) //继续等 case Some(i) => go(n,iproc(Some(i))) //更新了状态,但未完成流程,不算一次重复 } } go(n,this) } def filter(f: O => Boolean): Process[I,O] = //过滤输出元素 this |> Process.filter(f) //this的输出接到下一个Process的输入端然后过滤它的输入元素 def orElse(p: Process[I,O]): Process[I,O] = this match { case Halt() => p case Await(iproc) => Await { case None => p case x => iproc(x) } case _ => this } def zip[O2](p2: Process[I,O2]): Process[I,(O,O2)] = Process.zip(this,p2) def zipWithIndex: Process[I,(O,Int)] = this zip (count map {_ + 1}) //zip从0开始 } object Process { case class Halt[I,O]() extends Process[I,O] case class Emit[I,O](head: O, tail: Process[I,O] = Halt[I,O]()) extends Process[I,O] case class Await[I,O](rcvfn: Option[I] => Process[I,O]) extends Process[I,O] def emit[I,O](out: O, next: Process[I,O] = Halt[I,O]()) = Emit(out, next) def await[I,O](iproc: I => Process[I,O], fallback: Process[I,O] = Halt[I,O]): Process[I,O] = Await { case Some(i) => iproc(i) //使用基本类型I case None => fallback //定义了没有输入元素时应该怎么处理 } def liftOnce[I,O](f: I => O): Process[I,O] = //给我一个I=>O,我返回Process[I,O] Await { case Some(i) => emit(f(i)) //等到一个输入元素I。把它升成一个状态为输出的Process case None => Halt() } def repeatLift[I,O](f: I => O): Process[I,O] = liftOnce(f).repeat def lift[I,O](f: I => O): Process[I,O] = //不同实现方式的repeatLift Await { case Some(i) => emit(f(i), lift(f)) case None => Halt() } def filter[I](f: I => Boolean): Process[I,I] = //对输入I进行过滤,不转变I, 所以结果是: Process[I,I] Await[I,I] { //用PartialFunction来分解两种输入参数值面对的情况 case None => Halt[I,I]() //没有输入,停止 case Some(i) if(f(i)) => Emit[I,I](i) }.repeat //重复过滤所有输入元素 def take[I](n: Int): Process[I,I] = //可以中途退出 if (n <= 0) Halt[I,I]() else Await[I,I] { //进行输入、输出这种IO操作 case None => Halt[I,I]() //没有输入就完成退出 case Some(i) => Emit[I,I](i,take[I](n-1)) //输出通过过滤的,继续过滤剩下的输入元素 } def takeWhile[I](f: I => Boolean): Process[I,I] = //可以中途退出 Await[I,I] { case None => Halt[I,I]() //没有输入就完成退出 case Some(i) if(f(i)) => Emit[I,I](i, takeWhile[I](f)) } def sendAsIs[I]: Process[I,I] = lift(identity) //直接输出任何输入元素 def drop[I](n: Int): Process[I,I] = //必须浏览所有输入元素。不可中途退出 if (n <= 0) sendAsIs[I] else Await[I,I](i => drop[I](n-1)) //收取输入元素,直接扔掉,继续n-1循环 def dropWhile[I](f: I => Boolean): Process[I,I] = //必须浏览所有输入元素。不可中途退出 await(i => if (f(i)) dropWhile[I](f) //注意用await, 不是Await else emit(i, sendAsIs[I])) //输出这个元素后继续循环输入元素 def sum: Process[Double,Double] = { //读进数字,输出当前总数 def go(acc: Double): Process[Double,Double] = await(d => emit(acc+d, go(acc+d))) go(0.0) } def count[I]: Process[I,Int] = //读取输入元素次数 //读入任何东西都转成数字1.0 |> 读一个加一个 |> 读入一个就转成一个Int lift((i: I) => 1.0 ) |> sum |> lift(_.toInt) //每一个输入元素都会走完整个管道 def count2[I]: Process[I,Int] = { //递归实现方式 def go(c: Int): Process[I,Int] = await((i: I) => emit(c+1, go(c+1))) go(0) } def mean: Process[Double,Double] = { def go(s: Double, c: Double): Process[Double,Double] = await((d: Double) => emit((s+d)/(c+1), go(s+d,c+1))) go(0.0,0.0) } //以上的内部函数go都体现了一些共同点:有一个起始值,然后维护状态。我们可以分解出一个新的函数 def loop[S,I,O](z: S)(f: (I,S) => (O,S)): Process[I,O] = await((i: I) => f(i,z) match { case (o,s2) => emit(o, loop(s2)(f)) }) //用loop来实现上面的函数 def sum2: Process[Double,Double] = loop(0.0)((i:Double,s) => (s+i,s+1)) def count3[I]: Process[I,Int] = loop(0)((_: I, c) => (c+1, c+1)) def any: Process[Boolean, Boolean] = //检查是否收到过true值。即使收到true还是会继续收取输入直至完成读取 loop(false)((b: Boolean, s) => ( b || s, b || s)) def exists[I](f: I => Boolean): Process[I,Boolean] = //不能中途退出 lift(f) |> any //重复检查输入然后确定是否true. 一旦遇到true永远返回true def echo[I]: Process[I,I] = await(i => emit(i)) def skip[I,O]: Process[I,O] = await(i => Halt()) def ignore[I,O]: Process[I,O] = skip.repeat def feedOne[I,O](oi: Option[I])(p: Process[I,O]): Process[I,O] = //把一个元素输入p p match { case Halt() => p //无法输入,它还是它 case Emit(out,next) => Emit(out, feedOne(oi)(next)) //正在输出。输出完当前元素再开始喂入 case Await(iproc) => iproc(oi) //直接喂入 } def zip[I,O,O2](p1: Process[I,O], p2: Process[I,O2]): Process[I,(O,O2)] = //同一串输入元素同时喂入p1,p2。合并输出2tuple (p1,p2) match { case (Halt(), _) => Halt() case (_, Halt()) => Halt() case (Emit(h1,t1), Emit(h2,t2)) => Emit((h1,h2), zip(t1,t2)) case (Await(iproc), _) => Await((oi: Option[I]) => zip(iproc(oi), feedOne(oi)(p2))) case (_, Await(iproc)) => Await((oi: Option[I]) => zip(feedOne(oi)(p1), iproc(oi))) } val mean2 = zip[Double,Double,Int](sum,count) |> lift {case (s,c) => s/c} count |> exists {_ > 40000} }
版权声明:本文为博主原创文章,未经博主允许不得转载。
泛函编程(35)-泛函Stream IO:IO处理过程-IO Process
标签:编程 scala functional programmi
原文地址:http://blog.csdn.net/tiger_xc/article/details/47081319