scalaz-stream库的主要设计目标是实现函数式的I/O编程(functional I/O)。这样用户就能使用功能单一的基础I/O函数组合成为功能完整的I/O程序。还有一个目标就是保证资源的安全使用(resource safety):使用scalaz-stream编写的I/O程序能确保资源的安全使用,特别是在完成一项I/O任务后自动释放所有占用的资源包括file handle、memory等等。我们在上一篇的讨论里笼统地解释了一下scalaz-stream核心类型Process的基本情况,不过大部分时间都用在了介绍Process1这个通道类型。在这篇讨论里我们会从实际应用的角度来介绍整个scalaz-stream链条的设计原理及应用目的。我们提到过Process具有Emit/Await/Halt三个状态,而Append是一个链接stream节点的重要类型。先看看这几个类型在scalaz-stream里的定义:

case class Emit[+O](seq: Seq[O]) extends HaltEmitOrAwait[Nothing, O] with EmitOrAwait[Nothing, O]

case class Await[+F[_], A, +O](
req: F[A]
, rcv: (EarlyCause \/ A) => Trampoline[Process[F, O]] @uncheckedVariance
, preempt : A => Trampoline[Process[F,Nothing]] @uncheckedVariance = (_:A) => Trampoline.delay(halt:Process[F,Nothing])
) extends HaltEmitOrAwait[F, O] with EmitOrAwait[F, O] case class Halt(cause: Cause) extends HaltEmitOrAwait[Nothing, Nothing] with HaltOrStep[Nothing, Nothing] case class Append[+F[_], +O](
head: HaltEmitOrAwait[F, O]
, stack: Vector[Cause => Trampoline[Process[F, O]]] @uncheckedVariance
) extends Process[F, O]

我们看到Process[F,O]被包嵌在Trampoline类型里,所以Process是通过Trampoline来实现函数结构化的,可以有效解决大量stream运算堆栈溢出问题(StackOverflowError)。撇开Trampoline等复杂的语法,以上类型可以简化成以下理论结构:

 rait Process[+F[_],+O]
case object Cause case class Emit[O](out: O) extends Process[Nothing, O] case class Halt(cause: Cause) extends Process[Nothing,Nothing] case class Await[+F[_],E,+O](
req: F[E],
rcv: E => Process[F,O],
preempt: E => Process[F,Nothing] = Halt) extends Process[F,O] case class Append[+F[_],+O](
head: Process[F,O],
stack: Vector[Cause => Process[F,O]]) extends Process[F,O]

我们来说明一下:

Process[F[_],O]:从它的类型可以推断出scalaz-stream可以在输出O类型元素的过程中进行可能含副作用的F类型运算。

Emit[O](out: O):发送一个O类型元素;不可能进行任何附加运算

Halt(cause: Cause):停止发送;cause是停止的原因:End-完成发送,Err-出错终止,Kill-强行终止

Await[+F[_],E,+O]:这个是运算流的核心Process状态。先进行F运算req,得出结果E后输入函数rcv转换到下一个Process状态,完成后执行preempt这个事后清理函数。这不就是个flatMap函数结构版嘛。值得注意的是E类型是个内部类型,由F运算产生后输入rcv后就不再引用了。我们可以在preepmt函数里进行资源释放。如果我们需要构建一个运算流,看来就只有使用这个Await类型了

Append[+F[_],+O]:Append是一个Process[F,O]链接类型。首先它不但担负了元素O的传送,更重要的是它还可以把上一节点的F运算传到下一个节点。这样才能在下面节点时运行对上一个节点的事后处置函数(finalizer)。Append可以把多个节点结成一个大节点:head是第一个节点,stack是一串函数,每个函数接受上一个节点完成状态后运算出下一个节点状态

一个完整的scalaz-stream由三个类型的节点组成Source(源点)/Transducer(传换点)/Sink(终点)。节点间通过Await或者Append来链接。我们再来看看Source/Transducer/Sink的类型款式:

上游:Source       >>> Process0[O]   >>> Process[F[_],O]

中游:Transduce    >>> Process1[I,O]

下游:Sink/Channel >>> Process[F[_],O => F[Unit]], Channel >>> Process[F[_],I => F[O]]

我们可以用一个文件处理流程来描述完整scalaz-stream链条的作用:

Process[F[_],O],用F[O]方式读取文件中的O值,这时F是有副作用的

>>> Process[I,O],I代表从文件中读取的原始数据,O代表经过筛选、处理产生的输出数据

