Làm cách nào để áp dụng mẫu thư viện của tôi cho các bộ sưu tập Scala?


92

Một trong những mô hình mạnh mẽ có sẵn trong Scala là * mẫu phong phú thêm-my-thư viện, trong đó sử dụng chuyển đổi ngầm để xuất hiện thêm các phương pháp đến các lớp học hiện tại mà không đòi hỏi phải có độ phân giải phương pháp năng động. Ví dụ: nếu chúng ta muốn rằng tất cả các chuỗi đều có phương thức spacesđếm chúng có bao nhiêu ký tự khoảng trắng, chúng ta có thể:

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

Thật không may, mẫu này gặp rắc rối khi xử lý các bộ sưu tập chung. Ví dụ, một số câu hỏi đã được đặt ra về việc nhóm các mục tuần tự với các bộ sưu tập . Không có gì được xây dựng trong đó hoạt động trong một lần, vì vậy đây có vẻ là một ứng cử viên lý tưởng cho mẫu thư viện của tôi làm giàu bằng cách sử dụng một bộ sưu tập chung Cvà một loại phần tử chung 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
    }
  }
}

ngoại trừ, tất nhiên, nó không hoạt động . REPL cho chúng ta biết:

<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ó hai vấn đề: làm thế nào để lấy một danh sách C[C[A]]trống C[A](hoặc từ không khí loãng)? Và làm thế nào để chúng ta lấy C[C[A]]lại từ same +:dòng thay vì a Seq[Seq[A]]?

* Trước đây được gọi là thư viện ma cô.


1
Câu hỏi tuyệt vời! Và, tốt hơn nữa, nó đi kèm với một câu trả lời! :-)
Daniel C. Sobral,

2
@Daniel - Tôi không phản đối việc có hai câu trả lời trở lên!
Rex Kerr

2
Quên nó đi, anh bạn. Tôi đánh dấu trang này để tra cứu bất cứ khi nào tôi cần làm điều gì đó như thế này. :-)
Daniel C. Sobral

Câu trả lời:


74

Chìa khóa để hiểu vấn đề này là nhận ra rằng có hai cách khác nhau để xây dựng và làm việc với các bộ sưu tập trong thư viện bộ sưu tập. Một là giao diện bộ sưu tập công khai với tất cả các phương pháp hay của nó. Cái khác, được sử dụng rộng rãi trong việc tạo thư viện bộ sưu tập, nhưng hầu như không bao giờ được sử dụng bên ngoài nó, là bộ xây dựng.

Vấn đề của chúng ta trong việc làm giàu giống hệt như vấn đề mà bản thân thư viện bộ sưu tập gặp phải khi cố gắng trả về các bộ sưu tập cùng loại. Đó là, chúng ta muốn xây dựng các bộ sưu tập, nhưng khi làm việc chung, chúng ta không có cách nào để tham chiếu đến "cùng một kiểu mà bộ sưu tập đã có". Vì vậy, chúng tôi cần những người xây dựng .

Bây giờ câu hỏi là: chúng ta lấy các nhà xây dựng của mình từ đâu? Nơi rõ ràng là từ chính bộ sưu tập. Điều này không hoạt động . Chúng tôi đã quyết định, khi chuyển sang một bộ sưu tập chung, rằng chúng tôi sẽ quên loại bộ sưu tập. Vì vậy, mặc dù bộ sưu tập có thể trả về một trình tạo sẽ tạo ra nhiều bộ sưu tập hơn thuộc loại chúng ta muốn, nó sẽ không biết loại đó là gì.

Thay vào đó, chúng tôi đưa các nhà xây dựng của mình từ CanBuildFrom ẩn ý đang trôi nổi xung quanh. Chúng tồn tại đặc biệt cho mục đích khớp các loại đầu vào và đầu ra và cung cấp cho bạn một trình tạo được đánh máy thích hợp.

Vì vậy, chúng tôi có hai bước nhảy vọt về khái niệm cần thực hiện:

  1. Chúng tôi không sử dụng các hoạt động tập hợp tiêu chuẩn, chúng tôi đang sử dụng trình tạo.
  2. Chúng tôi nhận được những nhà xây dựng này CanBuildFromtừ các bộ sưu tập ngầm , không phải từ bộ sưu tập của chúng tôi trực tiếp.

Hãy xem một ví dụ.

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)
}

Hãy tách nó ra. Đầu tiên, để xây dựng bộ sưu tập, chúng ta biết rằng chúng ta sẽ cần tạo hai loại bộ sưu tập: C[A]cho mỗi nhóm và C[C[A]]tập hợp tất cả các nhóm lại với nhau. Vì vậy, chúng ta cần hai trình xây dựng, một trình xây dựng lấy As và xây dựng C[A]s, và một C[A]trình xây dựng lấy s và xây dựng C[C[A]]s. Nhìn vào kiểu chữ ký của CanBuildFrom, chúng ta thấy

