函数式编程的数据结构

来源:互联网 发布:移动蜂窝数据是什么 编辑:程序博客网 时间:2024/06/15 15:43

      • 概述
      • 常用数据结构或容器类型
        • 序列Seq
          • 序列之List
          • 序列之Vector
          • 序列之惰性流Stream
        • 映射表Map
        • 集Set
        • Option
          • 返回Option对象操作的缺点
        • Either
          • 抛出异常还是返回Either值
        • Try
        • Validation scalaz提供
      • 常用操作
        • 遍历操作foreach
        • 映射操作map
        • 扁平映射flatMap
        • 过滤filter
        • 折叠fold
        • 归约reduce

概述

面向对象语言通常为各个领域建立对等类来实现其业务,而函数式编程则倾向于使用核心数据结构和算法来实现业务逻辑。

通常数据结构被定义为:相互之间存在一种或多种特定关系的数据元素的集合。也即,Data_Structure=(D, R),其中D为数据的有限集,而R则为关系的有限集

在数据存储上,有一种对象类型,它可以持有其它对象或指向其它对象的指针,同时还包含了一系列处理其它对象的方法,这种对象类型就是容器

容器是一种对特定代码复用问题的良好的解决方案,同时容器也可以自行扩展(通过调用其提供的方法来添加或释放持有的对象)。

在Scala中数据结构通常以容器的形式实现,同时也可以在一定程度上将容器看作是元素的集合,这里就对常用的数据结构(容器或集合)及其操作进行简单介绍。


常用数据结构(或容器类型)

Scala编程中常用的数据结构(或容器类型)有:序列(Seq)、映射表(Map)、集(Set)、Option、Either、Try及scalaz提供的Validation。分别介绍如下:


序列(Seq)

元素可以按特定顺序进行访问的数据结构,如元素的插入顺序或其它特定顺序。

Predef将scala.collection.Seq导入到了当前作用域中,此类型是可变和不可变集合类型的共同基类,尽管其并未暴露任何用于修改集合的方法,但仍存在并发情况下出错的潜在风险。在实际使用时,如果确信只需使用不可变的Seq,可以在较高级别的包对象中为Seq定义新的别名,并以之覆盖原有别名定义;也可以将其写到包定义中,然后在使用时引入此定义。相关的代码如下:

type Seq[+A] = scala.collection.immutable.Seq[A] //声明类型别名val Seq = scala.collection.immutable.Seq //将伴随对象引用作用域,于是类型Seq(1, 2)这样的语句将会触发scala.collection.immutable.Seq.apply方法的调用

对于Seq而言,其有比较多的子类型,下面就对比较常用的几个进行简单介绍:


序列之List

向列表里追加元素时,该元素会被追加到列表的头部。除了头部,剩下的部分就是原列表的元素,这些元素并没有被修改,它们变成了新列表的尾部。

从旧列表中创建新列表的操作的复杂度是O(1)
计算列表长度操作的复杂度是O(N)
对于那些访问头部以外元素操作的复杂度是O(n)
可以将List可作是链表

不推荐将列表作为参数或返回值,而应该用Seq,这样Seq的任何子类都可以使用了(如List、Vector)。

Scala中定义的可变列表类型有ListBufferMutableList只有当必须修改元素时才应该使用可变类型


序列之Vector

Vector的常用操作的复杂度都是O(1),可以看作是数组。


序列之惰性流(Stream)

对于一个潜在无限数据流,无限性决定其不可能被完全放到内存中的一个集合中。对此,你可能只需要前N个元素,而丢弃其它元素。针对上述情形,惰性流(Scala中的Stream类型)就有了产生的必要。

惰性流只会对集合的头部调用一次随机函数,然后一直等待,直到调用者要求得到集合的尾部值,这意味着它只在被要求的时候才对其尾部求值

求值是指:在计算时,定义一个无限数据流唯一可能的方法是使用一个会一直生成数值的函数。该函数可能从某个输入渠道(如套接字或大文件)读取数据,或本身就是一个能产生数值序列的函数。

import scala.math.BigIntval fibs: Stream[BigInt] = BigInt(0) #:: BigInt(1) #:: fibs.zip(fibs.tail).map(n => n._1 + n._2)//#::用于向Stream的头部添加元素。lazy val fib: Stream[BigInt] = {  def loop(h: BigInt, n: Int): Stream[BigInt] = h #:: loop(n, h+n)  loop(1, 1)}

映射表(Map)

映射表,也被称为散列散列表,是用来存储键值对的一种数据结构

