簡體   English   中英

如何將enrich-my-library模式應用於Scala集合?

[英]How do I apply the enrich-my-library pattern to Scala collections?

一個Scala中最強大的模式是充實,我的圖書館*模式,它采用隱式轉換出現添加方法,以現有的類,而不需要動態方法解析。 例如,如果我們希望所有字符串都有方法spaces來計算它們有多少個空白字符,我們可以:

class SpaceCounter(s: String) {
  def spaces = s.count(_.isWhitespace)
}
implicit def string_counts_spaces(s: String) = new SpaceCounter(s)

scala> "How many spaces do I have?".spaces
res1: Int = 5

不幸的是,這種模式在處理泛型集合時遇到了麻煩。 例如,已經詢問了許多關於按順序對項目進行分組的問題。 沒有內置的東西可以一次性工作,所以這似乎是使用泛型集合C和泛型元素類型A富 - 我的庫模式的理想候選者:

class SequentiallyGroupingCollection[A, C[A] <: Seq[A]](ca: C[A]) {
  def groupIdentical: C[C[A]] = {
    if (ca.isEmpty) C.empty[C[A]]
    else {
      val first = ca.head
      val (same,rest) = ca.span(_ == first)
      same +: (new SequentiallyGroupingCollection(rest)).groupIdentical
    }
  }
}

當然,除了它不起作用 REPL告訴我們:

<console>:12: error: not found: value C
               if (ca.isEmpty) C.empty[C[A]]
                               ^
<console>:16: error: type mismatch;
 found   : Seq[Seq[A]]
 required: C[C[A]]
                 same +: (new SequentiallyGroupingCollection(rest)).groupIdentical
                      ^

有兩個問題:我們如何從空C[A]列表(或從空中)獲得C[C[A]] 我們如何從same +:行而不是Seq[Seq[A]]返回C[C[A]] Seq[Seq[A]]

* 以前稱為pimp-my-library。

理解這個問題的關鍵是要意識到在集合庫中有兩種不同的方法來構建和使用集合 一個是公共集合界面及其所有不錯的方法。 另外,這是在創建集合庫廣泛使用,但幾乎從未在其外部使用,是建設者。

我們在豐富方面的問題與集合庫本身在嘗試返回相同類型的集合時面臨的問題完全相同。 也就是說,我們想要構建集合,但是當一般地工作時,我們沒有辦法引用“集合已經存在的相同類型”。 所以我們需要建設者

現在的問題是:我們從哪里獲得建築商? 顯而易見的地方來自收藏品本身。 這不起作用 在轉向通用集合時,我們已經決定忘記集合的類型。 因此,即使集合可以返回一個構建器,該構建器將生成我們想要的類型的更多集合,但它不知道該類型是什么。

相反,我們從CanBuildFrom我們的建設者得到CanBuildFrom暗示。 它們專門用於匹配輸入和輸出類型,並為您提供適當類型的構建器。

因此,我們有兩個概念上的飛躍:

  1. 我們沒有使用標准集合操作,我們正在使用構建器。
  2. 我們從隱式CanBuildFrom獲取這些構建器,而不是直接從我們的集合中獲取。

我們來看一個例子。

class GroupingCollection[A, C[A] <: Iterable[A]](ca: C[A]) {
  import collection.generic.CanBuildFrom
  def groupedWhile(p: (A,A) => Boolean)(
    implicit cbfcc: CanBuildFrom[C[A],C[A],C[C[A]]], cbfc: CanBuildFrom[C[A],A,C[A]]
  ): C[C[A]] = {
    val it = ca.iterator
    val cca = cbfcc()
    if (!it.hasNext) cca.result
    else {
      val as = cbfc()
      var olda = it.next
      as += olda
      while (it.hasNext) {
        val a = it.next
        if (p(olda,a)) as += a
        else { cca += as.result; as.clear; as += a }
        olda = a
      }
      cca += as.result
    }
    cca.result
  }
}
implicit def iterable_has_grouping[A, C[A] <: Iterable[A]](ca: C[A]) = {
  new GroupingCollection[A,C](ca)
}

讓我們分開吧。 首先,為了構建集合集合,我們知道我們需要構建兩種類型的集合:每個集合的C[A]和集合所有組的C[C[A]] 因此,我們需要兩個助洗劑,一種采用A S和構建C[A] s和一個取C[A] S和構建C[C[A]]秒。 看看CanBuildFrom的類型簽名,我們看到了