CanBuildFrom[-From, -Elem, +To]

có nghĩa là CanBuildFrom muốn biết loại bộ sưu tập mà chúng tôi đang bắt đầu - trong trường hợp của chúng tôi, đó là C[A], và sau đó là các phần tử của bộ sưu tập đã tạo và loại bộ sưu tập đó. Vì vậy, chúng tôi điền những thông số đó vào dưới dạng các thông số ngầm định cbfcccbfc .

Nhận ra điều này, đó là hầu hết công việc. Chúng tôi có thể sử dụng CanBuildFroms của chúng tôi để cung cấp cho chúng tôi các nhà xây dựng (tất cả những gì bạn cần làm là áp dụng chúng). Và một người xây dựng có thể xây dựng một bộ sưu tập với +=, chuyển đổi nó thành bộ sưu tập mà nó được cho là cuối cùng result, và tự làm trống và sẵn sàng bắt đầu lại vớiclear . Các trình xây dựng bắt đầu trống, điều này giải quyết lỗi biên dịch đầu tiên của chúng tôi và vì chúng tôi đang sử dụng trình xây dựng thay vì đệ quy, lỗi thứ hai cũng biến mất.

Một chi tiết nhỏ cuối cùng - ngoài thuật toán thực sự hoạt động - là ở chuyển đổi ngầm định. Lưu ý rằng chúng tôi new GroupingCollection[A,C]không sử dụng [A,C[A]]. Điều này là do khai báo lớp dành cho Cmột tham số, mà nó tự điền vào nó với giá trị Ađược truyền cho nó. Vì vậy, chúng tôi chỉ giao cho nó loại C, và để nó tạo C[A]ra từ nó. Chi tiết nhỏ, nhưng bạn sẽ gặp lỗi thời gian biên dịch nếu bạn thử cách khác.

Ở đây, tôi đã thực hiện phương pháp chung chung hơn một chút so với tập hợp "các phần tử bằng nhau" - đúng hơn, phương pháp này sẽ cắt tập hợp ban đầu ra bất cứ khi nào việc kiểm tra các phần tử tuần tự của nó không thành công.

Hãy xem phương pháp của chúng tôi đang hoạt động:

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))

Nó hoạt động!

Vấn đề duy nhất là chúng ta nói chung không có sẵn các phương thức này cho mảng, vì điều đó sẽ yêu cầu hai chuyển đổi ngầm định liên tiếp. Có một số cách để giải quyết vấn đề này, bao gồm viết một chuyển đổi ngầm định riêng cho các mảng, truyền sang WrappedArray, v.v.


Chỉnh sửa: Cách tiếp cận ưa thích của tôi để xử lý các mảng và chuỗi và như vậy là làm cho mã trở nên chung chung hơn và sau đó sử dụng các chuyển đổi ngầm định thích hợp để làm cho chúng cụ thể hơn theo cách mà các mảng cũng hoạt động. Trong trường hợp cụ thể này:

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
  }
}

Ở đây chúng tôi đã thêm một hàm ẩn cung cấp cho chúng tôi một Iterable[A]từ - đối với Chầu hết các bộ sưu tập, đây sẽ chỉ là danh tính (ví dụ: List[A]đã là một Iterable[A]), nhưng đối với mảng, nó sẽ là một chuyển đổi ngầm thực sự. Và do đó, chúng tôi đã bỏ yêu cầu - C[A] <: Iterable[A]về cơ bản chúng tôi chỉ đưa ra yêu cầu <%rõ ràng, vì vậy chúng tôi có thể sử dụng nó một cách rõ ràng theo ý muốn thay vì để trình biên dịch điền vào cho chúng tôi. Ngoài ra, chúng tôi đã nới lỏng hạn chế mà bộ sưu tập của chúng tôi là - đầu C[C[A]]tiên, nó là bất kỳ D[C], mà chúng tôi sẽ điền vào sau để trở thành những gì chúng tôi muốn. Vì chúng ta sẽ điền vào phần này sau, nên chúng tôi đã đẩy nó lên cấp lớp thay vì cấp phương thức. Nếu không, về cơ bản nó giống nhau.

Bây giờ câu hỏi là làm thế nào để sử dụng điều này. Đối với các bộ sưu tập thông thường, chúng tôi có thể:

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)
}

nơi bây giờ chúng tôi cắm C[A]cho CC[C[A]]cho D[C]. Lưu ý rằng chúng ta cần các kiểu chung chung rõ ràng trong lệnh gọi để new GroupingCollectionnó có thể giữ thẳng những kiểu nào tương ứng với cái gì. Nhờ có implicit c2i: C[A] => Iterable[A], điều này tự động xử lý các mảng.

