Scalaz(23)- 泛函数据结构: Zipper-游标定位
外面沙尘滚滚一直向北去了,意识到年关到了,码农们都回乡过年去了,而我却留在这里玩弄“拉链”。不要想歪了,我说的不是裤裆拉链而是scalaz Zipper,一种泛函数据结构游标(cursor)。在函数式编程模式里的集合通常是不可变的(immutable collection),我们会发现在FP编程过程中处理不可变集合(immutable collection)数据的方式好像总是缺些什么,比如在集合里左右逐步游动像moveNext,movePrev等等,在一个集合的中间进行添加、更新、删除的功能更是欠奉了,这主要是因为操作效率问题。不可变集合只有对前置操作(prepend operation)才能获得可靠的效率,即对集合首位元素的操作,能得到相当于O(1)的速度,其它操作基本上都是O(n)速度,n是集合的长度,也就是随着集合的长度增加,操作效率会以倍数下降。还有一个原因就是编程时会很不方便,因为大多数程序都会对各种集合进行大量的操作,最终也会导致程序的复杂臃肿,不符合函数式编程要求的精简优雅表达形式。我想可能就是因为以上各种原因,scalaz提供了Zipper typeclass帮助对不可变集合操作的编程。Zipper的定义如下:scalaz/Zipper.scala
final case class Zipper[+A](lefts: Stream[A], focus: A, rights: Stream[A])
它以Stream为基础,A可以是任何类型,无论基础类型或高阶类型。Zipper的结构如上:当前焦点窗口、左边一串数据元素、右边一串,形似拉链,因而命名Zipper。或者这样看会更形象一点:
final case class Zipper[+A](
lefts: Stream[A],
focus: A,
rights: Stream[A])
scalaz提供了Zipper构建函数可以直接用Stream生成一个Zipper:
trait StreamFunctions {
...
final def toZipper[A](as: Stream[A]): Option[Zipper[A]] = as match {
case Empty => None
case h #:: t => Some(Zipper.zipper(empty, h, t))
}
final def zipperEnd[A](as: Stream[A]): Option[Zipper[A]] = as match {
case Empty => None
case _ =>
val x = as.reverse
Some(Zipper.zipper(x.tail, x.head, empty))
}
...
zipperEnd生成倒排序的Zipper:
Stream(,,).toZipper //> res2: Option[scalaz.Zipper[Int]] = Some(Zipper(<lefts>, 1, <rights>))
Stream("A","B","C").toZipper //> res3: Option[scalaz.Zipper[String]] = Some(Zipper(<lefts>, A, <rights>))
Stream(Stream(,),Stream(,)).toZipper //> res4: Option[scalaz.Zipper[scala.collection.immutable.Stream[Int]]] = Some(Z
//| ipper(<lefts>, Stream(1, ?), <rights>))
Stream(,,).zipperEnd //> res5: Option[scalaz.Zipper[Int]] = Some(Zipper(<lefts>, 3, <rights>))
scalaz也为List,NonEmptyList提供了Zipper构建函数:
trait ListFunctions {
...
final def toZipper[A](as: List[A]): Option[Zipper[A]] =
stream.toZipper(as.toStream)
final def zipperEnd[A](as: List[A]): Option[Zipper[A]] =
stream.zipperEnd(as.toStream)
...
final class NonEmptyList[+A] private[scalaz](val head: A, val tail: List[A]) {
...
def toZipper: Zipper[A] = zipper(Stream.Empty, head, tail.toStream)
def zipperEnd: Zipper[A] = {
import Stream._
tail.reverse match {
case Nil => zipper(empty, head, empty)
case t :: ts => zipper(ts.toStream :+ head, t, empty)
}
}
...
都是先转换成Stream再生成Zipper的。Zipper本身的构建函数是zipper,在NonEmptyList的Zipper生成中调用过:
trait ZipperFunctions {
def zipper[A](ls: Stream[A], a: A, rs: Stream[A]): Zipper[A] =
Zipper(ls, a, rs)
}
用这些串形结构的构建函数产生Zipper同样很简单:
List(,,,).toZipper //> res0: Option[scalaz.Zipper[Int]] = Some(Zipper(<lefts>, 1, <rights>))
List(List(,),List(,)).toZipper //> res1: Option[scalaz.Zipper[List[Int]]] = Some(Zipper(<lefts>, List(1, 2), <r
//| ights>))
NonEmptyList("A","C","E").toZipper //> res2: scalaz.Zipper[String] = Zipper(<lefts>, A, <rights>)
NonEmptyList(,,).zipperEnd //> res3: scalaz.Zipper[Int] = Zipper(<lefts>, 3, <rights>)
有了串形集合的Zipper构建方法后我们再看看一下Zipper的左右游动函数:
final case class Zipper[+A](lefts: Stream[A], focus: A, rights: Stream[A]) {
...
/**
* Possibly moves to next element to the right of focus.
*/
def next: Option[Zipper[A]] = rights match {
case Stream.Empty => None
case r #:: rs => Some(zipper(Stream.cons(focus, lefts), r, rs))
}
/**
* Possibly moves to next element to the right of focus.
*/
def nextOr[AA >: A](z: => Zipper[AA]): Zipper[AA] =
next getOrElse z
/**
* Possibly moves to the previous element to the left of focus.
*/
def previous: Option[Zipper[A]] = lefts match {
case Stream.Empty => None
case l #:: ls => Some(zipper(ls, l, Stream.cons(focus, rights)))
}
/**
* Possibly moves to previous element to the left of focus.
*/
def previousOr[AA >: A](z: => Zipper[AA]): Zipper[AA] =
previous getOrElse z
/**
* Moves focus n elements in the zipper, or None if there is no such element.
*
* @param n number of elements to move (positive is forward, negative is backwards)
*/
def move(n: Int): Option[Zipper[A]] = {
@tailrec
def move0(z: Option[Zipper[A]], n: Int): Option[Zipper[A]] =
if (n > && rights.isEmpty || n < && lefts.isEmpty) None
else {
if (n == ) z
else if (n > ) move0(z flatMap ((_: Zipper[A]).next), n - )
else move0(z flatMap ((_: Zipper[A]).previous), n + )
}
move0(Some(this), n)
}
/**
* Moves focus to the start of the zipper.
*/
def start: Zipper[A] = {
val rights = this.lefts.reverse ++ focus #:: this.rights
this.copy(Stream.Empty, rights.head, rights.tail)
}
/**
* Moves focus to the end of the zipper.
*/
def end: Zipper[A] = {
val lefts = this.rights.reverse ++ focus #:: this.lefts
this.copy(lefts.tail, lefts.head, Stream.empty)
}
/**
* Moves focus to the nth element of the zipper, or the default if there is no such element.
*/
def moveOr[AA >: A](n: Int, z: => Zipper[AA]): Zipper[AA] =
move(n) getOrElse z
...
start,end,move,next,previous移动方式都齐了。还有定位函数:
...
/**
* Moves focus to the nearest element matching the given predicate, preferring the left,
* or None if no element matches.
*/
def findZ(p: A => Boolean): Option[Zipper[A]] =
if (p(focus)) Some(this)
else {
val c = this.positions
std.stream.interleave(c.lefts, c.rights).find((x => p(x.focus)))
} /**
* Moves focus to the nearest element matching the given predicate, preferring the left,
* or the default if no element matches.
*/
def findZor[AA >: A](p: A => Boolean, z: => Zipper[AA]): Zipper[AA] =
findZ(p) getOrElse z /**
* Given a traversal function, find the first element along the traversal that matches a given predicate.
*/
def findBy[AA >: A](f: Zipper[AA] => Option[Zipper[AA]])(p: AA => Boolean): Option[Zipper[AA]] = {
@tailrec
def go(zopt: Option[Zipper[AA]]): Option[Zipper[AA]] = {
zopt match {
case Some(z) => if (p(z.focus)) Some(z) else go(f(z))
case None => None
}
}
go(f(this))
} /**
* Moves focus to the nearest element on the right that matches the given predicate,
* or None if there is no such element.
*/
def findNext(p: A => Boolean): Option[Zipper[A]] = findBy((z: Zipper[A]) => z.next)(p) /**
* Moves focus to the previous element on the left that matches the given predicate,
* or None if there is no such element.
*/
def findPrevious(p: A => Boolean): Option[Zipper[A]] = findBy((z: Zipper[A]) => z.previous)(p)
...
操作函数如下:
...
/**
* An alias for insertRight
*/
def insert[AA >: A]: (AA => Zipper[AA]) = insertRight(_: AA) /**
* Inserts an element to the left of focus and focuses on the new element.
*/
def insertLeft[AA >: A](y: AA): Zipper[AA] = zipper(lefts, y, focus #:: rights) /**
* Inserts an element to the right of focus and focuses on the new element.
*/
def insertRight[AA >: A](y: AA): Zipper[AA] = zipper(focus #:: lefts, y, rights) /**
* An alias for `deleteRight`
*/
def delete: Option[Zipper[A]] = deleteRight /**
* Deletes the element at focus and moves the focus to the left. If there is no element on the left,
* focus is moved to the right.
*/
def deleteLeft: Option[Zipper[A]] = lefts match {
case l #:: ls => Some(zipper(ls, l, rights))
case Stream.Empty => rights match {
case r #:: rs => Some(zipper(Stream.empty, r, rs))
case Stream.Empty => None
}
} /**
* Deletes the element at focus and moves the focus to the left. If there is no element on the left,
* focus is moved to the right.
*/
def deleteLeftOr[AA >: A](z: => Zipper[AA]): Zipper[AA] =
deleteLeft getOrElse z /**
* Deletes the element at focus and moves the focus to the right. If there is no element on the right,
* focus is moved to the left.
*/
def deleteRight: Option[Zipper[A]] = rights match {
case r #:: rs => Some(zipper(lefts, r, rs))
case Stream.Empty => lefts match {
case l #:: ls => Some(zipper(ls, l, Stream.empty))
case Stream.Empty => None
}
} /**
* Deletes the element at focus and moves the focus to the right. If there is no element on the right,
* focus is moved to the left.
*/
def deleteRightOr[AA >: A](z: => Zipper[AA]): Zipper[AA] =
deleteRight getOrElse z /**
* Deletes all elements except the focused element.
*/
def deleteOthers: Zipper[A] = zipper(Stream.Empty, focus, Stream.Empty)
...
/**
* Update the focus in this zipper.
*/
def update[AA >: A](focus: AA) = {
this.copy(this.lefts, focus, this.rights)
} /**
* Apply f to the focus and update with the result.
*/
def modify[AA >: A](f: A => AA) = this.update(f(this.focus))
...
insert,modify,delete也很齐备。值得注意的是多数Zipper的移动函数和操作函数都返回Option[Zipper[A]]类型,如此我们可以用flatMap把这些动作都连接起来。换句话说就是我们可以用for-comprehension在Option的context内实现行令编程(imperative programming)。我们可以通过一些例子来示范Zipper用法:
val zv = for {
z <- List(,,,,,).toZipper
s1 <- z.next
s2 <- s1.modify{_ + }.some
} yield s2 //> zv : Option[scalaz.Zipper[Int]] = Some(Zipper(<lefts>, 10, <rights>))
zv.get.show //> res8: scalaz.Cord = Zipper(Stream(2), 10, Stream(1,5,4,11))
zv.get.toList //> res9: List[Int] = List(2, 10, 1, 5, 4, 11)
...
val zv = for {
z <- List(,,,,,).toZipper
s1 <- z.next
s2 <- s1.modify{_ + }.some
s3 <- s2.move()
s4 <- s3.delete
} yield s4 //> zv : Option[scalaz.Zipper[Int]] = Some(Zipper(<lefts>, 5, <rights>))
zv.get.show //> res8: scalaz.Cord = Zipper(Stream(10,2), 5, Stream(4,11))
zv.get.toList //> res9: List[Int] = List(2, 10, 5, 4, 11)
...
val zv = for {
z <- List(,,,,,).toZipper
s1 <- z.next
s2 <- s1.modify{_ + }.some
s3 <- s2.move()
s4 <- s3.delete
s5 <- s4.findZ {_ === }
s6 <- if (s5.focus === ) s5.delete else s2.insert().some
} yield s6 //> zv : Option[scalaz.Zipper[Int]] = Some(Zipper(<lefts>, 12, <rights>))
zv.get.show //> res8: scalaz.Cord = Zipper(Stream(10,2), 12, Stream(1,5,4,11))
zv.get.toList //> res9: List[Int] = List(2, 10, 12, 1, 5, 4, 11)
...
val zv = for {
z <- List(,,,,,).toZipper
s1 <- z.next
s2 <- s1.modify{_ + }.some
s3 <- s2.move()
s4 <- s3.delete
s5 <- s4.findZ {_ === }
s6 <- if (s5.focus === ) s5.delete else s2.insert().some
s7 <- s6.end.delete
s8 <- s7.start.some
} yield s8 //> zv : Option[scalaz.Zipper[Int]] = Some(Zipper(<lefts>, 2, <rights>))
zv.get.show //> res8: scalaz.Cord = Zipper(Stream(), 2, Stream(10,12,1,5,4))
zv.get.toList //> res9: List[Int] = List(2, 10, 12, 1, 5, 4)
我在上面的程序里在for{...}yield里面逐条添加指令从而示范游标当前焦点和集合元素跟随着的变化。这段程序可以说就是一段行令程序。
回到上面提到的效率和代码质量讨论。我们提过scalaz提供Zipper就是为了使集合操作编程更简明优雅,实际情况是怎样的呢?
举个例子:有一串数字,比如:List(1,4,7,9,5,6,10), 我想找出第一个高点元素,它的左边低,右边高,在我们的例子里是元素9。如果我们尝试用习惯的行令方式用索引去编写这个函数:
def peak(list: List[Int]): Option[Int] = {
list.indices.find { index =>
val x = list(index)
index > && index < list.size - &&
x > list(index - ) && x > list(index + )
}.map(list(_))
}
哇!这东西不但极其复杂难懂而且效率低下,重复用find索引导致速度降到O(n * n)。如果用Array会把效率提高到O(n),不过我们希望用immutable方式。那么用函数式编程方式呢?
def peak_fp(list: List[Int]): Option[Int] = list match {
case x :: y :: z :: tl if y > x && y > z => Some(y)
case x :: tl => peak(tl)
case Nil => None
}
用模式匹配(pattern matching)和递归算法(recursion),这段程序好看多了,而且效率也可以提高到O(n)。
但我们再把情况搞得复杂一点:把高点值增高一点(+1)。还是用FP方式编写:
def raisePeak(list: List[Int]): Option[List[Int]] = {
def rec(head: List[Int], tail: List[Int]): Option[List[Int]] = tail match {
case x :: y :: z :: tl if y > x && y > z =>
Some((x :: head).reverse ::: ((y +) :: z :: tl))
case x :: tl => rec(x :: head, tl) case Nil => None
}
rec(List.empty, list)
}
代码又变得臃肿复杂起来。看来仅仅用FP编程方式还不足够,还需要用一些新的数据结构什么的来帮助。scalaz的Zipper可以在这个场景里派上用场了:
def raisePeak_z(list: List[Int]): Option[List[Int]] = {
for {
zipper <- list.toZipper
peak <- zipper.positions.findNext( z =>
(z.previous, z.next) match {
case (Some(p), Some(n)) => p.focus < z.focus && n.focus < z.focus
case _ => false
})
} yield (peak.focus.modify(_ + ).toStream.toList)
}
用Zipper来写程序表达清楚许多。这里用上了Zipper.positions:
/**
* A zipper of all positions of the zipper, with focus on the current position.
*/
def positions: Zipper[Zipper[A]] = {
val left = std.stream.unfold(this)(_.previous.map(x => (x, x)))
val right = std.stream.unfold(this)(_.next.map(x => (x, x))) zipper(left, this, right)
}
positions函数返回类型是Zipper[Zipper[A]]符合findNext使用。我们前面已经提到:使用Zipper的成本约为O(n)。
Scalaz(23)- 泛函数据结构: Zipper-游标定位的更多相关文章
- Scalaz(24)- 泛函数据结构: Tree-数据游览及维护
上节我们讨论了Zipper-串形不可变集合(immutable sequential collection)游标,在串形集合中左右游走及元素维护操作.这篇我们谈谈Tree.在电子商务应用中对于xml, ...
- 游标定位:Cursor类
关于 Cursor Cursor 是每行的集合. 使用 moveToFirst() 定位第一行. 你必须知道每一列的名称. 你必须知道每一列的数据类型. Cursor 是一个随机的数据源. 所有的数据 ...
- 泛函编程(5)-数据结构(Functional Data Structures)
编程即是编制对数据进行运算的过程.特殊的运算必须用特定的数据结构来支持有效运算.如果没有数据结构的支持,我们就只能为每条数据申明一个内存地址了,然后使用这些地址来操作这些数据,也就是我们熟悉的申明变量 ...
- 泛函编程(8)-数据结构-Tree
上节介绍了泛函数据结构List及相关的泛函编程函数设计使用,还附带了少许多态类型(Polymorphic Type)及变形(Type Variance)的介绍.有关Polymorphism的详细介绍会 ...
- 泛函编程(6)-数据结构-List基础
List是一种最普通的泛函数据结构,比较直观,有良好的示范基础.List就像一个管子,里面可以装载一长条任何类型的东西.如需要对管子里的东西进行处理,则必须在管子内按直线顺序一个一个的来,这符合泛函编 ...
- 怎样学习Scala泛函编程
确切来说应该是我打算怎么去学习Scala泛函编程.在网上找不到系统化完整的Scala泛函编程学习资料,只好把能找到的一些书籍.博客.演讲稿.论坛问答.技术说明等组织一下,希望能达到学习目的.关于Sca ...
- SQL Server 游标
结果集,结果集就是select查询之后返回的所有行数据的集合. 在关系数据库中,我们对于查询的思考是面向集合的.而游标打破了这一规则,游标使得我们思考方式变为逐行进行. 正常面向集合的思维方式是: 而 ...
- 网上看到一份详细sql游标说明 《转载 https://www.cnblogs.com/xiongzaiqiren/p/sql-cursor.html》
SQL游标(cursor)详细说明及内部循环使用示例 游标 游标(cursor)是系统为用户开设的一个数据缓冲区,存放SQL语句的执行结果.每个游标区都有一个名字,用户可以用SQL语句逐一从游标中获 ...
- Oracle PLSQL游标、游标变量的使用
参考文章:https://www.cnblogs.com/huyong/archive/2011/05/04/2036377.html 在 PL/SQL 程序中,对于处理多行记录的事务经常使用游标来实 ...
随机推荐
- Sqlserver分页的问题
好久没有用SqlServer了,今天写了一个分页,遇到了小问题,本着温故而知新的道理,再来随便写些什么吧. 语句是这样的 string sql=“select * from ( select*,(ro ...
- 快速入门系列--WCF--03RESTFUL服务与示例
之前介绍了基于SOAP的Web服务,接下来将介绍基于REST的轻量级的Web服务. REST(Representational State Transfer)与技术无关,代表一种软件架构风格,可以成为 ...
- 体验Visual Studio 2015 Windows Forms应用程序开发与维护
昨天到半夜还没有等到Visual Studio 2015的下载地址,实在熬不住就先休息了.北美地区的时区比北京时间要晚一些,今天早上到公司就看到Visual Studio 2015的下载地址,迅速的将 ...
- 关于AJAX跨域调用ASP.NET MVC或者WebAPI服务的问题及解决方案
作者:陈希章 时间:2014-7-3 问题描述 当跨域(cross domain)调用ASP.NET MVC或者ASP.NET Web API编写的服务时,会发生无法访问的情况. 重现方式 使用模 ...
- 构建自己的PHP框架--抽象框架的内容
上一篇博客中,我们搭建了一个最简单的框架,从单一入口的public/index.php进入,解析出相应的Controller和Action,去执行,渲染出相应的页面或者输出相应的数据. 但是我们可以看 ...
- RobotFramework - 在Window7系统中安装本地RobotFrmamework自动化测试环境
RIDE Installation 安装顺序:Python ---> setuptools & pip ---> Robot Framewok ---> wxPython(v ...
- 推荐20款基于 jQuery & CSS 的文本效果插件
jQuery 和 CSS 可以说是设计和开发行业的一次革命.这一切如此简单,快捷的一站式服务.jQuery 允许你在你的网页中添加一些真正令人惊叹的东西而不用付出很大的努力,要感谢那些优秀的 jQue ...
- apache解析多个域名
之前搭建了一个网站在这台服务器上,今天心血来潮准备搭建个word press 博客,准备使用二级域名 blog.xdlxb.cn 来解析. 只需要设置httpd.conf 文件就可以了 如下 开启重定 ...
- Hibernate的session一级缓存
一级缓存是Session周期的,当session创建的时候就有,当session结束的时候,缓存被清空 当缓存存在的时候,每次查询的数据,都会放在缓存中,如果再次查询相同的数据,则不会再次查询数据库, ...
- git代码回滚:Reset、Checkout、Revert的选择
代码回滚:Reset.Checkout.Revert的选择 Zhongyi Tong edited this page on Dec 8, 2015 · 5 revisions Pages 19 Ho ...