上期我们讨论了IO处理过程:Process[I,O]。我们说Process就像电视信号盒子一样有输入端和输出端两头。Process之间可以用一个Process的输出端与另一个Process的输入端连接起来形成一串具备多项数据处理功能的完整IO过程。但合成的IO过程两头输入端则需要接到一个数据源,而另外一端则可能会接到一个数据接收设备如文件、显示屏等。我们在这篇简单地先介绍一下IO数据源Source和IO数据接收端Sink。

我们先用一个独立的数据类型来代表数据源Source进行简单的示范说明,这个类型与Process类型没有任何关系:

 import ProcessLib._
object SourceSink {
trait Source[O] { //以下helper function都是把Source当作O类的List处理
def |>[O2](p: Process[O,O2]): Source[O2] //粘接一个Process p. 向其输入O
def filter(f: O => Boolean): Source[O] = this |> Process.filter(f) //向p输入O
def map[O2](f: O => O2): Source[O2] = this |> Process.lift(f)
def take(n: Int): Source[O] = this |> Process.take(n) //截取前n个O
def takeWhile(f: O => Boolean): Source[O] = this |> Process.takeWhile(f)
def drop(n: Int): Source[O] = this |> Process.drop(n) //跳过前n个O
def dropWhile(f: O => Boolean): Source[O] = this |> Process.dropWhile(f)
}

从以上trait可以看到:Source的工作原理就是把一个Process的输入黏贴到Source的输出端。我们可以用这个 |> 把一串Process粘到Source的输出,如:Src.proc1.proc2.proc3。不过我们得先把proc1,proc2,proc3定义成Source组件函数,因为Source是一个完全独立的类型。

我们再来看看一个Source特殊案例:

 case class ResourceR[R,I,O](   //Source的一个只读资源案例
acquire: IO[R], //资源使用门户 resource handle
release: R => IO[Unit], //完成使用资源后的清理函数
step: R => IO[Option[I]], //资源内容读取函数
trans: Process[I,O] //输出方式
) extends Source[O] {
def |>[O2](p: Process[O,O2]): Source[O2] = //实现抽象函数
ResourceR(acquire,release,step,trans |> p) //每次输入都产生一个ResourceR.它的trans与p进行管道对接
}

这是个只读的数据源。我们看到所有的动作都被包嵌在IO类型里,这样可以把副作用的产生延后到一些Source Interpreter来运算。这里我们只要用最简单的IO来说明就可以了:

 trait IO[A] { self =>
def run: A
def map[B](f: A => B): IO[B] =
new IO[B] { def run = f(self.run) }
def flatMap[B](f: A => IO[B]): IO[B] =
new IO[B] { def run = f(self.run).run }
}
object IO {
def unit[A](a: => A): IO[A] = new IO[A] { def run = a }
def flatMap[A,B](fa: IO[A])(f: A => IO[B]) = fa flatMap f
def apply[A](a: => A): IO[A] = unit(a) // syntax for IO { .. }
}

这个IO类型我们在前面的讨论里曾经练习过。

现在我们来看看一个文件读取的ResourceR例子:

 object Source {
import java.io._
def lines(fileName: String): Source[String] = //从文件fileName里读取String
ResourceR( //创建一个Source的实例
IO {io.Source.fromFile(fileName) }, //资源
(src: io.Source) => IO { src.close }, //清理
(src: io.Source) => IO { //读取
lazy val iterator = src.getLines
if (iterator.hasNext) Some(iterator.next) else None //读完返回None
},
Process.passUnchanged) //Process[I,I],读什么输入什么
}

现在我们可以这样写一段程序了:

  Source.lines("input.txt").count.exists{_ >= 40000 }
//> res0: ch15.SourceSink.Source[Boolean] = ResourceR(ch15.SourceSink$IO$$anon$
//| 3@762efe5d,<function1>,<function1>,Await(<function1>))

噢,记住把count和exists放到Source trait里:

     def exists(f: O => Boolean): Source[Boolean] = this |> Process.exists(f)
def count: Source[Int] = this |> Process.count

上面的表达式可以说还只是IO过程的描述。实际副作用产生是在interpreter里:

     def collect: IO[IndexedSeq[O]] = {  //读取数据源返回IO[IndexedSeq[O]], 用IO.run来实际运算
def tryOr[A](a: => A)(cleanup: IO[Unit]): A = //运算表达式a, 出现异常立即清理现场
try a catch {case e: Exception => cleanup.run; throw e}
@annotation.tailrec //这是个尾递归算法,根据trans状态
def go(acc: IndexedSeq[O], cleanup: IO[Unit], step: IO[Option[I]], trans: Process[I,O]): IndexedSeq[O] =
trans match {
case Halt() => cleanup.run; acc //停止状态,清理现场
case Emit(out,next) => go(tryOr(out +: acc)(cleanup), cleanup, step, next) //积累acc
case Await(iproc) => tryOr(step.run)(cleanup) match {
case None => cleanup.run; acc //读完了清理现场
case si => go(acc,cleanup,step,iproc(si)) //读入元素作为Process输入来改变Process状态
}
}
acquire map {res => go(IndexedSeq(),release(res),step(res),trans)} //开始读取
}

注意:无论读取完成或中途失败退出都会导致现场清理以防止资源漏出。可以推断这个interpreter还是很安全的。

与Source同样,我们还是用一个独立的类型Sink来代表数据接收端进行简单说明:

 trait Sink[I] {
def <|[I2](p: Process[I2,I]): Sink[I2] //p的输出接到Sink的输入
def filter(f: I => Boolean): Sink[I] = this <| Process.filter(f) //从p接收I
def map[I2](f: I2 => I): Sink[I2] = this <| Process.lift(f) //将接收的I2变成I
def take(n: Int): Sink[I] = this <| Process.take(n) //从p接收前n个I
def takeWhile(f: I => Boolean): Sink[I] = this <| Process.takeWhile(f)
def drop(n: Int): Sink[I] = this <| Process.drop(n) //过滤掉首n个I
def dropWhile(f: I => Boolean): Sink[I] = this <| Process.dropWhile(f)
}

这和Source trait及其相似。注意和Process连接是反向的:由p指向Sink。

同样,一个只写的资源实例如下:

 case class ResourceW[R,I,I2](  //只写资源
acquire: IO[R], //资源使用门户, resource handle
release: R => IO[Unit], //清理函数
rcvf: R => (I2 => IO[Unit]), //接收方式
trans: Process[I,I2] //处理过程
) extends Sink[I] {
def <|[I2](p: Process[I2,I]): Sink[I2] =
ResourceW(acquire,release,rcvf,p |> trans) //制造一个ResourceW实例,由p到trans
}

这个也和ResourceR相似。还是与Process连接方式是反方向的:由p到trans。

以下是一个向文件写入的Sink组件:

 object Sink {
import java.io._
def file(fileName: String, append: Boolean = false): Sink[String] = //结果是Sink[String]。必须用interpreter来运算
ResourceW( //是一个ResourceW实例
IO {new FileWriter(fileName,append) }, //创建FileWriter
(w: FileWriter) => IO {w.close}, //释放FileWriter
(w: FileWriter) => (s: String) => IO {w.write(s)}, //写入
Process.passUnchanged //不处理写入数据
)
}

在学习过程中发现,独立于Process类型的Source,Sink类型使IO算法的表达式类型的集成很困难。这也限制了组件的功能。我们无法实现泛函编程简洁高雅的表达形式。在下面的讨论中我们会集中精力分析具备数据源功能的Process,希望在表达方式上能有所进步。

泛函编程(36)-泛函Stream IO:IO数据源-IO Source & Sink的更多相关文章

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

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

  2. 泛函编程(38)-泛函Stream IO:IO Process in action

    在前面的几节讨论里我们终于得出了一个概括又通用的IO Process类型Process[F[_],O].这个类型同时可以代表数据源(Source)和数据终端(Sink).在这节讨论里我们将针对Proc ...

  3. 泛函编程(35)-泛函Stream IO:IO处理过程-IO Process

    IO处理可以说是计算机技术的核心.不是吗?使用计算机的目的就是希望它对输入数据进行运算后向我们输出计算结果.所谓Stream IO简单来说就是对一串按序相同类型的输入数据进行处理后输出计算结果.输入数 ...