>>> O => F[Unit]是一个不返回结果的函数,代表对输入的O类型数据进行F运算,如把O类型数据存写入一个文件

/>> I => F[O]是个返回结果的函数,对输入I进行F运算后返回O,如把一条记录写入数据库后返回写入状态

以上流程简单描述:从文件中读出数据->加工处理读出数据->写入另一个文件。虽然从描述上看起来很简单,但我们的目的是资源安全使用:无论在任何终止情况下:正常读写、中途强行停止、出错终止,scalaz-stream都会主动关闭开启的文件、停止使用的线程、释放占用的内存等其它资源。这样看来到不是那么简单了。我们先试着分析Source/Transducer/Sink这几种类型的作用:

 import Process._
emit() //> res0: scalaz.stream.Process0[Int] = Emit(Vector(0))
emitAll(Seq(,,)) //> res1: scalaz.stream.Process0[Int] = Emit(List(1, 2, 3))
Process(,,) //> res2: scalaz.stream.Process0[Int] = Emit(WrappedArray(1, 2, 3))
Process() //> res3: scalaz.stream.Process0[Nothing] = Emit(List())

以上都是Process0的构建方式,也算是数据源。但它们只是代表了内存中的一串值,对我们来说没什么意义,因为我们希望从外设获取这些值,比如从文件或者数据库里读取数据,也就是说需要F运算效果。Process0[O] >>> Process[Nothing,O],而我们需要的是Process[F,O]。那么我们这样写如何呢?

 val p: Process[Task,Int] = emitAll(Seq(,,))
//> p : scalaz.stream.Process[scalaz.concurrent.Task,Int] = Emit(List(1, 2, 3)) emitAll(Seq(,,)).toSource
//> res4: scalaz.stream.Process[scalaz.concurrent.Task,Int] = Emit(List(1, 2, 3))

类型倒是匹配了,但表达式Emit(...)里没有任何Task的影子,这个无法满足我们对Source的需要。看来只有以下这种方式了:

 await(Task.delay{})(emit)
//> res5: scalaz.stream.Process[scalaz.concurrent.Task,Int] = Await(scalaz.concurrent.Task@57855c9a,<function1>,<function1>)
eval(Task.delay{})
//> res6: scalaz.stream.Process[scalaz.concurrent.Task,Int] = Await(scalaz.concurrent.Task@63e2203c,<function1>,<function1>)

现在不但类型匹配,而且表达式里还包含了Task运算。我们通过Task.delay可以进行文件读取等带有副作用的运算,这是因为Await将会运行req:F[E] >>> Task[Int]。这正是我们需要的Source。那我们能不能用这个Source来发出一串数据呢?

 def emitSeq[A](xa: Seq[A]):Process[Task,A] =
xa match {
case h :: t => await(Task.delay {h})(emit) ++ emitSeq(t)
case Nil => halt
} //> emitSeq: [A](xa: Seq[A])scalaz.stream.Process[scalaz.concurrent.Task,A]
val es1 = emitSeq(Seq(,,)) //> es1 : scalaz.stream.Process[scalaz.concurrent.Task,Int] = Append(Await(scalaz.concurrent.Task@2d6eabae,<function1>,<function1>),Vector(<function1>))
val es2 = emitSeq(Seq("a","b","c")) //> es2 : scalaz.stream.Process[scalaz.concurrent.Task,String] = Append(Await(
scalaz.concurrent.Task@4e7dc304,<function1>,<function1>),Vector(<function1>))
es1.runLog.run //> res7: Vector[Int] = Vector(1, 2, 3)
es2.runLog.run //> res8: Vector[String] = Vector(a, b, c)

以上示范中我们用await运算了Task,然后返回了Process[Task,?],一个可能带副作用运算的Source。实际上我们在很多情况下都需要从外部的源头用Task来获取一些数据,通常这些数据源都对数据获取进行了异步(asynchronous)运算处理,然后通过callback方式来提供这些数据。我们可以用Task.async函数来把这些callback函数转变成Task,下一步我们只需要用Process.eval或者await就可以把这个Task升格成Process[Task,?]。我们先看个简单的例子:假如我们用scala.concurrent.Future来进行异步数据读取,可以这样把Future转换成Process:

 def getData(dbName: String): Task[String] = Task.async { cb =>
import scala.concurrent._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Success,Failure}
Future { s"got data from $dbName" }.onComplete {
case Success(a) => cb(a.right)
case Failure(e) => cb(e.left)
}
} //> getData: (dbName: String)scalaz.concurrent.Task[String]
val procGetData = eval(getData("MySQL")) //> procGetData : scalaz.stream.Process[scalaz.concurrent.Task,String] = Await(scalaz.concurrent.Task@dd3b207,<function1>,<function1>)
procGetData.runLog.run //> res9: Vector[String] = Vector(got data from MySQL)

