上节我们探讨了通过scalaz-stream-fs2来驱动一套数据处理流程,用fs2的Pipe类型来实现对数据流的逐行操作。本篇讨论准备在上节讨论的基础上对数据流的流动和元素操作进行优化完善。如数据流动中增加诸如next、skip、eof功能、内容控制中增加对行元素的append、insert、update、remove等操作方法。但是经过一番对fs2的再次解读,发现这些操作模式并不像我所想象那样的方式,实际上用fs2来实现数据行控制可能会更加简单和直接。这是因为与传统数据库行浏览方式不同的是fs2是一种拖式流(pull-model stream),它的数据行集合是一种泛函不可变集合。每一行一旦读取就等于直接消耗了断(consumed),所以只支持一种向前逐行读取模式。如果形象地描述的话,我们习惯的所谓数据集浏览可能是下面这样的场景:
读取一行数据 >>> (使用或更新行字段值)>>> 向下游发送新的一行数据。只有停止发送动作才代表终止运算。完成对上游的所有行数据读取并不代表终止操作,因为我们还可以不断向下游发送自定义产生的数据行。
我们用fs2模拟一套数据流管道FDAPipeLine,管道中间有不定数量的作业节点FDAWorkNode。作业方式包括从管道上游截取一个数据元素、对其进行处理、然后选择是否向下游的管道接口(FDAPipeJoint)发送。下面是这套模拟的类型:fdapipes/package.scala
package com.bayakala.funda {
import fs2._
package object fdapipes {
//数据行类型
trait FDAROW
//数据处理管道
type FDAPipeLine[ROW] = Stream[Task, ROW]
//数据作业节点
type FDAWorkNode[ROW] = Pipe[Task, ROW, ROW]
//数据管道开关阀门,从此处获得管道内数据
type FDAValve[ROW] = Handle[Task, ROW]
//管道连接器
type FDAPipeJoint[ROW] = Pull[Task, ROW, Unit]
//作业类型
type FDATask[ROW] = ROW => Option[List[ROW]]
}
}
流动控制方法:FDAValves.scala
package com.bayakala.funda.fdapipes
import fs2._
object FDAValves { //流动控制方法
//跳过本行(不向下游发送)
def fda_skip[ROW] = Some(List[ROW]())
//将本行发送至下游连接管道
def fda_next[ROW](r: ROW) = Some(List[ROW](r))
//终止流动
def fda_break = None
}
数据发送方法:FDAPipes.scala
package com.bayakala.funda.fdapipes
import fs2._
object FDAJoints { //数据发送方法
//write rows down the pipeline
def fda_pushRow[ROW](row: ROW) = Pull.output1(row)
def fda_pushRows[ROW](rows: List[ROW]) = Pull.output(Chunk.seq(rows))
}
作业节点工作方法:
package com.bayakala.funda.fdapipes
import FDAJoints._
object FDANodes { //作业节点工作方法
def fda_execUserTask[ROW](task: FDATask[ROW]): FDAWorkNode[ROW] = {
def go: FDAValve[ROW] => FDAPipeJoint[ROW] = h => {
h.receive1Option {
case Some((r, h)) => task(r) match {
case Some(xr) => xr match {
case Nil => go(h)
case _ => fda_pushRows(xr) >> go(h)
}
case None => fda_halt
}
case None => fda_halt
}
}
in => in.pull(go)
}
}
设置示范环境:
package com.bayakala.funda.fdapipes.examples
import fs2._
import com.bayakala.funda.fdapipes._
import FDANodes._
import FDAValves._
import Helpers._
object Example1 extends App {
case class Employee(id: Int, name: String, age: Int, salary: BigDecimal) extends FDAROW
// test data set
val r1 = Employee(1, "John", 23, 100.00)
val r2 = Employee(2, "Peter", 25,100.00)
val r3 = Employee(3, "Kay", 35,100.00)
val r4 = Employee(4, "Cain", 45,100.00)
val r5 = Employee(5, "Catty", 35,100.00)
val r6 = Employee(6, "Little", 19,80.00)
注意Employee是一种行类型,因为它extends FDAROW。
我们再写一个跟踪显示当前流动数据行的函数:examples/Helpers.scala
package com.bayakala.funda.fdapipes.examples
import com.bayakala.funda.fdapipes._
import fs2.Task
object Helpers {
def log[ROW](prompt: String): FDAWorkNode[ROW] =
_.evalMap {row => Task.delay{ println(s"$prompt> $row"); row }}
}
下面我们就用几个有不同要求的例子来示范流动控制和数据处理功能,这些例子就是给最终用户的标准编程示范版本,然后由用户照版编写:
1、根据每条数据状态逐行进行处理:
// 20 - 30岁加10%, 30岁> 加20%,其它加 5%
def raisePay: FDATask[FDAROW] = row => {
row match {
case emp: Employee => {
val cur = emp.age match {
case a if ((a >= 20) && (a < 30)) => emp.copy(salary = emp.salary * 1.10)
case a if ((a >= 30)) => emp.copy(salary = emp.salary * 1.20)
case _ => emp.copy(salary = emp.salary * 1.05)
}
fda_next(cur)
}
case _ => fda_skip
}
}
用户提供的功能函数类型必须是FDATask[FDAROW]。类型参数FDAROW代表数据行通用类型。如果用户指定了FDATask[Employee]函数类型,那么必须保证管道中流动的数据行只有Employee一种类型。完成对当前行数据的处理后用fda_next(emp)把它发送到下一节连接管道。我们用下面的组合函数来进行运算:
Stream(r1,r2,r3,r4,r5,r6)
.through(log("加薪前>"))
.through(fda_execUserTask[FDAROW](raisePay))
.through(log("加薪后>"))
.run.unsafeRun
-----
运算结果:
加薪前>> Employee(1,John,23,100.0)
加薪后>> Employee(1,John,23,110.00)
加薪前>> Employee(2,Peter,25,100.0)
加薪后>> Employee(2,Peter,25,110.00)
加薪前>> Employee(3,Kay,35,100.0)
加薪后>> Employee(3,Kay,35,120.00)
加薪前>> Employee(4,Cain,45,100.0)
加薪后>> Employee(4,Cain,45,120.00)
加薪前>> Employee(5,Catty,35,100.0)
加薪后>> Employee(5,Catty,35,120.00)
加薪前>> Employee(6,Little,19,80.0)
加薪后>> Employee(6,Little,19,84.000)
// 筛选40岁以上员工
def filter40: FDATask[FDAROW] = row => {
row match {
case emp: Employee => {
if (emp.age > 40)
Some(List(emp))
else fda_skip[Employee]
}
case _ => fda_break
}
}
println("---------")
Stream(r1,r2,r3,r4,r5,r6)
.through(log("年龄>"))
.through(fda_execUserTask[FDAROW](filter40))
.through(log("合格>"))
.run.unsafeRun
---
运算结果:
年龄>> Employee(1,John,23,100.0)
年龄>> Employee(2,Peter,25,100.0)
年龄>> Employee(3,Kay,35,100.0)
年龄>> Employee(4,Cain,45,100.0)
合格>> Employee(4,Cain,45,100.0)
年龄>> Employee(5,Catty,35,100.0)
年龄>> Employee(6,Little,19,80.0)
-
3、根据当前数据行状态终止作业:
// 浏览至第一个30岁以上员工,跳出
def stopOn30: FDATask[Employee] = emp => {
if (emp.age > 30)
fda_break
else
Some(List(emp))
}
println("---------")
Stream(r1,r2,r3,r4,r5,r6)
.through(log("当前员工>"))
.through(fda_execUserTask[Employee](stopOn30))
.through(log("选入名单>"))
.run.unsafeRun
---
运算结果:
当前员工>> Employee(1,John,23,100.0)
选入名单>> Employee(1,John,23,100.0)
当前员工>> Employee(2,Peter,25,100.0)
选入名单>> Employee(2,Peter,25,100.0)
当前员工>> Employee(3,Kay,35,100.0)
我们还可以把多个功能串接起来。像下面这样把1和2两个功能连起来:
Stream(r1,r2,r3,r4,r5,r6)
.through(log("加薪前>"))
.through(fda_execUserTask[FDAROW](raisePay))
.through(log("加薪后>"))
.through(log("年龄>"))
.through(fda_execUserTask[FDAROW](filter40))
.through(log("合格>"))
.run.unsafeRun
---
运算结果:
加薪前>> Employee(1,John,23,100.0)
加薪后>> Employee(1,John,23,110.00)
年龄>> Employee(1,John,23,110.00)
加薪前>> Employee(2,Peter,25,100.0)
加薪后>> Employee(2,Peter,25,110.00)
年龄>> Employee(2,Peter,25,110.00)
加薪前>> Employee(3,Kay,35,100.0)
加薪后>> Employee(3,Kay,35,120.00)
年龄>> Employee(3,Kay,35,120.00)
加薪前>> Employee(4,Cain,45,100.0)
加薪后>> Employee(4,Cain,45,120.00)
年龄>> Employee(4,Cain,45,120.00)
合格>> Employee(4,Cain,45,120.00)
加薪前>> Employee(5,Catty,35,100.0)
加薪后>> Employee(5,Catty,35,120.00)
年龄>> Employee(5,Catty,35,120.00)
加薪前>> Employee(6,Little,19,80.0)
加薪后>> Employee(6,Little,19,84.000)
年龄>> Employee(6,Little,19,84.000)
下面我把完整的示范代码提供给大家:
package com.bayakala.funda.fdapipes.examples
import fs2._
import com.bayakala.funda.fdapipes._
import FDANodes._
import FDAValves._
import Helpers._
object Example1 extends App {
case class Employee(id: Int, name: String, age: Int, salary: BigDecimal) extends FDAROW
// test data set
val r1 = Employee(1, "John", 23, 100.00)
val r2 = Employee(2, "Peter", 25,100.00)
val r3 = Employee(3, "Kay", 35,100.00)
val r4 = Employee(4, "Cain", 45,100.00)
val r5 = Employee(5, "Catty", 35,100.00)
val r6 = Employee(6, "Little", 19,80.00)
// 20 - 30岁加10%, 30岁> 加20%,其它加 5%
def raisePay: FDATask[FDAROW] = row => {
row match {
case emp: Employee => {
val cur = emp.age match {
case a if ((a >= 20) && (a < 30)) => emp.copy(salary = emp.salary * 1.10)
case a if ((a >= 30)) => emp.copy(salary = emp.salary * 1.20)
case _ => emp.copy(salary = emp.salary * 1.05)
}
fda_next(cur)
}
case _ => fda_skip
}
}
Stream(r1,r2,r3,r4,r5,r6)
.through(log("加薪前>"))
.through(fda_execUserTask[FDAROW](raisePay))
.through(log("加薪后>"))
.run.unsafeRun
// 筛选40岁以上员工
def filter40: FDATask[FDAROW] = row => {
row match {
case emp: Employee => {
if (emp.age > 40)
Some(List(emp))
else fda_skip[Employee]
}
case _ => fda_break
}
}
println("---------")
Stream(r1,r2,r3,r4,r5,r6)
.through(log("年龄>"))
.through(fda_execUserTask[FDAROW](filter40))
.through(log("合格>"))
.run.unsafeRun
// 浏览至第一个30岁以上员工,跳出
def stopOn30: FDATask[Employee] = emp => {
if (emp.age > 30)
fda_break
else
Some(List(emp))
}
println("---------")
Stream(r1,r2,r3,r4,r5,r6)
.through(log("当前员工>"))
.through(fda_execUserTask[Employee](stopOn30))
.through(log("选入名单>"))
.run.unsafeRun
println("---------")
Stream(r1,r2,r3,r4,r5,r6)
.through(log("加薪前>"))
.through(fda_execUserTask[FDAROW](raisePay))
.through(log("加薪后>"))
.through(log("年龄>"))
.through(fda_execUserTask[FDAROW](filter40))
.through(log("合格>"))
.run.unsafeRun
}