Nhưng chờ đã, nếu chúng ta muốn sử dụng chuỗi thì sao? Bây giờ chúng tôi đang gặp rắc rối, bởi vì bạn không thể có một "chuỗi các chuỗi". Đây là nơi mà sự trừu tượng bổ sung sẽ giúp ích: chúng ta có thể gọi Dmột cái gì đó phù hợp để giữ các chuỗi. Hãy chọn Vectorvà làm như sau:

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
  )
}

Chúng ta cần một cái mới CanBuildFromđể xử lý việc xây dựng một vectơ chuỗi (nhưng điều này thực sự dễ dàng, vì chúng ta chỉ cần gọi Vector.newBuilder[String]), và sau đó chúng ta cần điền vào tất cả các loại để GroupingCollectionđược nhập hợp lý. Lưu ý rằng chúng ta đã trôi nổi xung quanh một [String,Char,String]CanBuildFrom, vì vậy các chuỗi có thể được tạo từ các tập hợp các ký tự.

Hãy thử nó ra:

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, !!)

Bạn có thể sử dụng <% để thêm hỗ trợ cho mảng.
Ẩn danh

@Anonymous - Người ta sẽ nghi ngờ như vậy. Nhưng bạn đã thử nó trong trường hợp này chưa?
Rex Kerr

@Rex: "yêu cầu hai chuyển đổi ngầm liên tiếp" nhắc tôi nhớ đến stackoverflow.com/questions/5332801/… Có thể áp dụng ở đây không?
Peter Schmitz

@Peter - Rất có thể! Tuy nhiên, tôi có xu hướng viết các chuyển đổi ngầm hiểu rõ ràng hơn là dựa vào chuỗi <%.
Rex Kerr

Dựa trên nhận xét của @Peters, tôi đã cố gắng thêm một chuyển đổi ngầm khác cho các mảng, nhưng tôi không thành công. Tôi thực sự không hiểu nơi để thêm giới hạn xem. @Rex, bạn có thể vui lòng chỉnh sửa câu trả lời của mình và chỉ cách lấy mã để làm việc với mảng được không?
kiritsuku

29

Theo cam kết này, việc "làm giàu" bộ sưu tập của Scala dễ dàng hơn rất nhiều so với khi Rex đưa ra câu trả lời xuất sắc của mình. Đối với những trường hợp đơn giản, nó có thể trông như thế này,

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)

bổ sung filterMapthao tác tôn trọng "cùng một loại kết quả" cho tất cả các GenTraversableLikes,

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

Và đối với ví dụ từ câu hỏi, giải pháp bây giờ giống như,

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)

Phiên REPL mẫu,

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)

Một lần nữa, lưu ý rằng nguyên tắc loại kết quả tương tự đã được quan sát theo cách giống hệt như cách mà nó đã groupIdenticalđược định nghĩa trực tiếp GenTraversableLike.


3
Yay! Có thậm chí nhiều hơn huyền diệu mảnh để theo dõi theo cách này, nhưng tất cả đều kết hợp độc đáo! Thật nhẹ nhõm khi không phải lo lắng về mỗi bộ sưu tập không phân cấp.
Rex Kerr

3
Quá tệ, Iterator bị loại trừ vô cớ vì thay đổi một dòng của tôi đã bị từ chối. "error: không thể tìm thấy giá trị ngầm định cho tham số bằng chứng của loại scala.collection.generic.FromRepr [Iterator [Int]]"
psp

Thay đổi một dòng nào đã bị từ chối?
Miles Sabin


2
Tôi không thấy điều này trong chủ; nó đã bay hơi, hay kết thúc ở một nhánh sau 2.10.0, hoặc ...?
Rex Kerr

9

Kể từ khi cam kết này , câu thần chú ma thuật có chút thay đổi so với khi Miles đưa ra câu trả lời xuất sắc của mình.

Những điều sau đây hoạt động, nhưng nó có phải là kinh điển không? Tôi hy vọng một trong những canons sẽ sửa nó. (Hay đúng hơn là đại bác, một trong những khẩu súng lớn.) Nếu giới hạn chế độ xem là giới hạn trên, bạn sẽ mất ứng dụng đối với Mảng và Chuỗi. Dường như không quan trọng nếu ràng buộc là GenTraversableLike hoặc TraversableLike; nhưng IsTraversableLike cung cấp cho bạn một 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)

Có nhiều cách để lột da một con mèo có chín mạng sống. Phiên bản này nói rằng một khi nguồn của tôi được chuyển đổi thành GenTraversableLike, miễn là tôi có thể tạo kết quả từ GenTraversable, chỉ cần làm điều đó. Tôi không quan tâm đến Repr cũ của mình.

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)

Nỗ lực đầu tiên này bao gồm một chuyển đổi xấu xí từ Repr thành 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)
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.