CanBuildFrom[-From, -Elem, +To]

這意味着CanBuildFrom想知道我們開始的集合類型 - 在我們的例子中,它是C[A] ,然后是生成的集合的元素和該集合的類型。 所以我們將它們作為隱式參數cbfcccbfc

意識到這一點,這是大部分工作。 我們可以使用我們的CanBuildFrom來為我們提供構建器(您需要做的就是應用它們)。 並且一個構建器可以使用+=構建一個集合,將其轉換為最終應該與result的集合,並將其自身清空並准備好以clear重新開始。 構建器開始為空,這解決了我們的第一個編譯錯誤,並且因為我們使用構建器而不是遞歸,所以第二個錯誤也消失了。

最后一個小細節 - 除了實際完成工作的算法之外 - 是隱式轉換。 請注意,我們使用new GroupingCollection[A,C]而不是[A,C[A]] 這是因為類的聲明是為C帶有一個參數,它填充它本身帶有A傳遞給它。 所以我們只需將它交給C ,然后讓它創建C[A] 一些細節,但如果你嘗試另一種方式,你會得到編譯時錯誤。

在這里,我使方法比“等元素”集合更通用 - 相反,只要對順序元素的測試失敗,該方法就會將原始集合分開。

讓我們看看我們的方法:

scala> List(1,2,2,2,3,4,4,4,5,5,1,1,1,2).groupedWhile(_ == _)
res0: List[List[Int]] = List(List(1), List(2, 2, 2), List(3), List(4, 4, 4), 
                             List(5, 5), List(1, 1, 1), List(2))

scala> Vector(1,2,3,4,1,2,3,1,2,1).groupedWhile(_ < _)
res1: scala.collection.immutable.Vector[scala.collection.immutable.Vector[Int]] =
  Vector(Vector(1, 2, 3, 4), Vector(1, 2, 3), Vector(1, 2), Vector(1))

有用!

唯一的問題是我們通常沒有這些方法可用於數組,因為這需要連續兩次隱式轉換。 有幾種方法可以解決這個問題,包括為數組編寫單獨的隱式轉換,轉換為WrappedArray等等。


編輯:我最喜歡的處理數組和字符串的方法是使代碼通用,然后使用適當的隱式轉換使它們更具體,使數組也能工作。 在這種特殊情況下:

class GroupingCollection[A, C, D[C]](ca: C)(
  implicit c2i: C => Iterable[A],
           cbf: CanBuildFrom[C,C,D[C]],
           cbfi: CanBuildFrom[C,A,C]
) {
  def groupedWhile(p: (A,A) => Boolean): D[C] = {
    val it = c2i(ca).iterator
    val cca = cbf()
    if (!it.hasNext) cca.result
    else {
      val as = cbfi()
      var olda = it.next
      as += olda
      while (it.hasNext) {
        val a = it.next
        if (p(olda,a)) as += a
        else { cca += as.result; as.clear; as += a }
        olda = a
      }
      cca += as.result
    }
    cca.result
  }
}

在這里我們添加了一個隱式的,它給了我一個來自CIterable[A] - 對於大多數集合來說,這只是身份(例如List[A]已經是Iterable[A] ),但是對於數組,它將是一個真正的隱式轉換。 因此,我們已經放棄了C[A] <: Iterable[A]的要求 - 我們基本上只需要<% explicit,因此我們可以隨意使用它而不是編譯器填充它適合我們。 此外,我們已經放寬了我們收藏品集合C[C[A]]限制 - 而且,它是任何D[C] ,我們將在后面填寫它們是我們想要的。 因為我們稍后將填寫此內容,所以我們已將其推升到類級別而不是方法級別。 否則,它基本相同。

現在的問題是如何使用它。 對於常規館藏,我們可以:

implicit def collections_have_grouping[A, C[A]](ca: C[A])(
  implicit c2i: C[A] => Iterable[A],
           cbf: CanBuildFrom[C[A],C[A],C[C[A]]],
           cbfi: CanBuildFrom[C[A],A,C[A]]
) = {
  new GroupingCollection[A,C[A],C](ca)(c2i, cbf, cbfi)
}

其中,現在我們插入C[A]CC[C[A]]D[C] 請注意,我們在調用new GroupingCollection確實需要顯式泛型類型,因此它可以直接保持哪些類型對應於什么。 由於implicit c2i: C[A] => Iterable[A] ,這會自動處理數組。

