fs2是scalaz-stream的最新版本,沿用了scalaz-stream被动式(pull model)数据流原理但采用了全新的实现方法。fs2比较scalaz-stream而言具备了:更精简的基础组件(combinator)、更安全的类型、资源使用(type safe, resource safety)、更高的运算效率。由于fs2基本沿用了scalaz-stream的原理,所以我们会在下面的讨论里着重介绍fs2的使用。根据fs2的官方文件,fs2具备了以下新的特点:

1、完全不含任何外部依赖(third-party dependency)

2、流元素增加了节组(chunk)类型和相关的操作方法

3、fs2不再只局限于Task一种副作用运算方式(effect)。用户可以提供自己的effect类型

4、更精简的流转换组件(stream transformation primitives)

5、增加了更多并行运算组件(concurrent primitives)

6、通过bracket函数增强了资源使用安全,特别是异线程资源占用的事后处理过程。用onFinalize取代了onComplete

7、stream状态转换采用了全新的实现方式,使用了新的数据结构:Pull

8、Stream取代了Process。fs2中再没有Process1、Tee、Wye、Channel这些类型别名,取而代之的是:

  • type Pipe[F,A,B] = Stream[F,A] => Stream[F,B]
  • type Pipe2[F,A,B,C] = (Stream[F,A], Stream[F,B]) => Stream[F,C]
  • Pipe 替代了 Channel 和 Process1
  • Pipe2 替代了 Tee 和 Wye

下面我们来看看fs2的一些基本操作:

 Stream()                       //> res0: fs2.Stream[Nothing,Nothing] = Segment(Emit(Chunk()))
Stream(,,) //> res1: fs2.Stream[Nothing,Int] = Segment(Emit(Chunk(1, 2, 3)))
Stream.emit() //> res2: fs2.Stream[Nothing,Int] = Segment(Emit(Chunk(4)))
Stream.emits(Seq(,,)) //> res3: fs2.Stream[Nothing,Int] = Segment(Emit(Chunk(1, 2, 3)))

Stream的类型款式是:Stream[F[_],A]。从上面的例子我们看到所有的F[_]都是Nothing,我们称这样的流为纯数据流(pure stream)。再值得注意的是每个流构建都形成了一个Chunk,代表一节元素。fs2增加了Chunk类型来提高数据元素处理效率。这是fs2的一项新功能。

我们可以用toList或者toVector来运算纯数据流中的元素值:

 Stream.emits(Seq(,,)).toList        //> res3: List[Int] = List(1, 2, 3)
Stream.emits(Seq(,,)).toVector //> res4: Vector[Int] = Vector(1, 2, 3)

纯数据流具备了许多与List相似的操作函数:

 (Stream(,,) ++ Stream(,)).toList             //> res5: List[Int] = List(1, 2, 3, 4, 5)
Stream(,,).map { _ + }.toList //> res6: List[Int] = List(2, 3, 4)
Stream(,,).filter { _ % == }.toList //> res7: List[Int] = List(2)
Stream(,,).fold()(_ + _).toList //> res8: List[Int] = List(6)
Stream(None,Some(),Some(),None).collect {
case None =>
case Some(i) => i
}.toList //> res9: List[Int] = List(0, 1, 3, 0)
Stream.range(,).intersperse().toList //> res10: List[Int] = List(1, 42, 2, 42, 3, 42, 4)
Stream(,,).flatMap {x => Stream(x,x)}.toList //> res11: List[Int] = List(1, 1, 2, 2, 3, 3)
Stream(,,).repeat.take().toList //> res12: List[Int] = List(1, 2, 3, 1, 2)

以上都是一些基本的List操作函数示范。