我们也可以把java的callback转变成Task:

   import com.ning.http.client._
val asyncHttpClient = new AsyncHttpClient() //> asyncHttpClient : com.ning.http.client.AsyncHttpClient = com.ning.http.client.AsyncHttpClient@245b4bdc
def get(s: String): Task[Response] = Task.async[Response] { callback =>
asyncHttpClient.prepareGet(s).execute(
new AsyncCompletionHandler[Unit] {
def onCompleted(r: Response): Unit = callback(r.right) def onError(e: Throwable): Unit = callback(e.left)
}
)
} //> get: (s: String)scalaz.concurrent.Task[com.ning.http.client.Response]
val prcGet = Process.eval(get("http://sina.com"))
//> prcGet : scalaz.stream.Process[scalaz.concurrent.Task,com.ning.http.client.Response] = Await(scalaz.concurrent.Task@222545dc,<function1>,<function1>)
prcGet.run.run //> 12:25:27.852 [New I/O worker #1] DEBUG c.n.h.c.p.n.r.NettyConnectListener -Request using non cached Channel '[id: 0x23fa1307, /192.168.200.3:50569 =>sina.com/66.102.251.33:80]':

如果直接按照scalaz Task callback的类型款式 def async(callback:(Throwable \/ Unit) => Unit):

   def read(callback: (Throwable \/ Array[Byte]) => Unit): Unit = ???
//> read: (callback: scalaz.\/[Throwable,Array[Byte]] => Unit)Unit
val t: Task[Array[Byte]] = Task.async(read) //> t : scalaz.concurrent.Task[Array[Byte]] = scalaz.concurrent.Task@1a677343
val t2: Task[Array[Byte]] = for {
bytes <- t
moarBytes <- t
} yield (bytes ++ moarBytes) //> t2 : scalaz.concurrent.Task[Array[Byte]] = scalaz.concurrent.Task@15de0b3c
val prct2 = Process.eval(t2) //> prct2 : scalaz.stream.Process[scalaz.concurrent.Task,Array[Byte]] = Await(scalaz.concurrent.Task@15de0b3c,<function1>,<function1>) def asyncRead(succ: Array[Byte] => Unit, fail: Throwable => Unit): Unit = ???
//> asyncRead: (succ: Array[Byte] => Unit, fail: Throwable => Unit)Unit
val t3: Task[Array[Byte]] = Task.async { callback =>
asyncRead(b => callback(b.right), err => callback(err.left))
} //> t3 : scalaz.concurrent.Task[Array[Byte]] = scalaz.concurrent.Task@489115ef
val t4: Task[Array[Byte]] = t3.flatMap(b => Task(b))
//> t4 : scalaz.concurrent.Task[Array[Byte]] = scalaz.concurrent.Task@3857f613
val prct4 = Process.eval(t4) //> prct4 : scalaz.stream.Process[scalaz.concurrent.Task,Array[Byte]] = Await(scalaz.concurrent.Task@3857f613,<function1>,<function1>)

我们也可以用timer来产生Process[Task,A]:

   import scala.concurrent.duration._
implicit val scheduler = java.util.concurrent.Executors.newScheduledThreadPool()
//> scheduler : java.util.concurrent.ScheduledExecutorService = java.util.concurrent.ScheduledThreadPoolExecutor@516be40f[Running, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0]
val fizz = time.awakeEvery(.seconds).map(_ => "fizz")
//> fizz : scalaz.stream.Process[scalaz.concurrent.Task,String] = Await(scalaz.concurrent.Task@5762806e,<function1>,<function1>)
val fizz3 = fizz.take() //> fizz3 : scalaz.stream.Process[scalaz.concurrent.Task,String] = Append(Halt(End),Vector(<function1>))
fizz3.runLog.run //> res9: Vector[String] = Vector(fizz, fizz, fizz)

Queue、Top和Signal都可以作为带副作用数据源的构建器。我们先看看Queue是如何产生数据源的:

   type BigStringResult = String