val age = Map("Yuan" -> 27)

key -> value语法是使用库中的隐式转换实现的,实际调用了Map.apply方法

Map有不可变(scala.collection.immutable.Map)和可变(scala.collection.mutable.Map)两种实现,不可变的实现已经用Predef导入了,而可变的则需要手工显式导入。

Map定义了+-操作用于增加和移除元素++--操作用于增加或移除Iterator(或其它集合和列表)中定义的元素


集(Set)

集是没有重复对象的集合,所有元素都是唯一的。

Set有不可变(scala.collection.immutable.Set)和可变(scala.collection.mutable.Set)两种实现,不可变的实现已经用Predef导入了,而可变的则需要手工显式导入。

Set定义了+++---操作用于增加和移除元素


Option

Option是一个二元容器,其中也许包含一个元素,也许不包含任何元素,上述两种情况分别由Option的两个子类SomeNone来表示。


返回Option对象操作的缺点

当操作返回的结果是None时,可能是出错导致返回None,也可能是其返回值就是None(也即具有二义性)。也即None对象不能提供任何信息告诉我们为什么不返回值


Either

Either是一类能且只能持有两种事件中一种的容器,也即Either持有这个或那个元素项。Either[+A, +B]有两个子类,分别为Left[+A]Right[+B],通过这两个子类选择持有哪个元素。

Either概念的产生早于Scala,很长时间以来它被认为是抛出异常的一种替代解决方案。为了尊重历史习惯,当Either用于表示错误标识或某一对象值时,Left用于表示错误标识,而Right则用于表示正常返回值(Because Right Are Always Right)。

在实际实用时,将Either定义为Or或许更符合阅读,方法如下:

type Or[String, Int] = Either[String, Int]val test = String Or Int = Left("foo")

Either对象提供了leftright方法,这两个方法会分别构建一个提供组合方法的投影对象LeftProjection和RightProjection,投影对象既要以持有Left值也可以持有Right值。在调用LeftProjection.map时,如果此Either对象持有Left实例,map操作会作用于Left实例持有的对象;如果持有Right对象,此时不会对Right对象进行操作,因此第二个参数保持不变(这与Option.map方法处理None对象的方式类似)。同理,对于RightProjection.map也是如此。


抛出异常还是返回Either值

抛出异常的优点:

抛出异常能避免对错误数据进行计算;而有时调用栈中的某些对象捕获异常可以对故障执行合理的恢复。

抛出异常的缺点:

抛出异常会破坏引用的透明性

使用Either对象的特点:

不再通过抛出异常来捕获调用堆栈中某些应用的控制权,而是将异常作为调用堆栈中的结果返回,以此来消除程序错误并使客户更清楚地理解API的行为。

使用Either对象的优点:

可以保障引用透明性,并通过类型签名提醒调用者可能会出现错误。


Try

在Java和Scala类库中,抛出异常是一种很常见的做法,为了处理异常,我们会编写try{...} catch{...} finally{...}这样的样板代码并将结果封装到Either对象中。Scala中Try完成了对上述样板代码的封装,让你可以集中精力对正常逻辑进行处理,让Try负责捕获异常。

Try有两个子类,分别是SuccessFailure(此类只保存Throwable类型的值)。相对于Either类型而言,Try类型是一个非对称类型,该类型只包含了一个正常类型以及用于错误场景的Throwable类型。


Validation (scalaz提供)

假设for推导式中出现了空值(Option)或错误(Failure),那么组合器便不会调用后续的表达式,也即,会在发生第一个错误时便停止执行后续代码。如果我们正在执行一些相互独立的操作,并希望执行这些操作时收集所有发生的错误,待操作执行完成后再决定如何处理错误,此时Validation就成了我们的不二之选。

Validation有两个子类,分别是SuccessFailure,尽管名称与Try的子类相同但两者并不是同一个。

与Either类型相似,Validation类中的第一个参数表示用于汇报错误的类型,Scalaz负责调用合适的连接方法来将多个错误拼接起来;第二个参数类型代表正常返回的值类型。

//libraryDependencies += "org.scalaz" %% "scalaz-core" % "7.2.9"import scalaz._import sta.AllInstances._def integer(i: Any): Validation[List[String], Long] = i match {  case int: Int => Success(int.toLong)  case long: Long => Success(long)  case _ => Failure(List(s"Not integer $i"))}val sum1 = integer(2) +++ integer(3)//Success(5)val sum2 = integer("2xx") +++ integer("3xx") +++ integer(4) +++ integer("5xx")//Failure(List(Not integer 2xx, Not integer 3xx, Not integer 5xx))

