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. H5常用代码:适配方案2

    前面的通过视口做适配的方案由于安卓低版本原生浏览器的存在,在许多场合不尽如人意,会在低版本安卓上出现,不缩放,手动缩放未禁止的问题. 于是出现了第二种适配方案,既然通过视口缩放可以兼容,那为什么不直接 ...

  2. lua二进制操作函数

    由于 Lua 脚本语言本身不支持对数字的二进制操作(例如 与,或,非 等操作),MUSHclient 为此提供了一套专门用于二进制操作的函数,它们都定义在一个“bit”表中,使用时只要requre “ ...

  3. Atitit  从 RGB 到 HSL 或 HSV 的转换

    Atitit  从 RGB 到 HSL 或 HSV 的转换 1.1. 从 RGB 到 HSL 或 HSV 的转换公式与原理1 1.2. public static HSV RGB2HSV(Color ...

  4. atitit。企业的价值观 员工第一 vs 客户第一.docx

    atitit.企业的价值观 员工第一 vs 客户第一.docx 1. 客户第一的说法是错误的,员工优先是正确的,理念与价值观1 1.1. 任何一个组织,应该组织成员优先级要比外部成员高才对1 1.2. ...

  5. Atitit java 二维码识别 图片识别

    Atitit java 二维码识别 图片识别 1.1. 解码11.2. 首先,我们先说一下二维码一共有40个尺寸.官方叫版本Version.11.3. 二维码的样例:21.4. 定位图案21.5. 数 ...

  6. 1027 HTML的初学

       HTML   是一种超文本标记语言内容(Hyper Text  Markup Language)    CSS       网页美化    Javascript    脚本语言(JS)    H ...

  7. 每天一个linux命令(50):crontab命令

    前一天学习了 at 命令是针对仅运行一次的任务,循环运行的例行性计划任务,linux系统则是由 cron (crond) 这个系统服务来控制的.Linux 系统上面原本就有非常多的计划性工作,因此这个 ...

  8. Update: ELCImagePickerController

    March 3rd, 2011 Posted by: Matt Tuzzolo - posted under:Articles » Featured I recently spent some tim ...

  9. 深入理解CSS定位中的堆叠z-index

    × 目录 [1]定义 [2]堆叠规则 [3]堆叠上下文[4]兼容 前面的话 对于所有定位,最后都不免遇到两个元素试图放在同一位置上的情况.显然,其中一个必须盖住另一个.但,如何控制哪个元素放在上层,这 ...

  10. H5游戏开发之Stick Hero

    自从上次发布一个小恐龙游戏以后,到现在10天了,前后又写了3个游戏,挑了一个感觉比较有挑战的游戏和大家分享一下. 效果演示 这是我模拟一个苹果游戏<stick hero>游戏写的一个小游戏 ...