我们知道,纯数据流就是scalaz-stream里的Process1,即transducer,是负责对流进行状态转换的。在fs2里transducer就是Pipe(也是channel),我们一般用through来连接transducer。上面示范中的take,filter等都是transducer,我们可以在object pipe里找到这些函数:

 object pipe {
...
/** Drop `n` elements of the input, then echo the rest. */
def drop[F[_],I](n: Long): Stream[F,I] => Stream[F,I] =
_ pull (h => Pull.drop(n)(h) flatMap Pull.echo)
...
/** Emits `true` as soon as a matching element is received, else `false` if no input matches */
def exists[F[_], I](p: I => Boolean): Stream[F, I] => Stream[F, Boolean] =
_ pull { h => Pull.forall[F,I](!p(_))(h) flatMap { i => Pull.output1(!i) }} /** Emit only inputs which match the supplied predicate. */
def filter[F[_], I](f: I => Boolean): Stream[F,I] => Stream[F,I] =
mapChunks(_ filter f) /** Emits the first input (if any) which matches the supplied predicate, to the output of the returned `Pull` */
def find[F[_],I](f: I => Boolean): Stream[F,I] => Stream[F,I] =
_ pull { h => Pull.find(f)(h).flatMap { case o #: h => Pull.output1(o) }} /**
* Folds all inputs using an initial value `z` and supplied binary operator,
* and emits a single element stream.
*/
def fold[F[_],I,O](z: O)(f: (O, I) => O): Stream[F,I] => Stream[F,O] =
_ pull { h => Pull.fold(z)(f)(h).flatMap(Pull.output1) }
...
/** Emits all elements of the input except the first one. */
def tail[F[_],I]: Stream[F,I] => Stream[F,I] =
drop() /** Emit the first `n` elements of the input `Handle` and return the new `Handle`. */
def take[F[_],I](n: Long): Stream[F,I] => Stream[F,I] =
_ pull Pull.take(n)
...

我们可以用through来连接这些transducer:

 Stream(,,).repeat
.throughPure(pipe.take())
.throughPure(pipe.filter(_ % == ))
.toList //> res13: List[Int] = List(2, 2, 2)

以上的throughPure等于是through + pure。Pure是没有任何作用的F[_],是专门为帮助compiler进行类型推导的类型。其实我们可以用pure先把纯数据流升格后再用through:

 Stream(,,).repeat.pure
.through(pipe.take())
.through(pipe.filter(_ % == ))
.toList //> res14: List[Int] = List(2, 2, 2)

这时compiler不再出错误信息了。在fs2 pipe对象里的函数通过方法注入或者类型继承变成了Stream的自身函数,所以我们也可以直接在Stream类型上使用这些transducer:

 Stream(,,).repeat.take().filter(_ %  == ).toList
//> res15: List[Int] = List(2, 2, 2)

我们在前面提到过fs2使用了全新的方法和数据类型来实现transducer。transducer的类型是Pipe,即:

type Pipe[F[_],-I,+O] = Stream[F,I] => Stream[F,O]

我们看到Pipe就是一个Function1的类型别名,一个lambda:提供一个Stream[F,I],返回Stream[F,O]。那么在fs2里是如何读取一个Stream[F,I]里的元素呢?我们前面提到是通过一个新的数据结构Pull来实现的,先来看看fs2是如何实现Stream >> Pull >> Stream转换的:

 val pll = Stream(,,).pure.open    //> pll  : fs2.Pull[fs2.Pure,Nothing,fs2.Stream.Handle[fs2.Pure,Int]] = fs2.Pull
de5031f
val strm = pll.close //> strm : fs2.Stream[fs2.Pure,Nothing] = evalScope(Scope(Bind(Eval(Snapshot),<
function1>))).flatMap(<function1>)

对一个Stream施用open后得到一个Pull类型。pll是个Pull数据结构,它的类型定义如下:

class Pull[+F[_],+O,+R](private[fs2] val get: Free[P[F,O]#f,Option[Either[Throwable,R]]])

在Pull的类型参数中F是一个运算,O代表输出元素类型,R代表Pull里的数据资源。我们可以从R读取元素。在上面的例子里pll的R值是个Handle类型。这个类型里应该提供了读取元素的方法:

implicit class HandleOps[+F[_],+A](h: Handle[F,A]) {
def push[A2>:A](c: Chunk[A2])(implicit A2: RealSupertype[A,A2]): Handle[F,A2] =
self.push(h: Handle[F,A2])(c)
def push1[A2>:A](a: A2)(implicit A2: RealSupertype[A,A2]): Handle[F,A2] =
self.push1(h: Handle[F,A2])(a)
def #:[H](hd: H): Step[H, Handle[F,A]] = Step(hd, h)
def await: Pull[F, Nothing, Step[Chunk[A], Handle[F,A]]] = self.await(h)
def await1: Pull[F, Nothing, Step[A, Handle[F,A]]] = self.await1(h)
def awaitNonempty: Pull[F, Nothing, Step[Chunk[A], Handle[F,A]]] = Pull.awaitNonempty(h)
def echo1: Pull[F,A,Handle[F,A]] = Pull.echo1(h)
def echoChunk: Pull[F,A,Handle[F,A]] = Pull.echoChunk(h)
def peek: Pull[F, Nothing, Step[Chunk[A], Handle[F,A]]] = self.peek(h)
def peek1: Pull[F, Nothing, Step[A, Handle[F,A]]] = self.peek1(h)
def awaitAsync[F2[_],A2>:A](implicit S: Sub1[F,F2], F2: Async[F2], A2: RealSupertype[A,A2]):
Pull[F2, Nothing, AsyncStep[F2,A2]] = self.awaitAsync(Sub1.substHandle(h))
def await1Async[F2[_],A2>:A](implicit S: Sub1[F,F2], F2: Async[F2], A2: RealSupertype[A,A2]):
Pull[F2, Nothing, AsyncStep1[F2,A2]] = self.await1Async(Sub1.substHandle(h))
def covary[F2[_]](implicit S: Sub1[F,F2]): Handle[F2,A] = Sub1.substHandle(h)
} implicit class HandleInvariantEffectOps[F[_],+A](h: Handle[F,A]) {
def invAwait1Async[A2>:A](implicit F: Async[F], A2: RealSupertype[A,A2]):
Pull[F, Nothing, AsyncStep1[F,A2]] = self.await1Async(h)
def invAwaitAsync[A2>:A](implicit F: Async[F], A2: RealSupertype[A,A2]):
Pull[F, Nothing, AsyncStep[F,A2]] = self.awaitAsync(h)
def receive1[O,B](f: Step[A,Handle[F,A]] => Pull[F,O,B]): Pull[F,O,B] = h.await1.flatMap(f)
def receive[O,B](f: Step[Chunk[A],Handle[F,A]] => Pull[F,O,B]): Pull[F,O,B] = h.await.flatMap(f)
}

果然在Handle提供的函数里有await,receive等这些读取函数。我们试着来实现一个简单的transducer:一个filter函数:

 import scala.language.higherKinds
def myFilter[F[_],A](f: A => Boolean): Pipe[F, A, A] = {
def go(h: Stream.Handle[F,A]): Pull[F,A,Unit] = {
// h.receive1 {case Step(a,h) => if(f(a)) Pull.output1(a) >> go(h) else go(h)}
h.await1.flatMap { case Step(a,h) => if(f(a)) Pull.output1(a) >> go(h) else go(h)}
}
// sin => sin.open.flatMap {h => go(h)}.close
sin => sin.pull(go _)
} //> myFilter: [F[_], A](f: A => Boolean)fs2.Pipe[F,A,A] Stream.range(,).pure.through(myFilter(_ % == )).toList
//> res17: List[Int] = List(0, 2, 4, 6, 8)

我们从Pull里用await1或者receive1把一个Step数据结构从Handle里扯(pull)出来然后再output到Pull结构里。把这个Pull close后得到我们需要的Stream。我们把例子使用的类型及函数款式陈列在下面:

type Pipe[F[_],-I,+O] = Stream[F,I] => Stream[F,O]

def await1[F[_],I]: Handle[F,I] => Pull[F,Nothing,Step[I,Handle[F,I]]] = {...}

def receive1[F[_],I,O,R](f: Step[I,Handle[F,I]] => Pull[F,O,R]): Handle[F,I] => Pull[F,O,R] =
_.await1.flatMap(f) def pull[F[_],F2[_],A,B](s: Stream[F,A])(using: Handle[F,A] => Pull[F2,B,Any])(implicit S: Sub1[F,F2])
: Stream[F2,B] =
Pull.close { Sub1.substPull(open(s)) flatMap (h => Sub1.substPull(using(h))) }

再示范另一个Pipe的实现:take

 def myTake[F[_],A](n: Int): Pipe[F,A,A] = {
def go(n: Int): Stream.Handle[F,A] => Pull[F,A,Unit] = h => {
if (n <= ) Pull.done
else h.receive1 { case a #: h => Pull.output1(a).flatMap{_ => go(n-)(h)}}
}
sin => sin.pull(go(n))
} //> myTake: [F[_], A](n: Int)fs2.Pipe[F,A,A]
Stream.range(,).pure.through(myTake()).toList //> res18: List[Int] = List(0, 1, 2)

我们曾经提过fs2功能提升的其中一项是增加了节组(Chunk)数据类型和相关的操作函数。Chunk是fs2内部使用的一种集合,这样fs2就可以一节一节(by chunks)来处理数据了。Chunk本身具备了完整的集合函数:

/**
* Chunk represents a strict, in-memory sequence of `A` values.
*/
trait Chunk[+A] { self =>
def size: Int
def uncons: Option[(A, Chunk[A])] =
if (size == ) None
else Some(apply() -> drop())
def apply(i: Int): A
def copyToArray[B >: A](xs: Array[B]): Unit
def drop(n: Int): Chunk[A]
def take(n: Int): Chunk[A]
def filter(f: A => Boolean): Chunk[A]
def foldLeft[B](z: B)(f: (B,A) => B): B
def foldRight[B](z: B)(f: (A,B) => B): B
def indexWhere(p: A => Boolean): Option[Int] = {
val index = iterator.indexWhere(p)
if (index < ) None else Some(index)
}
def isEmpty = size ==
def toArray[B >: A: ClassTag]: Array[B] = {
val arr = new Array[B](size)
copyToArray(arr)
arr
}
def toList = foldRight(Nil: List[A])(_ :: _)
def toVector = foldLeft(Vector.empty[A])(_ :+ _)
def collect[B](pf: PartialFunction[A,B]): Chunk[B] = {
val buf = new collection.mutable.ArrayBuffer[B](size)
iterator.collect(pf).copyToBuffer(buf)
Chunk.indexedSeq(buf)
}
def map[B](f: A => B): Chunk[B] = {
val buf = new collection.mutable.ArrayBuffer[B](size)
iterator.map(f).copyToBuffer(buf)
Chunk.indexedSeq(buf)
}
def mapAccumulate[S,B](s0: S)(f: (S,A) => (S,B)): (S,Chunk[B]) = {
val buf = new collection.mutable.ArrayBuffer[B](size)
var s = s0
for { c <- iterator } {
val (newS, newC) = f(s, c)
buf += newC
s = newS
}
(s, Chunk.indexedSeq(buf))
}
def scanLeft[B](z: B)(f: (B, A) => B): Chunk[B] = {
val buf = new collection.mutable.ArrayBuffer[B](size + )
iterator.scanLeft(z)(f).copyToBuffer(buf)
Chunk.indexedSeq(buf)
}
def iterator: Iterator[A] = new Iterator[A] {
var i =
def hasNext = i < self.size
def next = { val result = apply(i); i += ; result }
}
...

fs2的大部分转换函数都考虑了对Chunk数据的处理机制。我们先看看fs2是如何表现Chunk数据的:

 (Stream(,) ++ Stream(,,) ++ Stream(,)).chunks.toList
//> res16: List[fs2.Chunk[Int]] = List(Chunk(1, 2), Chunk(3, 4, 5), Chunk(6, 7))

fs2是按照Stream的构建批次来分节的。我们来示范一下如何使用Pull的Chunk机制:

 def myTakeC[F[_],A](n: Int): Pipe[F,A,A] = {
def go(n: Int): Stream.Handle[F,A] => Pull[F,A,Unit] = h => {
if ( n <= ) Pull.done
else Pull.awaitLimit(n)(h).flatMap {case Step(chunk,h) =>
if (chunk.size <= n) Pull.output(chunk) >> go(n-chunk.size)(h)
else Pull.output(chunk.take(n)) }
}
sin => sin.pull(go(n))
} //> myTakeC: [F[_], A](n: Int)fs2.Pipe[F,A,A]
val s1 = (Stream(,) ++ Stream(,,) ++ Stream(,))
//> s1 : fs2.Stream[Nothing,Int] = append(append(Segment(Emit(Chunk(1, 2))), S
egment(Emit(Chunk(()))).flatMap(<function1>)), Segment(Emit(Chunk(()))).fla
tMap(<function1>))
s1.pure.through(myTake()).chunks.toList //> res20: List[fs2.Chunk[Int]] = List(Chunk(1), Chunk(2), Chunk(3), Chunk(4))
s1.pure.through(myTakeC()).chunks.toList //> res21: List[fs2.Chunk[Int]] = List(Chunk(1, 2), Chunk(3, 4))

myTake和myTakeC产生了不同的结果。

fs2的特长应该是多线程编程了。在Stream的类型款式中:Stream[F[_],A],F[_]是一种可能产生副作用的运算方式,当F[_]等于Nothing时,Stream[Nothing,A]是一种纯数据流,而Stream[F[_],A]就是一种运算流了。我们可以在对运算流进行状态转换的过程中进行运算来实现F的副作用如:数据库读写、IO操作等。fs2不再绑定Task一种运算方式了。任何有Catchable实例的Monad都可以成为Stream的运算方式。但是,作为一种以多线程编程为主导的工具库,没有什么运算方式会比Task更合适了。
我们可以把一个纯数据流升格成运算流:

 val s2 = Stream.emits(Seq(,,)).covary[Task]    //> s2  : fs2.Stream[fs2.Task,Int] = Segment(Emit(Chunk(1, 2, 3)))

我们先运算这个运算流,结果为一个Task,然后再运算Task来获取运算值:

 val s2 = Stream.emits(Seq(,,)).covary[Task]    //> s2  : fs2.Stream[fs2.Task,Int] = Segment(Emit(Chunk(1, 2, 3)))
val t2 = s2.runLog //> t2 : fs2.Task[Vector[Int]] = Task
t2.unsafeRun //> res22: Vector[Int] = Vector(1, 2, 3)

现在使用myTake和myFilter就不需要pure升格了:

 s3.through(myFilter(_ %  == )).through(myTake()).runLog.unsafeRun
//> res23: Vector[Int] = Vector(2, 2, 2)

下面的例子里展示了fs2的运算流从源头(Source)到传换(Transducer)一直到终点(Sink)的使用示范:

 def stdOut: Sink[Task,String]  =
_.evalMap { x => Task.delay{ println(s"milli: $x")}}
//> stdOut: => fs2.Sink[fs2.Task,String]
Stream.repeatEval(Task.delay{System.currentTimeMillis})
.map(_.toString)
.through(myTake())
.to(stdOut)
.run.unsafeRun //> milli: 1472001934708
//| milli: 1472001934714
//| milli: 1472001934714

在上面的例子里我们使用了through,to等连接函数。由于数据最终发送到终点stdOut,我们无须用runLog来记录运算结果。

Scalaz(55)- scalaz-stream: fs2-基础介绍,fs2 stream transformation的更多相关文章

  1. Nodejs基础:stream模块入门介绍与使用

    本文摘录自<Nodejs学习笔记>,更多章节及更新,请访问 github主页地址.欢迎加群交流,群号 197339705. 模块概览 nodejs的核心模块,基本上都是stream的的实例 ...

  2. Node.js学习笔记(一)基础介绍

    什么是Node.js 官网介绍: Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine. Node.js us ...

  3. Node.js 基础介绍

    什么是Node.js 官网介绍: Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine. Node.js us ...

  4. Erlang基础 -- 介绍 -- Wordcount示例演示

    在前两个blog中,已经说了Erlang的历史.应用场景.特点,这次主要演示一个Wordcount的示例,就是给定一个文本文件,统计这个文本文件中的单词以及该单词出现的次数. 今天和群友们讨论了一个问 ...

  5. Web服务基础介绍

    Web服务基础介绍 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.正常情况下的单次web服务访问流程 博主推荐阅读: https://www.cnblogs.com/yinzh ...

  6. Java 8 新特性之 Stream 流基础体验

    Java 8 新特性之 Stream 流基础体验 package com.company; import java.util.ArrayList; import java.util.List; imp ...

  7. XML基础介绍【一】

    XML基础介绍[一] 1.XML简介(Extensible Markup Language)[可扩展标记语言] XML全称为Extensible Markup Language, 意思是可扩展的标记语 ...

  8. Web3D编程入门总结——WebGL与Three.js基础介绍

    /*在这里对这段时间学习的3D编程知识做个总结,以备再次出发.计划分成“webgl与three.js基础介绍”.“面向对象的基础3D场景框架编写”.“模型导入与简单3D游戏编写”三个部分,其他零散知识 ...

  9. C++ 迭代器 基础介绍

    C++ 迭代器 基础介绍 迭代器提供对一个容器中的对象的访问方法,并且定义了容器中对象的范围.迭代器就如同一个指针.事实上,C++的指针也是一种迭代器.但是,迭代器不仅仅是指针,因此你不能认为他们一定 ...

  10. php常用Stream函数集介绍

    php常用Stream函数集介绍 作者: 字体:[增加 减小] 类型:转载 时间:2013-06-24   本篇文章是对php中的常用Stream函数集进行了详细的分析介绍,需要的朋友参考下     ...

随机推荐

  1. Atitit vod click event design flow  视频点播系统点击事件文档

    Atitit vod click event design flow  视频点播系统点击事件文档 重构规划1 Click cate1 Click  mov4 重构规划 事件注册,与事件分发管理器分开 ...

  2. Atitit 异常的实现原理 与用户业务异常

    Atitit 异常的实现原理 与用户业务异常 1.1. 异常的实现原理1 1.2. 用户业务异常1 1.3. 异常转译和异常链2 1.4. 避免异常2 1.5. 异常恢复3 1.6. catch代码块 ...

  3. Paip.语义分析----情绪情感词汇表总结

    Paip.语义分析----情绪情感词汇表总结 以下词语是按感情色彩共分为十四类: 作者Attilax  艾龙,  EMAIL:1466519819@qq.com  来源:attilax的专栏 地址:h ...

  4. salesforce 零基础学习(三十六)通过Process Builder以及Apex代码实现锁定记录( Lock Record)

    上一篇内容是通过Process Builder和Approval Processes实现锁定记录的功能,有的时候,往往锁定一条记录需要很多的限制条件,如果通过Approval Processes的条件 ...

  5. HTML内联元素

    前面的话 用于标记段落里的文本和其他内容组的元素种类很多,本文将这些文本级元素进行简单分类,便于整理和记忆 通用容器 <span>元素是短语内容的通用行内容器,并没有任何特殊语义.可以使用 ...

  6. CSS字体

    字体系列 [1]5种通用字体系列:拥有相似外观的字体系列 serif字体:字体成比例,且有上下短线,包括Times\Georgia\New century Schoolbook sans-serif字 ...

  7. Dijkstra算法(二)之 C++详解

    本章是迪杰斯特拉算法的C++实现. 目录 1. 迪杰斯特拉算法介绍 2. 迪杰斯特拉算法图解 3. 迪杰斯特拉算法的代码说明 4. 迪杰斯特拉算法的源码 转载请注明出处:http://www.cnbl ...

  8. Codrops 优秀教程:CSS 3D Transforms 实现书本效果

    这个使用  CSS 3D Transforms 实现创意书本效果的来自 Codrops 网站.你可以看到两种类型的书设计:精装书和平装书.这两个效果都可以很容易地使用 CSS 修改.赶紧体验一下吧. ...

  9. 页面copyright部分始终居于页面底部

    <!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content ...

  10. GDB 和 windbg 命令对照(转载)

    From:http://blog.csdn.net/joeleechj/article/details/10020501 命令                                      ...