val qJobResult = async.unboundedQueue[BigStringResult]
//> qJobResult : scalaz.stream.async.mutable.Queue[demo.ws.blogStream.BigStringResult] = scalaz.stream.async.mutable.Queue$$anon$1@25d250c6
def longGet(jobnum: Int): BigStringResult = {
Thread.sleep()
s"Some large data sets from job#${jobnum}"
} //> longGet: (jobnum: Int)demo.ws.blogStream.BigStringResult // multi-tasking
val start = System.currentTimeMillis() //> start : Long = 1468556250797
Task.fork(qJobResult.enqueueOne(longGet())).unsafePerformAsync{case _ => ()}
Task.fork(qJobResult.enqueueOne(longGet())).unsafePerformAsync{case _ => ()}
Task.fork(qJobResult.enqueueOne(longGet())).unsafePerformAsync{case _ => ()}
val timemill = System.currentTimeMillis() - start
//> timemill : Long = 17
Thread.sleep()
qJobResult.close.run
val bigData = {
//multi-tasking
val j1 = qJobResult.dequeue
val j2 = qJobResult.dequeue
val j3 = qJobResult.dequeue
for {
r1 <- j1
r2 <- j2
r3 <- j3
} yield r1 + ","+ r2 + "," + r3
} //> bigData : scalaz.stream.Process[[x]scalaz.concurrent.Task[x],String] = Await(scalaz.concurrent.Task@778d1062,<function1>,<function1>) bigData.runLog.run //> res9: Vector[String] = Vector(Some large data sets from job#2,Some large data sets from job#3,Some large data sets from job#1)

再看看Topic示范:

 import scala.concurrent._
import scala.concurrent.duration._
import scalaz.stream.async.mutable._
import scala.concurrent.ExecutionContext.Implicits.global
val sharedData: Topic[BigStringResult] = async.topic()
//> sharedData : scalaz.stream.async.mutable.Topic[demo.ws.blogStream.BigStringResult] = scalaz.stream.async.package$$anon$1@797badd3
val subscriber = sharedData.subscribe.runLog //> subscriber : scalaz.concurrent.Task[Vector[demo.ws.blogStream.BigStringResult]] = scalaz.concurrent.Task@226a82c4
val otherThread = future {
subscriber.run // Added this here - now subscriber is really attached to the topic
} //> otherThread : scala.concurrent.Future[Vector[demo.ws.blogStream.BigStringResult]] = List()
// Need to give subscriber some time to start up.
// I doubt you'd do this in actual code. // topics seem more useful for hooking up things like
// sensors that produce a continual stream of data, // and where individual values can be dropped on
// floor.
Thread.sleep() sharedData.publishOne(longGet()).run // don't just call publishOne; need to run the resulting task
sharedData.close.run // Don't just call close; need to run the resulting task // Need to wait for the output
val result = Await.result(otherThread, Duration.Inf)
//> result : Vector[demo.ws.blogStream.BigStringResult] = Vector(Some large data sets from job#1)

以上对可能带有副作用的Source的各种产生方法提供了解释和示范。scalaz-stream的其他类型节点将在下面的讨论中深入介绍。

