在前面的几节讨论里我们终于得出了一个概括又通用的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的更多相关文章

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

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

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

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

  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. 泛函编程(36)-泛函Stream IO:IO数据源-IO Source & Sink

    上期我们讨论了IO处理过程:Process[I,O].我们说Process就像电视信号盒子一样有输入端和输出端两头.Process之间可以用一个Process的输出端与另一个Process的输入端连接 ...

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

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

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

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

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

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

  9. 泛函编程(14)-try to map them all

    虽然明白泛函编程风格中最重要的就是对一个管子里的元素进行操作.这个管子就是这么一个东西:F[A],我们说F是一个针对元素A的高阶类型,其实F就是一个装载A类型元素的管子,A类型是相对低阶,或者说是基础 ...

随机推荐

  1. Atitit.加密算法ati Aes的框架设计v2.2

    Atitit.加密算法ati Aes的框架设计v2.2 版本进化1 V2.2   add def decode key api1 v1版本1 Aes的历史2 Atitit.加密算法 des  aes  ...

  2. iOS-----用LLDB调试,让移动开发更简单(二)

    image lookup –address 当我们有一个地址,想查找这个地址具体对应的文件位置,可以使用image lookup --address,简写为image lookup -a e.g: 当 ...

  3. 请求一个action,将图片的二进制字节字符串在视图页面以图片形式输出

    有些时候需要将二进制图片字节在发送浏览器以图片形式显示: 下面是一些示例代码: 控制器: /// <summary> /// 将图片的二进制字节字符串在视图页面以图片形式输出 /// &l ...

  4. DOM_03之元素及常用对象

    1.修改样式:访问内联样式:elem.style.css属性名:获得其他:var style=getComputerStyle(elem):*(* 获得焦点onfocus:失去焦点onblur:): ...

  5. 静态(static)代码块、构造代码块、构造函数、父类子类执行顺序

    静态代码块:static修饰的代码块. 在类加载-初始化的时候进行,主要目的是给变量赋予初始值 构造代码块:直接在类中定义且没有加static关键字的代码块称为构造代码块. java会把构造代码块放到 ...

  6. 25.按要求编写一个Java应用程序: (1)编写一个矩形类Rect,包含: 两个属性:矩形的宽width;矩形的高height。 两个构造方法: 1.一个带有两个参数的构造方法,用于将width和height属性初化; 2.一个不带参数的构造方法,将矩形初始化为宽和高都为10。 两个方法: 求矩形面积的方法area() 求矩形周长的方法perimeter() (2)通过继承Rect类编写一个具有

    package zhongqiuzuoye; //自己写的方法 public class Rect { public double width; public double height; Rect( ...

  7. Netty学习四:Channel

    1. Channel Channel是Netty的核心概念之一,它是Netty网络通信的主体,由它负责同对端进行网络通信.注册和数据操作等功能. 1.1 工作原理 如上图所示: 一旦用户端连接成功,将 ...

  8. The Triumph Of Bio-logic

    理性技术的局限 机械逻辑 人造机械的构成逻辑,比如说钟表的制作原理.是一种可被人类掌握的,可推演的理论. 生物逻辑 一个有机系统的构成逻辑.比如说草原.细胞.大脑等这些系统的存在所依托的逻辑,我们成为 ...

  9. Office英语学习好帮手

    Office提供了强大实用的英语学习助手,它可以自动翻译中英文,还可以显示详尽的解释帮助信息,当然标准的发音也是必不可少的.如何启动屏幕取词翻译功能呢?如何让office自动取词并翻译呢?如何收听单词 ...

  10. Sybase 数据库新增用户,赋权

    Sybase数据库, 需求:新增用户user1,赋予对原数据库中表t_jingyu的查询权限 数据库原用户登陆 isql -U用户 -P密码 -S服务名 sp_addlogin 'user1','us ...