但是等等,如果我們想要使用字符串怎么辦? 現在我們遇到了麻煩,因為你不能擁有一串“字符串”。 這是額外抽象有用的地方:我們可以調用D適合保存字符串的東西。 讓我們選擇Vector ,並執行以下操作:

val vector_string_builder = (
  new CanBuildFrom[String, String, Vector[String]] {
    def apply() = Vector.newBuilder[String]
    def apply(from: String) = this.apply()
  }
)

implicit def strings_have_grouping(s: String)(
  implicit c2i: String => Iterable[Char],
           cbfi: CanBuildFrom[String,Char,String]
) = {
  new GroupingCollection[Char,String,Vector](s)(
    c2i, vector_string_builder, cbfi
  )
}

我們需要一個新的CanBuildFrom來處理字符串向量的構建(但這很簡單,因為我們只需要調用Vector.newBuilder[String] ),然后我們需要填寫所有類型,以便GroupingCollection是理智地打字。 請注意,我們已經在[String,Char,String] CanBuildFrom周圍浮動,因此可以從字符集合中創建字符串。

我們來試試吧:

scala> List(true,false,true,true,true).groupedWhile(_ == _)
res1: List[List[Boolean]] = List(List(true), List(false), List(true, true, true))

scala> Array(1,2,5,3,5,6,7,4,1).groupedWhile(_ <= _) 
res2: Array[Array[Int]] = Array(Array(1, 2, 5), Array(3, 5, 6, 7), Array(4), Array(1))

scala> "Hello there!!".groupedWhile(_.isLetter == _.isLetter)
res3: Vector[String] = Vector(Hello,  , there, !!)

此提交中 ,“充實”Scala集合要比Rex給出的出色答案要容易得多。 對於簡單的情況,它可能看起來像這樣,

import scala.collection.generic.{ CanBuildFrom, FromRepr, HasElem }
import language.implicitConversions

class FilterMapImpl[A, Repr](val r : Repr)(implicit hasElem : HasElem[Repr, A]) {
  def filterMap[B, That](f : A => Option[B])
    (implicit cbf : CanBuildFrom[Repr, B, That]) : That = r.flatMap(f(_).toSeq)
}

implicit def filterMap[Repr : FromRepr](r : Repr) = new FilterMapImpl(r)

它將filterMap操作的“相同結果類型” filterMap到所有GenTraversableLike

scala> val l = List(1, 2, 3, 4, 5)
l: List[Int] = List(1, 2, 3, 4, 5)

scala> l.filterMap(i => if(i % 2 == 0) Some(i) else None)
res0: List[Int] = List(2, 4)

scala> val a = Array(1, 2, 3, 4, 5)
a: Array[Int] = Array(1, 2, 3, 4, 5)

scala> a.filterMap(i => if(i % 2 == 0) Some(i) else None)
res1: Array[Int] = Array(2, 4)

scala> val s = "Hello World"
s: String = Hello World

scala> s.filterMap(c => if(c >= 'A' && c <= 'Z') Some(c) else None)
res2: String = HW

對於問題的例子,解決方案現在看起來像,

class GroupIdenticalImpl[A, Repr : FromRepr](val r: Repr)
  (implicit hasElem : HasElem[Repr, A]) {
  def groupIdentical[That](implicit cbf: CanBuildFrom[Repr,Repr,That]): That = {
    val builder = cbf(r)
    def group(r: Repr) : Unit = {
      val first = r.head
      val (same, rest) = r.span(_ == first)
      builder += same
      if(!rest.isEmpty)
        group(rest)
    }
    if(!r.isEmpty) group(r)
    builder.result
  }
}

implicit def groupIdentical[Repr : FromRepr](r: Repr) = new GroupIdenticalImpl(r)

示例REPL會話,

scala> val l = List(1, 1, 2, 2, 3, 3, 1, 1)
l: List[Int] = List(1, 1, 2, 2, 3, 3, 1, 1)

scala> l.groupIdentical
res0: List[List[Int]] = List(List(1, 1),List(2, 2),List(3, 3),List(1, 1))

scala> val a = Array(1, 1, 2, 2, 3, 3, 1, 1)
a: Array[Int] = Array(1, 1, 2, 2, 3, 3, 1, 1)

