泛函编程(38)-泛函Stream IO:IO Process in action
在前面的几节讨论里我们终于得出了一个概括又通用的IO Process类型Process[F[_],O]。这个类型同时可以代表数据源(Source)和数据终端(Sink)。在这节讨论里我们将针对Process[F,O]的特性通过一些应用实例来示范它的组合性(composibility)和由数据源到接收终端IO全过程的功能完整性。
我们已经在前面的讨论中对IO Process的各种函数组合进行了调研和尝试,现在我们先探讨一下数据源设计方案:为了实现资源使用的安全性和IO程序的可组合性,我们必须保证无论在完成资源使用或出现异常时数据源都能得到释放,同时所有副作用的产生都必须延后直至Interpreter开始运算IO程序时。
我们先试试一个读取文件字符内容的组件:
import java.io.{BufferedReader, FileReader}
def readFile(fileName: String): Process[IO,String] =
await[IO,BufferedReader,String](IO{new BufferedReader(new FileReader(fileName))}){
case Left(err) => Halt(err)
case Right(r) => {
lazy val next: Process[IO,String] = await(IO{r.readLine}) {
case Left(err2) => await(IO{r.close}){_ => Halt[IO,String](err2)}()
case Right(line) => emit(line, next)
}()
next
}
}()
注意以下几个问题:首先是所有的IO动作都是通过await函数来实现的,再就是所有产生副作用的语句都被包嵌在IO{}里,这是典型的延迟运算。我们先来看看这个await函数:
def await[F[_],A,O](req: F[A])(
rcvfn: Either[Throwable,A] => Process[F,O] = (a: Either[Throwable,A]) => Halt[F,O](End))
(fallback: Process[F,O] = Halt[F,O](End),
onError: Process[F,O] = Halt[F,O](End)): Process[F,O] = Await(req,rcvfn,fallback,onError)
req是个F[A],在例子里就是IO[A]。把这个函数精简表述就是: await(IO(iorequest))(ioresult => Process)。从这个函数的款式可以看出:在调用await函数的时候并不会产生副作用,因为产生副作用的语句是包嵌在IO{}里面的。我们需要一个interpreter来运算这个IO{...}产生副作用。await的另一个输入参数是 iorequest => Process:iorequest是运算iorequest返回的结果:可以是A即String或者是个异常Exception。所以我们可以用PartialFunction来表达:
{ case Left(err) => Process[IO,???]; case Right(a) => Process[IO,???] }
我们用PartialFunction来描述运算iorequest后返回正常结果或者异常所对应的处理办法。
上面的例子readFile就是用这个await函数来打开文件: IO {new BufferedReader(new FileReader(fileName))}
读取一行:IO {r.readLine},如果读取成功则发送emit出去: case Right(line) => emit(line,next),
如果出现异常则关闭文件:case Left(err) => IO {r.close},注意我们会使用异常End来代表正常完成读取:
case object End extends Exception
case object Kill extends Exception
以上的Kill是强行终止信号。刚才说过,我们需要用一个interpreter来运算readFile才能正真产生期望的副作用如读写文件。现在我们就来了解一个interpreter:
def collect[O](src: Process[IO,O]): IndexedSeq[O] = {
val E = java.util.concurrent.Executors.newFixedThreadPool(4)
def go(cur: Process[IO,O], acc: IndexedSeq[O]): IndexedSeq[O] = cur match {
case Halt(e) => acc
case Emit(os,ns) => go(ns, acc ++ os)
case Halt(err) => throw err
case Await(rq,rf,fb,fl) =>
val next =
try rf(Right(unsafePerformIO(rq)(E)))
catch { case err: Throwable => rf(Left(err)) }
go(next, acc)
}
try go(src, IndexedSeq())
finally E.shutdown
}
首先要注意的是这句:unsafePerformIO(rq)(E)。它会真正产生副作用。当Process[IO,O] src当前状态是Await的时候就会进行IO运算。运算IO产生的结果作为Await的rf函数输入参数,正如我们上面描述的一样。所以,运算IO{iorequest}就是构建一个Await结构把iorequest和转换状态函数rf放进去就像这样:
await(iorequest)(rf)(fb,fl) = Await(ioreques,rf,fb,fl),
然后返回到collect,collect看到src状态是Await就会运算iorequest然后再运行rf。
我们的下一个问题是如何把文件里的内容一行一行读入而不是一次性预先全部搬进内存,这样我们可以读一行,处理一行,占用最少内存。我们再仔细看看readFile的这个部分:
def readFile(fileName: String): Process[IO,String] =
await[IO,BufferedReader,String](IO{new BufferedReader(new FileReader(fileName))}){
case Left(err) => Halt(err)
case Right(r) => {
lazy val next: Process[IO,String] = await(IO{r.readLine}) {
case Left(err2) => Halt[IO,String](err2) //await(IO{r.close}){_ => Halt[IO,String](err2)}()
case Right(line) => emit(line, next)
}()
next
}
}()
如果成功创建BufferedReader,运算IO产生Right(r)结果;运算IO{r.readLine}后返回最终结果next。next可能是Halt(err)或者Emit(line,next)。如果这样分析那么整个readFile函数也就会读入文件的第一行然后emit输出。记着泛函编程特点除了递归算法之外还有状态机器(state machine)方式的程序运算。在以上例子里的运算结果除输出值line外还有下一个状态next。再看看以下这个组件:
//状态进位,输出Process[F,O2]
final def drain[O2]: Process[F,O2] = this match {
case Halt(e) => Halt(e) //终止
case Emit(os,ns) => ns.drain //运算下一状态ns,输出
case Await(rq,rf,fb,cl) => Await(rq, rf andThen (_.drain)) //仍旧输出Await
}
这个drain组件实际上起到了一个移动状态的作用。如果我们这样写:readFile("myfile.txt").drain 那么在我们上面的例子里readFile返回Emit(line,next);drain接着readFile输出状态就会运算next,这样程序又回到readFile next的IO{r.readline}运算中了。如果我们在drain组件前再增加一些组件:
readFile("farenheit.txt").filter(line => !line.startsWith("#").map(line => line.toUpperCase).drain
那么我们就会得到读取一行字符;过滤起始为#的行;转成大写字符;返回再读一行交替循环这样的效果了。
很明显readFile实在太有针对性了。函数类型款式变的复杂可读性低。我们需要一种更概括的形式来实现泛函编程语言的简练而流畅表达形式。
我们首先应该把IO运算方式重新定义一下。用await函数显得太复杂:
//await 的精简表达形式
def eval[F[_],A](fa: F[A]): Process[F,A] = //运算F[A]
await[F,A,A](fa){
case Left(err) => Halt(err)
case Right(a) => emit(a, Halt(End))
}()
def evalIO[A](ioa: IO[A]) = eval[IO,A](ioa) //运算IO[A]
//确定终结的运算
def eval_[F[_],A,B](fa: F[A]): Process[F,B] = eval[F,A](fa).drain[B] //运算F[A]直到终止
如此运算IO只需要这样写:eval(iorequest),是不是精简多了。
再来一个通用安全的IO资源使用组件函数:
def resource[R,O]( //通用IO程序运算函数
acquire: IO[R])( //获取IO资源。open file
use: R => Process[IO,O])( //IO运算函数 readLine
release: R => Process[IO,O]): Process[IO,O] = //释放资源函数 close file
eval(acquire) flatMap { r => use(r).onComplete(release(r)) } def resource_[R,O]( //与resource一样,只是运算realease直至终止
acquire: IO[R])( //获取IO资源。open file
use: R => Process[IO,O])( //IO运算函数 readLine
release: R => IO[Unit]): Process[IO,O] = //释放资源函数 close file
resource(acquire)(use)(release andThen (eval_[IO,Unit,O]))
以下是个套用resource组件的例子:从一个文件里逐行读出,在完成读取或出现异常时主动释放资源:
def lines(fileName: String): Process[IO,String] = //从fileName里读取
resource
{IO {io.Source.fromFile(fileName)}} //占用资源
{src => //使用资源。逐行读取
lazy val iter = src.getLines
def nextLine = if (iter.hasNext) Some(iter.next) else None //下一行
lazy val getLines: Process[IO,String] = //读取
eval(IO{nextLine}) flatMap { //运算IO
case None => Halt(End) //无法继续读取:完成或者异常
case Some(line) => emit(line, getLines) //读取然后发送
}
getLines
}
{src => eval_ (IO{src.close})} //释放资源
现在我们应该可以很简练但又不失清楚详尽地描述一段IO程序:
打开文件fahrenheit.txt
读取一行字符
过滤空行或者以#开始的字行,可通过的字行代表亨氏温度数
把亨氏温度转换成摄氏温度数
这里面的温度转换函数如下:
def fahrenheitToCelsius(f: Double): Double =
(f - 32) * 5.0/9.0
那么整个程序就可以这样写了:
lines("fahrenheit.txt").
filter(line => !line.startsWith("#") && !line.trim.isEmpty).
map(line => fahrenheitToCelsius(line.toDouble).toString).
drain
这段代码是不是很清晰的描述了其所代表的功能,不错!
现在到了了解IO过程的另一端:Sink的时候了。我们如果需要通过Process来实现输出功能的话,也就是把Source[O]的这个O发送输出到一个Sink。实际上我们也可以用Process来表达Sink,先看一个简单版本的Sink如下:
type SimpleSink[F[_],O] = Process[F,O => F[Unit]]
SimpleSink就是一个IO Process,它的输出是一个 O => F[Unit]函数,用一个例子来解释:
def simpleWriteFile(fileName: String, append: Boolean = false) : SimpleSink[IO, String] =
resource[FileWriter, String => IO[Unit]]
{IO {new FileWriter(fileName,append)}} //acquire
{w => IO{(s:String) => IO{w.write(s)}}} //use
{w => IO{w.close}} //release
下面是个可使用的Sink:
type Sink[F[_],O] = Process[F, O => Process[F,Unit]]
import java.io.FileWriter
def fileW(file: String, append: Boolean = false): Sink[IO,String] =
resource[FileWriter, String => Process[IO,Unit]]
{ IO { new FileWriter(file, append) }}
{ w => stepWrite { (s: String) => eval[IO,Unit](IO(w.write(s))) }} //重复循环逐行写
{ w => eval_(IO(w.close)) }
/* 一个无穷循环恒量stream. */
def stepWrite[A](a: A): Process[IO,A] =
eval(IO(a)).flatMap { a => Emit(a, stepWrite(a)) } 通过Emit的下一状态重复运算IO(a)
我们需要实现逐行输出,所以用这个stepWrite来运算IO。stepWrite是通过返回Emit来实现无穷循环的。
下一步是把Sink和Process对接起来。我们可以用以下的to组件来连接:
def to[O2](sink: Sink[F,O]): Process[F,Unit] =
join { (this zipWith sink)((o,f) => f(o)) }
join组件就是标准的monadic组件,因为我们需要把 Process[F,Process[F,Unit]]打平为Process[F,Unit]:
def join[F[_],A](p: Process[F,Process[F,A]]): Process[F,A] =
p.flatMap(pa => pa)
现在我们可以在前面例子里的Process过程中再增加一个写入celsius.txt的组件:
val converter: Process[IO,Unit] =
lines("fahrenheit.txt"). //读取
filter(line => !line.startsWith("#") && !line.trim.isEmpty). //过滤
map(line => fahrenheitToCelsius(line.toDouble).toString). //温度转换
pipe(intersperse("\n")). //加end of line
to(fileW("celsius.txt")). //写入
drain //继续循环
上面的Sink类型运算IO后不返回任何结果(Unit)。但有时我们希望IO运算能返回一些东西,如运算数据库query之后返回结果集,那我们需要一个新的类型:
type Channel[F[_],I,O] = Process[F, I => Process[F,O]]
Channel和Sink非常相似,差别只在Process[F,O]和Process[F,Unit]。
我们用Channel来描述一个数据库查询:
import java.sql.{Connection, PreparedStatement, ResultSet}
def query(conn: IO[Connection]):
Channel[IO, Connection => PreparedStatement, Map[String,Any]] = //Map === Row
resource_ //I >>> Connection => PreparedStatement
{ conn } //打开connection
{ conn => constant { (q: Connection => PreparedStatement) => //循环查询
resource_
{ IO { //运行query
val rs = q(conn).executeQuery
val ncols = rs.getMetaData.getColumnCount
val cols = (1 to ncols).map(rs.getMetaData.getColumnName)
(rs, cols)
}}
{ case (rs, cols) => //读取纪录Row
def step =
if (!rs.next) None
else Some(cols.map(c => (c, rs.getObject(c): Any)).toMap)
lazy val rows: Process[IO,Map[String,Any]] = //循环读取
eval(IO(step)).flatMap {
case None => Halt(End)
case Some(row) => Emit(row, rows) //循环运算rows函数
}
rows
}
{ p => IO { p._1.close } } // close the ResultSet
}}
{ c => IO(c.close) }
以下提供更多的应用示范:
从一个文件里读取存放亨氏温度的文件名后进行温度转换并存放到celsius.txt中
val convertAll: Process[IO,Unit] = (for {
out <- fileW("celsius.txt").once // out的类型是String => Process[IO,Unit]
file <- lines("fahrenheits.txt") //fahrenheits.txt里保存了一串文件名
_ <- lines(file). //动态打开文件读取温度记录
map(line => fahrenheitToCelsius(line.toDouble)). //温度系统转换
flatMap(celsius => out(celsius.toString)) //输出
} yield ()) drain //继续循环
输出到多个.celsius文件:
val convertMultisink: Process[IO,Unit] = (for {
file <- lines("fahrenheits.txt") //读取文件名称
_ <- lines(file). //打开文件读取温度数据
map(line => fahrenheitToCelsius(line.toDouble)). //温度系统转换
map(_ toString).
to(fileW(file + ".celsius")) //写入文件
} yield ()) drain
我们可以按需要在处理过程中增加处理组件:
val convertMultisink2: Process[IO,Unit] = (for {
file <- lines("fahrenheits.txt")
_ <- lines(file).
filter(!_.startsWith("#")). //过滤#开始字串
map(line => fahrenheitToCelsius(line.toDouble)).
filter(_ > 0). // 过滤0度以下温度
map(_ toString).
to(fileW(file + ".celsius"))
} yield ()) drain
泛函编程(38)-泛函Stream IO:IO Process in action的更多相关文章
- 泛函编程(32)-泛函IO:IO Monad
由于泛函编程非常重视函数组合(function composition),任何带有副作用(side effect)的函数都无法实现函数组合,所以必须把包含外界影响(effectful)副作用不纯代码( ...
- 泛函编程(5)-数据结构(Functional Data Structures)
编程即是编制对数据进行运算的过程.特殊的运算必须用特定的数据结构来支持有效运算.如果没有数据结构的支持,我们就只能为每条数据申明一个内存地址了,然后使用这些地址来操作这些数据,也就是我们熟悉的申明变量 ...
- 泛函编程(35)-泛函Stream IO:IO处理过程-IO Process
IO处理可以说是计算机技术的核心.不是吗?使用计算机的目的就是希望它对输入数据进行运算后向我们输出计算结果.所谓Stream IO简单来说就是对一串按序相同类型的输入数据进行处理后输出计算结果.输入数 ...
- 泛函编程(37)-泛函Stream IO:通用的IO处理过程-Free Process
在上两篇讨论中我们介绍了IO Process:Process[I,O],它的工作原理.函数组合等.很容易想象,一个完整的IO程序是由 数据源+处理过程+数据终点: Source->Process ...
- 泛函编程(36)-泛函Stream IO:IO数据源-IO Source & Sink
上期我们讨论了IO处理过程:Process[I,O].我们说Process就像电视信号盒子一样有输入端和输出端两头.Process之间可以用一个Process的输出端与另一个Process的输入端连接 ...
- 泛函编程(30)-泛函IO:Free Monad-Monad生产线
在上节我们介绍了Trampoline.它主要是为了解决堆栈溢出(StackOverflow)错误而设计的.Trampoline类型是一种数据结构,它的设计思路是以heap换stack:对应传统递归算法 ...
- 泛函编程(27)-泛函编程模式-Monad Transformer
经过了一段时间的学习,我们了解了一系列泛函数据类型.我们知道,在所有编程语言中,数据类型是支持软件编程的基础.同样,泛函数据类型Foldable,Monoid,Functor,Applicative, ...
- 泛函编程(23)-泛函数据类型-Monad
简单来说:Monad就是泛函编程中最概括通用的数据模型(高阶数据类型).它不但涵盖了所有基础类型(primitive types)的泛函行为及操作,而且任何高阶类或者自定义类一旦具备Monad特性就可 ...
- 泛函编程(14)-try to map them all
虽然明白泛函编程风格中最重要的就是对一个管子里的元素进行操作.这个管子就是这么一个东西:F[A],我们说F是一个针对元素A的高阶类型,其实F就是一个装载A类型元素的管子,A类型是相对低阶,或者说是基础 ...
随机推荐
- lua二进制操作函数
由于 Lua 脚本语言本身不支持对数字的二进制操作(例如 与,或,非 等操作),MUSHclient 为此提供了一套专门用于二进制操作的函数,它们都定义在一个“bit”表中,使用时只要requre “ ...
- JavaScript函数后面加不加括号的区别
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8&quo ...
- Leetcode 35 Search Insert Position 二分查找(二分下标)
基础题之一,是混迹于各种难题的基础,有时会在小公司的大题见到,但更多的是见于选择题... 题意:在一个有序数列中,要插入数target,找出插入的位置. 楼主在这里更新了<二分查找综述>第 ...
- JavaScript内存优化
JavaScript内存优化 相对C/C++ 而言,我们所用的JavaScript 在内存这一方面的处理已经让我们在开发中更注重业务逻辑的编写.但是随着业务的不断复杂化,单页面应用.移动HTML5 应 ...
- Android 自定义View及其在布局文件中的使用示例(三):结合Android 4.4.2_r1源码分析onMeasure过程
转载请注明出处 http://www.cnblogs.com/crashmaker/p/3549365.html From crash_coder linguowu linguowu0622@gami ...
- CSS多列布局
× 目录 [1]列宽 [2]列数 [3]列间距[4]列rule[5]跨列[6]列填充[7]多列 前面的话 CSS新增了多列布局特性,可以让浏览器确定何时结束一列和开始下一列,无需任何额外的标记.简单来 ...
- 使用ExifInterface设置Datetime发生的问题
最近在弄一个Android小程序,需要把图像的生成时间设置到Exif的Datetime,用ExifInterface.setAttribute(ExifInterface.TAG_DATETIME,& ...
- 一套后台管理html模版
最近自己需要一套后台管理的模版,然后去网上查找,模版的确很多,但是适合我的并不多.我需要的模版是不会很大,我能够控制代码,样式不要太古朴,最好有点CSS3的效果.最后终于找到一张主页,然后再根据这个主 ...
- Deep learning:四十二(Denoise Autoencoder简单理解)
前言: 当采用无监督的方法分层预训练深度网络的权值时,为了学习到较鲁棒的特征,可以在网络的可视层(即数据的输入层)引入随机噪声,这种方法称为Denoise Autoencoder(简称dAE),由Be ...
- 【小型系统】抽奖系统-使用Java Swing完成
一.需求分析 1. 显示候选人照片和姓名. 2. 可以使用多种模式进行抽奖,包括一人单独抽奖.两人同时抽奖.三人同时抽奖. 3. 一个人可以在不同的批次的抽奖中获取一.二.三等奖,但是不能在同一批次抽 ...