使用Option、Either、Try和Validation可以减少异常的使用,同时还解决了一个重要的并发问题(由于我们无法保证异常执行的代码会运行在被称为“调用者”的同一个线程内,因此调用者无法捕获其它代码抛出的异常。如果能够像返回正常值那样返回异常,调用者便能得到异常值)。


常用操作

Scala中常用的容器(或集合)操作有:遍历、映射、过滤、折叠和归约等。


遍历操作(foreach)

Scala容器类型的标准遍历方法是foreach,其在scala.collection.IterableLike中定义,签名如下:

trait IterableLike[+A, +Repr] extends Any with Equals with GenIterableLike[A, Repr] {  def iterator: Iterator[A]  def foreach[U](f: A => U): Unit = iterator.foreach(f)}

foreach函数的输出类型是Unit,因此其是一个完全副作用的函数。又因为它的参数是一个函数,所以其是一个高阶函数

foreach函数的复杂度为O(N),N为元素个数。

foreach并不是一个纯函数,因为其只能执行带副作用的操作。然而,一旦有了foreach,我们就可以实现映射、过滤、折叠和归约等不带副作用的操作。同时,如果有了折叠(fold)操作,也可以由其实现foreach操作。


映射操作(map)

Scala容器类型的标准映射方法是map,其在scala.collection.TraversableLike中定义,签名如下:

trait TraversableLike[+A, +Repr] extends Any with HasNewBuilder[A, Repr] with FilterMonadic[A, Repr] with TraversableOnce[A] with Parallelizable[A, ParIterable[A]] {  self =>  def repr: Repr = this.asInstanceOf[Repr]  def map[B, That](f: A => B)(implicit bf: CanBuildFrom[Repr, B, That]): That = {    //extacted to keep method size under 35 bytes, so that it can be JIT-inlined    def builder = {      val b = bf(repr)      b.sizeHint(this)      b    }    val b = builder    for(x <- this) b += f(x)    b.result  }}

其中,That是输出集合的类型,事实上与输入集合的类型相同;bf意味着我们可以用map的输出和函数f构造一个That,同时bf本身也负责构建;Repr是内部用来表示集合元素的。

map方法的实质是将一个集合转换为另一个集合,原集合A的元素经过转换函数处理后转化为集合B的元素。


扁平映射(flatMap)

Scala容器类型的标准扁平映射方法是flatMap,其在scala.collection.TraversableLike中定义,签名如下:

trait TraversableLike[+A, +Repr] extends Any with HasNewBuilder[A, Repr] with FilterMonadic[A, Repr] with TraversableOnce[A] with Parallelizable[A, ParIterable[A]] {  self =>  def repr: Repr = this.asInstanceOf[Repr]  def flatMap[B, That](f: A => GenTraversableOnce[B])(implicit bf: CanBuildFrom[Repr, B, That]): That = {    //extacted to keep method size under 35 bytes, so thatit can be JIT-inlined    def builder = bf(repr)    val b = builder    for(x <- this) b ++= f(x).seq    b.result  }}

flatMap的行为很像先调用map,再调用flatten。但flatMap比连续调用这两个方法更高效,因为其不需要创建临时变量

flatMap不能处理超过一层的集合,如果函数返回的是深层嵌套的集合,那么集合只能被压扁一层。


过滤(filter)

Scala容器的标准过滤方法是filter,其在scala.collection.TraversableLike中定义,签名如下:

trait TraversableLike[+A, +Repr] extends Any with HasNewBuilder[A, Repr] with FilterMonadic[A, Repr] with TraversableOnce[A] with Parallelizable[A, ParIterable[A]] {  self =>  def filter(p: A => Boolean): Repr = filterImpl(p, isFlipped = false)  protected[this] def newBuilder: Builder[A, Repr]  private def filterImpl(p: A => Boolean, isFlipped: Boolean): Repr = {    val b = newBuilder    for(x <- this)      if(p(x) != isFlipped) b += x    b.result  }}

filter可以遍历一个集合,然后抽取其中满足特定条件的元素,组成一个新的集合。

在TraversableLike中还定义了一些其它的用于集合过滤或返回集合中部分元素的方法。一些方法在输入无限集合时不会返回,一些方法在输入同一个集合时,除非集合的遍历顺序固定,否则在多次运行的情况下可能产生不同的输出。常用的其它方法如下:

def drop(n: Int): Repr //去除起始的n个元素def dropWhile(p: A => Boolean): Repr //从头遍历,丢弃满足一定谓词的最长集合前缀def filterNot(p: A => Boolean): Repr = filterImpl(p, isFlipped = true) //filter的反义词def find(p: A => Boolean): Option[A] = {  var result: Option[A] = None  breakable {    for(x <- this)      if(p(x)) {result = Some(x); break}  }} //查找第一个满足给定谓词的元素def partition(p: A => Boolean): (Repr, Repr) = {  val l, r = newBuilder  for(x <- this) (if(p(x)) l else r) += x  (l.result, r.result)} //根据谓词将可遍历集合分成两个集合,这两个集合中元素的顺序与原集合保持一致

折叠(fold)

折叠是从一个“种子”值开始,然后以该值作为上下文,处理集合中的每个元素

Scala容器类型提供的标准折叠方法有fold、foldLeft、foldRight、:\和/:,其均在scala.collection.TraversableOnce中定义,签名如下:

trait TraversableOnce[+A] extends Any with GenTraversableOnce[A] {  def foldLeft[B](z: B)(op: (B, A) => B): B = {    var result = z    this foreach (x => result = op(result, x))    result  } //使用二元操作符op对集合元素做折叠 def /:[B](z: B)(op: (B, A) => B) = foldLeft(z)(op) //foldLeft的别名  def fold[A1 >: A](z: A1)(op: (A1, A1) => A1) = foldLeft(z)(op)  def foldRight[B](z: B)(op: (A, B) => B): B = {    reversed.foldLeft(z)((x, y) => op(y, x))  }  def :\[B](z: B)(op: (A, B) => B): B = foldRight(z)(op) //foldRight的别名}

fold函数并不保证固定的遍历顺序(上述代码只是一种实现方案,标准中并未对其进行规定),而foldLeft则从左到右遍历元素,foldRight则从右到左遍历集合元素。

上述实现中,foldRight是reverse + foldLeft方法来实现,这意味着其会遍历两次集合,对于大集合而言可能非常缓慢。

fold方法可以输出一个与集合元素完全不同的值,且对无限集合均不终止处理。同时,如果集合不是序列类型(非序列类型中,元素的存储顺序不固定)或操作不满足交换律的,以上方法每次运行返回的结果可能都不相同。


归约(reduce)

与折叠类似,归约也可以将一个集合“缩小”成一个更小的集合或一个值。但归约不需要调用者提供初始值,它通常将集合的第一个或最后一个元素当作初始值。

Scala容器类型提供的标准归约方法有reduce、reduceOption、reduceLeft、reduceLeftOption、reduceRight、reduceRightOption,其均在scala.collection.TraversableOnce中定义,签名如下:

trait TraversableOnce[+A] extends Any with GenTraversableOnce[A] {  def reduceLeft[B >: A](op: (B, A) => B): B = {    if(isEmpty)      throw new UnsupportedOperationException("empty.reduceLeft")    var first = true    var acc: B = 0.asInstanceOf[B]    for(x <- self) {      if(first) {        acc = x        first = false      } else acc = op(acc, x)    }    acc  }  def reduceLeftOption[B >: A](op: (B, A) => B): Option[B] = if(isEmpty) None else Some(reduceLeft(op))  def reduce[A1 >: A](op: (A1, A1) => A1): A1 = reduceLeft(op)  def reduceOption[A1 >: A](op: (A1, A1) => A1): Option[A1] = reduceLeftOption(op)  def reduceRight[B >: A](op: (A, B) => B): B = {    if(isEmpty)      throw new UnsupportedOperationException("empty.reduceRight")    reversed.reduceLeft((x, y) => op(y, x))  }  def reduceRightOption[B >: A](op: (A, B) => B): Option[B] = if(isEmpty) None else Some(reduceRight(op))}

若reduce对空集合进行操作,则在运行时会抛出异常。为了保证可执行性可以使用Option Reduce函数来替代相应的reduce操作。

reduce函数不保证固定的遍历顺序(上述代码只是一种实现,标准中并未对其进行规定),而reduceLeft则从左到右遍历集合元素,reduceRight则从右到左遍历集合元素。

reduce方法总是返回与元素相同类型或父类型的值,且对无限集合均不终止处理。同时,如果集合不是序列类型(非序列类型中,元素的存储顺序不固定)或操作不满足交换律的,以上方法每次运行返回的结果可能都不相同。


参考《Scala程序设计》第二版

原创粉丝点击