scala> a.groupIdentical
res1: Array[Array[Int]] = Array(Array(1, 1),Array(2, 2),Array(3, 3),Array(1, 1))

scala> val s = "11223311"
s: String = 11223311

scala> s.groupIdentical
res2: scala.collection.immutable.IndexedSeq[String] = Vector(11, 22, 33, 11)

同樣,請注意,已經觀察到相同的結果類型原則,與在groupIdentical上直接定義GenTraversableLike

這個提交中 ,魔術咒語與Miles給出的出色答案略有不同。

以下作品,但它是規范的嗎? 我希望其中一個經典能夠糾正它。 (或者說,大炮,大槍之一。)如果視圖綁定是上限,則會丟失對Array和String的應用程序。 如果綁定是GenTraversableLike或TraversableLike似乎並不重要; 但IsTraversableLike為您提供了GenTraversableLike。

import language.implicitConversions
import scala.collection.{ GenTraversable=>GT, GenTraversableLike=>GTL, TraversableLike=>TL }
import scala.collection.generic.{ CanBuildFrom=>CBF, IsTraversableLike=>ITL }

class GroupIdenticalImpl[A, R <% GTL[_,R]](val r: GTL[A,R]) {
  def groupIdentical[That](implicit cbf: CBF[R, R, That]): That = {
    val builder = cbf(r.repr)
    def group(r: GTL[_,R]) {
      val first = r.head
      val (same, rest) = r.span(_ == first)
      builder += same
      if (!rest.isEmpty) group(rest)
    }
    if (!r.isEmpty) group(r)
    builder.result
  }
}

implicit def groupIdentical[A, R <% GTL[_,R]](r: R)(implicit fr: ITL[R]):
  GroupIdenticalImpl[fr.A, R] =
  new GroupIdenticalImpl(fr conversion r)

只有一種方法可以讓有九條生命的貓皮膚美化。 這個版本說,一旦我的源轉換為GenTraversableLike,只要我可以從GenTraversable構建結果,就這樣做。 我對我的舊Repr不感興趣。

class GroupIdenticalImpl[A, R](val r: GTL[A,R]) {
  def groupIdentical[That](implicit cbf: CBF[GT[A], GT[A], That]): That = {
    val builder = cbf(r.toTraversable)
    def group(r: GT[A]) {
      val first = r.head
      val (same, rest) = r.span(_ == first)
      builder += same
      if (!rest.isEmpty) group(rest)
    }
    if (!r.isEmpty) group(r.toTraversable)
    builder.result
  }
}

implicit def groupIdentical[A, R](r: R)(implicit fr: ITL[R]):
  GroupIdenticalImpl[fr.A, R] =
  new GroupIdenticalImpl(fr conversion r)

第一次嘗試包括將Repr轉換為GenTraversableLike。

import language.implicitConversions
import scala.collection.{ GenTraversableLike }
import scala.collection.generic.{ CanBuildFrom, IsTraversableLike }

type GT[A, B] = GenTraversableLike[A, B]
type CBF[A, B, C] = CanBuildFrom[A, B, C]
type ITL[A] = IsTraversableLike[A]

class FilterMapImpl[A, Repr](val r: GenTraversableLike[A, Repr]) { 
  def filterMap[B, That](f: A => Option[B])(implicit cbf : CanBuildFrom[Repr, B, That]): That = 
    r.flatMap(f(_).toSeq)
} 

implicit def filterMap[A, Repr](r: Repr)(implicit fr: ITL[Repr]): FilterMapImpl[fr.A, Repr] = 
  new FilterMapImpl(fr conversion r)

class GroupIdenticalImpl[A, R](val r: GT[A,R])(implicit fr: ITL[R]) { 
  def groupIdentical[That](implicit cbf: CBF[R, R, That]): That = { 
    val builder = cbf(r.repr)
    def group(r0: R) { 
      val r = fr conversion r0
      val first = r.head
      val (same, other) = r.span(_ == first)
      builder += same
      val rest = fr conversion other
      if (!rest.isEmpty) group(rest.repr)
    } 
    if (!r.isEmpty) group(r.repr)
    builder.result
  } 
} 

implicit def groupIdentical[A, R](r: R)(implicit fr: ITL[R]):
  GroupIdenticalImpl[fr.A, R] = 
  new GroupIdenticalImpl(fr conversion r)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM