【Scala类型系统】函数式Queue的简易实现

实现一个函数式Queue泛型类

函数式队列是一种具有以下三种操作方式的数据结构:

head 返回队列的第一个元素
tail 返回除第一个元素之外的队列
append 返回尾部添加了指定元素的新队列

如果Queue是一个不变队列,也就是函数式队列。在添加元素的时候不会改变其内容,而是返回包含了这个元素的新队列。
如果Queue是可变类型的,那么append操作将改变队列的内容。
纯函数式队列和List具有相似性,都支持head和tail操作。可以通过List来实现函数式队列。

关于时间开销问题,append操作应该在常量时间内完成。
为了做到这一点,我们使用两个List,分别称为leading和trailing,来表达队列。leading包含前段元素,而trailing包含了反向排列的后段元素。队列在任何时刻的所有内容都可以表示为leading ::: trailing.reverse

  • 想要添加新元素,只要使用::操作符将其添加到trailing,使得append是常量时间。
  • 当原始的空队列通过后继的append操作构建起来时,trailing将不断增加,而leading始终是空白的。于是,在对空的leading第一次执行head或者tail操作之前,trailing应该被反转并复制给leading,这个操作称为mirror。
  • mirror操作花费的时间大概与队列的元素数量成正比,但仅在leading为空时。如果leading非空,它将直接返回。
  • 因为head和tail调用了mirror,所以他们的复杂度与队列长度呈线性关系。实际上,假设leading为空时,mirror才翻转并复制,那么n个元素需要进行n次tail之后才进行复制。这n次tail操作平均分担了mirror操作的时间复杂度,也相当于常量时间了。

代码如下:

class Queue[T] (private val leading: List[T],
                private val trailing: List[T])
{
  private def mirror =
    if (leading.isEmpty)
      new Queue(trailing.reverse, Nil)
    else
      this

  def head = mirror.leading.head

  def tail = {
    val q = mirror
    new Queue(q.leading.tail, q.trailing)
  }

  def append(element: T) =
    new Queue(leading, element :: trailing)
}

信息隐藏

私有构造器和工厂方法

上面实现的Queue以暴露本不该暴露的实现细节为代价,全局可访问Queue构造器,带有两个列表参数,不能作为直观表达队列的形式。
私有构造器和私有成员是隐藏类的初始化代码和表达代码的一种方式。
可以通过把private修饰符添加在类参数列表的前面把主构造器隐藏起来。

class Queue[T] private (private val leading: List[T],
                        private val trailing: List[T])

构造器是私有的,它只能被类本身及伴生对象访问。我们可以添加可以用初始元素序列创建队列的工厂方法。定义与类同名的Queue对象及apply方法:

object Queue {
  def apply[T](xs: T*) = new Queue[T](xs.toList, Nil)
}

私有类

使用私有类的方法可以更彻底的把类本身隐藏掉,仅提供能够暴露类公共接口的特质。

trait Queue[T] {
  def head: T
  def tail: Queue[T]
  def append(x: T): Queue[T]
}

object Queue {
  def apply[T](xs: T*): Queue[T] =
    new QueueImpl[T](xs.toList, Nil)

  private class QueueImpl[T](
                             private val leading: List[T],
                             private val trailing: List[T]
                               ) extends Queue[T]
  {
    def mirror =
      if (leading.isEmpty)
        new QueueImpl(trailing.reverse, Nil)
      else
        this

    def head: T = mirror.leading.head

    def tail: QueueImpl[T] = {
      val q = mirror
      new QueueImpl(q.leading.tail, q.trailing)
    }

    def append(x: T) =
      new QueueImpl(leading, x:: trailing)
  }
}

代码中定义了特质Queue,声明了方法head、tail和append。
这三个方法都实现在子类QueueImpl中,而它本身是对象Queue的内部类。这个方案暴露给客户的信息与前面相同,但使用了不同的技术。代之以逐个隐藏构造器与方法,这个版本隐藏全部实现类。

实现协变的泛型类

使用Queue[+T]方式对Queue实现协变,然而在append的实现中,T参数出现在了逆变的位置。
可以通过把append变为多态以使其泛型化(即提供给append方法类型参数)并使用它的类型参数的下界。

class Queue[+T] private (
    private[this] var leading: List[T],
    private[this] var trailing: List[T]
) {
  def append[U >: T](x: U) =
    new Queue[U](leading, x::trailing)
}

通过U >: T定义了T为U的下界。结果U必须是T的超类型。
假设存在类Fruit和两个子类,Apple和Orange。通过Queue类的定义,可以吧Orange对象加入到Queue[Apple],结果返回Queue[Fruit]类型。

对象私有数据

到目前为止,Queue类仍有一些问题。如果head被一遍遍的调用很多次,而leading列表为空,那么mirror操作可能会重复的把trailing复制到leading列表。
可以将leading和trailing指定为可以重新复制的变量,而mirror从trailing反向复制到leading的操作是在当前队列上的副作用,而不再返回新的队列。
通过将leading和trailing用private[this]修饰,声明为对象私有变量,使得这种副作用纯粹是Queue操作的内部实现,从而使它对于Queue的客户不可见。
代码如下:

class Queue[+T] private (
                        private[this] var leading: List[T],
                        private[this] var trailing: List[T]
                          ) {
  private def mirror() =
    if(leading.isEmpty) {
      while(!trailing.isEmpty) {
        leading = trailing.head :: leading
        trailing = trailing.tail
      }
    }

  def head: T = {
    mirror()
    leading.head
  }

  def tail: Queue[T] = {
    mirror()
    new Queue(leading.tail, trailing)
  }

  def append[U >: T](x: U) =
    new Queue[U](leading, x::trailing)
}

说明:
被定义在同一个对象内访问对象私有变量不会引起与变化型有关的问题。Scala的变化型检查规则包含了关于对象私有定义的特例。当检查到带有+/-号的类型参数只出现在具有相同变化型分类的位置上时,这种定义将被忽略。

转载请注明作者Jason Ding及其出处
jasonding.top
Github博客主页(http://blog.jasonding.top/)
CSDN博客(http://blog.csdn.net/jasonding1354)
简书主页(http://www.jianshu.com/users/2bd9b48f6ea8/latest_articles)
Google搜索jasonding1354进入我的博客主页

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容