  4. 泛函编程(37)-泛函Stream IO:通用的IO处理过程-Free Process

    在上两篇讨论中我们介绍了IO Process:Process[I,O],它的工作原理.函数组合等.很容易想象,一个完整的IO程序是由 数据源+处理过程+数据终点: Source->Process ...

  5. 泛函编程(32)-泛函IO:IO Monad

    由于泛函编程非常重视函数组合(function composition),任何带有副作用(side effect)的函数都无法实现函数组合,所以必须把包含外界影响(effectful)副作用不纯代码( ...

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

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

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

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

  8. 二、linux IO 编程---系统调用和POSIX标准和标准IO

    2.1 系统调用 2.1.1 概念 所谓系统调用(system call)是指曹错系统提供给用户程序的一组“特殊”接口,用户程序可以通过这组“特殊”接口来获得操作系统内核提供的特殊服务. 应用程序可以 ...

  9. 泛函编程(29)-泛函实用结构:Trampoline-不再怕StackOverflow

    泛函编程方式其中一个特点就是普遍地使用递归算法,而且有些地方还无法避免使用递归算法.比如说flatMap就是一种推进式的递归算法,没了它就无法使用for-comprehension,那么泛函编程也就无 ...

随机推荐

  1. Android开发学习之路-Service和Activity的通信

    在很多时候,Service都不仅仅需要在后台运行,还需要和Activity进行通信,或者接受Activity的指挥,如何来实现,来看代码. 定义一个服务 // 创建一个服务,然后在onBind()中返 ...

  2. token防止表单重复提交

    出现表单重复提交的三种情况: 一.服务器响应缓慢,用户多次点击提交按钮. 二.提交成功后刷新页面. 三.提交成功后返回表单页面再次点击提交. package com.jalja.token; impo ...

  3. 行集函数:OpenRowSet 和 OpenQuery

    在SQL Server中,行集函数是不确定性的,这意味着,每次调用,返回值不总是相同的.返回值是不确定的,这意味着,对于相同的输入值,不保证每次返回的值都是相同的.对行集函数的每次调用,行集函数都是单 ...

  4. poj2513Colored Sticks(无向图的欧拉回路)

    /* 题意:将两端涂有颜色的木棒连在一起,并且连接处的颜色相同! 思路:将每一个单词看成一个节点,建立节点之间的无向图!判断是否是欧拉回路或者是欧拉路 并查集判通 + 奇度节点个数等于2或者0 */ ...

  5. 关于未捕获异常的处理(WPF)

    这一篇文章来谈谈对于WPF应用程序开发中的未捕获异常的处理. 首先,我们当然是要求应用程序开发人员,尽可能地在程序可能出现异常的地方都去捕捉异常,使用try-catch的方式.但是总是有一些意外的情况 ...

  6. Js杂谈-DOM

    前言 对jQuery的依赖.导致js的原生方法的淡忘,如果是封装自己的库,那势必要用到js的许多原生方法.从Jquery强大的dom处理开始,我们开始回顾javascript那些古老而坚挺的DOM方法 ...

  7. Twitter Storm安装配置(Ubuntu系统)单机版

    要使用storm首先要安装以下工具:JDK.Python.zookeeper.zeromq.jzmq.storm (注:各个模块都是独立的,如果安装失败或者卡顿可以单独百度某个模块的安装,都是可以的. ...

  8. php操作mongodb中的ISODate格式日期

    mongodb 中数据记录的日期格式为"dateCreated" : ISODate("2011-12-20T07:22:50.836Z")经过翻阅php官网中 ...

  9. redis学习之三配置文件redis.conf 的含义

    摘自http://www.runoob.com/redis/redis-conf.html 安装redis之后的第一件事,我就开始配置密码,结果总是不生效,而我居然还没想到原因.今天突然用命令行设置了 ...

  10. QT学习笔记6

    事件(event) 一般来说,使用Qt编程时,我们并不会把主要精力放在事件上,因为在Qt中,需要我们关心的事件总会发出一个信号.比如,我们关心的是QPushButton的鼠标点击,但我们不需要关心这个 ...