Scalaz(47)- scalaz-stream: 深入了解-Source的更多相关文章

  1. Scalaz(52)- scalaz-stream: 并行运算-parallel processing concurrently by merging

    如果scalaz-stream真的是一个实用的数据流编程工具库的话,那它应该能处理同时从多个数据源获取数据以及把数据同时送到多个终点(Sink),最重要的是它应该可以实现高度灵活的多线程运算.但是:我 ...

  2. Scalaz(23)- 泛函数据结构: Zipper-游标定位

    外面沙尘滚滚一直向北去了,意识到年关到了,码农们都回乡过年去了,而我却留在这里玩弄“拉链”.不要想歪了,我说的不是裤裆拉链而是scalaz Zipper,一种泛函数据结构游标(cursor).在函数式 ...

  3. Scalaz(8)- typeclass:Monoid and Foldable

    Monoid是种最简单的typeclass类型.我们先看看scalaz的Monoid typeclass定义:scalaz/Monoid.scala trait Monoid[F] extends S ...

  4. Scalaz(6)- typeclass:Functor-just map

    Functor是范畴学(Category theory)里的概念.不过无须担心,我们在scala FP编程里并不需要先掌握范畴学知识的.在scalaz里,Functor就是一个普通的typeclass ...

  5. Scalaz(45)- concurrency :Task-函数式多线程编程核心配件

    我们在上一节讨论了scalaz Future,我们说它是一个不完善的类型,最起码没有完整的异常处理机制,只能用在构建类库之类的内部环境.如果scalaz在Future类定义中增加异常处理工具的话,用户 ...

  6. Scalaz(26)- Lens: 函数式不可变对象数据操作方式

    scala中的case class是一种特殊的对象:由编译器(compiler)自动生成字段的getter和setter.如下面的例子: case class City(name:String, pr ...

  7. Scalaz(22)- 泛函编程思维: Coerce Monadic Thinking

    马上进入新的一年2016了,来点轻松点的内容吧.前面写过一篇关于用Reader实现依赖注入管理的博文(Scalaz(16)- Monad:依赖注入-Dependency Injection By Re ...

  8. Scalaz(20)-Monad: Validation-Applicative版本的Either

    scalaz还提供了个type class叫Validation.乍看起来跟\/没什么分别.实际上这个Validation是在\/的基础上增加了Applicative功能,就是实现了ap函数.通过Ap ...

  9. Scalaz(18)- Monad: ReaderWriterState-可以是一种简单的编程语言

    说道FP,我们马上会联想到Monad.我们说过Monad的代表函数flatMap可以把两个运算F[A],F[B]连续起来,这样就可以从程序的意义上形成一种串型的流程(workflow).更直白的讲法是 ...

  10. Scalaz(17)- Monad:泛函状态类型-State Monad

    我们经常提到函数式编程就是F[T].这个F可以被视为一种运算模式.我们是在F运算模式的壳子内对T进行计算.理论上来讲,函数式程序的运行状态也应该是在这个运算模式壳子内的,也是在F[]内更新的.那么我们 ...

随机推荐

  1. java连接数据库的模糊查询

    1:模糊查询是比较常见的一种查询方式,例如在订单表中,包含有订单的具体日期.如果要查询某年某月的订单信息,最好的方式就是使用模糊查询.进行模糊查询需要使用关键字LIKE.在使用LIKE关键字进行模糊查 ...

  2. Android 坐标系和 MotionEvent 分析、滑动

    1.Android坐标系 在Android中,屏幕最左上角的顶点作为Android坐标系的原点,这个点向左是X轴正方向,这个点向下是Y轴正方向. 系统提供了getLocationOnScreen(in ...

  3. linux执行sh脚本文件命令

    linux执行sh脚本文件命令 很多时候需要多个命令来完成一项工作,而这个工作又常常是重复的,这个时候我们自然会想到将这些命令写成sh脚本,下次执行下这个脚本一切就都搞定了,下面就是发布代码的一个脚本 ...

  4. 快速入门系列--WCF--06并发限流、可靠会话和队列服务

    这部分将介绍一些相对深入的知识点,包括通过并发限流来保证服务的可用性,通过可靠会话机制保证会话信息的可靠性,通过队列服务来解耦客户端和服务端,提高系统的可服务数量并可以起到削峰的作用,最后还会对之前的 ...

  5. CSS学习目录

    前面的话 CSS是前端工程师的基本功,但好多执迷于学习javascript的人的基本功并不扎实.可能一些人从w3school网站匆匆过了一遍,只是对CSS常用概念有一些表面上的理解,就一头扎进java ...

  6. poj3249Test for Job(记忆化搜索)

    /* 题意:给一个DAG图,n个节点,每个节点都对应一个值,入度为零的点走到出度为零的点,计算所有可能路径 经过节点值的和最大! 思路:记忆话搜索:也就是如果我们搜索到某一个节点的时候发现该节点已经存 ...

  7. poj 2226 Muddy Fields(合理建图+二分匹配)

    /* 题意:用木板盖住泥泞的地方,不能盖住草.木板任意长!可以重叠覆盖! '*'表示泥泞的地方,'.'表示草! 思路: 首先让我们回忆一下HDU 2119 Matrix这一道题,一个矩阵中只有0, 1 ...

  8. Cocos2d-x 3.2 学习笔记(十二)TimberMan!疯狂伐木工!

    学习cocos2dx有一段时间了,试着做了2048游戏,最近又发现个经典游戏,啥也不说果断开工做自己的游戏——TimberMan! 首先说明:游戏资源摘自同类游戏,感谢这些游戏的资源让我完成自己的开发 ...

  9. nodejs操作mongodb

    一.下载地址 https://www.mongodb.com/download-center#community 二.控制台操作mongodb 1.安装完后添加环境变量. 2.在某个根目录下新建dat ...

  10. 电路相关知识--读<<继电器是如何成为CPU的>>

    电路相关知识–读<<继电器是如何成为CPU的>> */--> *///--> *///--> 电路相关知识–读<<继电器是如何成为CPU的> ...