1. 程式人生 > >【Scala型別系統】函式式Queue的簡易實現

【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的變化型檢查規則包含了關於物件私有定義的特例。當檢查到帶有+/-號的型別引數只出現在具有相同變化型分類的位置上時,這種定義將被忽略。