Kitaplığımı zenginleştir desenini Scala koleksiyonlarına nasıl uygularım?


92

Scala mevcut olan en güçlü modellerden biri için örtülü dönüşümleri kullanıyor zenginleştirmek-my-kütüphane * deseni vardır görünür dinamik yöntemi çözünürlüğü gerektirmeden mevcut sınıflara yöntemleri eklemek. Örneğin, tüm dizelerin spaceskaç tane boşluk karakteri olduğunu sayan bir yönteme sahip olmasını dilersek, şunları yapabilirdik:

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

Ne yazık ki, bu model genel koleksiyonlarla uğraşırken sorun yaşıyor. Örneğin, öğeleri koleksiyonlarla sıralı olarak gruplama hakkında bir dizi soru soruldu . Tek seferde çalışan yerleşik hiçbir şey yoktur, bu nedenle bu, genel bir koleksiyon Cve genel bir öğe türü kullanarak kitaplığımı zenginleştir modeli için ideal bir aday gibi görünüyor 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
    }
  }
}

dışında tabii ki işe yaramıyor . REPL bize şunu söylüyor:

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

İki sorun var: C[C[A]]Boş bir C[A]listeden (veya havadan) nasıl bir tane alırız ? Ve hattan a yerine nasıl C[C[A]]geri alabiliriz ?same +:Seq[Seq[A]]

* Eskiden pezevenk kütüphanem olarak biliniyordu.


1
Harika soru! Ve daha da iyisi, bir cevabı var! :-)
Daniel C. Sobral

2
@Daniel - İki veya daha fazla cevapla gelmesine itirazım yok!
Rex Kerr

2
Unut gitsin dostum. Ne zaman böyle bir şey yapmam gerekse onu aramak için buna yer imi koyuyorum. :-)
Daniel C. Sobral

Yanıtlar:


75

Bu sorunu anlamanın anahtarı , koleksiyon kitaplığında koleksiyonlar oluşturmanın ve bunlarla çalışmanın iki farklı yolu olduğunu fark etmektir . Biri, tüm güzel yöntemleriyle genel koleksiyon arayüzü. Koleksiyon kitaplığının oluşturulmasında yoğun olarak kullanılan , ancak dışında neredeyse hiç kullanılmayan diğeri ise inşaatçılar.

Zenginleştirme konusundaki sorunumuz, aynı türden koleksiyonları döndürmeye çalışırken koleksiyon kitaplığının kendisinin karşılaştığı sorunla tamamen aynıdır. Yani, koleksiyonlar oluşturmak istiyoruz, ancak genel olarak çalışırken, "koleksiyonun halihazırda olduğu türden" bahsetmek için bir yolumuz yok. Bu yüzden inşaatçılara ihtiyacımız var .

Şimdi soru şu: inşaatçılarımızı nereden alıyoruz? Bariz yer koleksiyonun kendisidir. Bu çalışmıyor . Jenerik bir koleksiyona geçerken, koleksiyonun türünü unutmaya karar verdik. Dolayısıyla koleksiyon, istediğimiz türde daha fazla koleksiyon oluşturacak bir kurucu döndürebilseydi bile, türün ne olduğunu bilemezdi.

Bunun yerine, inşaatçılarımızı CanBuildFrometrafta dolaşan sonuçlardan alıyoruz . Bunlar, özellikle girdi ve çıktı türlerini eşleştirmek ve size uygun şekilde yazılmış bir kurucu sağlamak amacıyla mevcuttur.

Yani, yapmamız gereken iki kavramsal adım var:

  1. Standart tahsilat işlemlerini kullanmıyoruz, inşaatçılar kullanıyoruz.
  2. Bu oluşturucuları CanBuildFromdoğrudan koleksiyonumuzdan değil, örtük e- postalardan alıyoruz.

Bir örneğe bakalım.

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

Bunu parçalara ayıralım. İlk olarak, koleksiyon koleksiyonunu oluşturmak için iki tür koleksiyon oluşturmamız gerektiğini biliyoruz: C[A]her grup için ve C[C[A]]bu tüm grupları bir araya toplar. Bu nedenle, iki kurucuya ihtiyacımız var, biri As alan ve C[A]s yapan, diğeri de C[A]s alan ve derleyen biri C[C[A]]. Tip imzasına CanBuildFrombaktığımızda görüyoruz

CanBuildFrom[-From, -Elem, +To]

Bu, CanBuildFrom'un başladığımız koleksiyon türünü bilmek istediği anlamına gelir - bizim durumumuzda, bu C[A]ve sonra oluşturulan koleksiyonun öğeleri ve bu koleksiyonun türü. Bu yüzden bunları örtük parametreler olarak doldururuz cbfccve cbfc.

Bunu fark ettikten sonra, işin çoğu bu. Bizim kullanabilirsiniz CanBuildFrombize inşaatçılar vermek için s (yapmanız gereken tüm bunları uygulamak olduğunu). Ve bir inşaatçı, bir koleksiyon oluşturabilir +=, onu nihayetinde birlikte olması gereken koleksiyona dönüştürebilir resultve kendini boşaltarak yeniden başlamaya hazır olabilir clear. Oluşturucular boş başlarlar, bu da ilk derleme hatamızı çözer ve özyineleme yerine derleyiciler kullandığımızdan, ikinci hata da ortadan kalkar.

Son bir küçük ayrıntı - işi gerçekten yapan algoritma dışında - örtük dönüştürmede. Kullandığımız unutmayın new GroupingCollection[A,C]değildir [A,C[A]]. Bunun nedeni, sınıf bildiriminin Ckendisine Ailetilen ile kendisini doldurduğu tek bir parametre için olmasıdır. Bu yüzden ona sadece tipini veriyoruz Cve onu yaratmasına izin veriyoruz C[A]. Küçük ayrıntı, ancak başka bir yol denerseniz derleme zamanı hataları alırsınız.

Burada, yöntemi "eşit öğeler" koleksiyonundan biraz daha genel yaptım - daha ziyade, yöntem, sıralı öğelerin testi başarısız olduğunda orijinal koleksiyonu parçalara ayırıyor.

Yöntemimizi iş başında görelim:

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

İşe yarıyor!

Tek sorun, genel olarak diziler için bu yöntemlerin mevcut olmamasıdır, çünkü bu, arka arkaya iki örtük dönüşüm gerektirir. Diziler için ayrı bir örtük dönüşüm yazmak, dönüştürmek, vb. Dahil olmak üzere bunu aşmanın birkaç yolu vardır WrappedArray.


Düzenleme: Dizilerle ve dizelerle ve benzeri şeylerle uğraşmak için tercih ettiğim yaklaşımım, kodu daha genel hale getirmek ve ardından dizilerin de çalışacağı şekilde tekrar daha spesifik hale getirmek için uygun örtük dönüştürmeleri kullanmaktır. Bu özel durumda:

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

Burada bize bir verdiği örtük ekledik Iterable[A]dan C(örn Bora'nın en koleksiyonları bu sadece kimlik olacak List[A]zaten olduğunu Iterable[A]), ancak diziler için gerçek bir örtük dönüşüm olacak. Ve sonuç olarak, - C[A] <: Iterable[A]temelde sadece <%açık olan gereksinimi yerine getirdik , böylece derleyicinin bizim için doldurması yerine onu açıkça isteyerek kullanabiliriz. Ayrıca, koleksiyon koleksiyonumuzun - C[C[A]]bunun D[C]yerine, daha sonra istediğimiz gibi dolduracağımız herhangi bir koleksiyon olduğu şeklindeki kısıtlamayı gevşettik . Bunu daha sonra dolduracağımız için, metot seviyesi yerine sınıf seviyesine kadar yükselttik. Aksi takdirde, temelde aynıdır.

Şimdi soru bunun nasıl kullanılacağıdır. Normal koleksiyonlar için şunları yapabiliriz:

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

nerede şimdi takın C[A]için Cve C[C[A]]için D[C]. Çağrıda açık jenerik türlere ihtiyacımız olduğuna dikkat edin, new GroupingCollectionböylece hangi türlerin neye karşılık geldiğini açıklığa kavuşturabilir. Sayesinde implicit c2i: C[A] => Iterable[A], bu dizileri otomatik olarak işler.

Ama bekleyin, ya dizeleri kullanmak istersek? Şimdi başımız belada, çünkü bir "dizginiz" olamaz. Ekstra soyutlamanın yardımcı olduğu yer burasıdır: Ddizeleri tutmak için uygun olan bir şeyi arayabiliriz . VectorAşağıdakileri seçip yapalım :

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

Bir CanBuildFromdizi vektörünün oluşturulmasını idare etmek için yeniye ihtiyacımız var (ama bu gerçekten kolay, çünkü sadece aramamız gerekiyor Vector.newBuilder[String]) ve sonra GroupingCollectionmantıklı bir şekilde yazılabilmesi için tüm türleri doldurmamız gerekiyor . Zaten bir [String,Char,String]CanBuildFrom etrafında kayan bir sistemimiz olduğuna dikkat edin, böylece karakter koleksiyonlarından karakter dizileri oluşturulabilir.

Hadi deneyelim:

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

Diziler için destek eklemek için <% kullanabilirsiniz.
Anonim

@Anonymous - Biri bundan şüphelenir. Ama bu durumda denedin mi?
Rex Kerr

@Rex: "arka arkaya iki örtük dönüştürme gerektir" bana stackoverflow.com/questions/5332801/… 'i hatırlatıyor. Burada uygulanabilir mi?
Peter Schmitz

@Peter - Büyük olasılıkla! Yine de <% zincirlemeye dayanmak yerine açık örtük dönüşümler yazma eğilimindeyim.
Rex Kerr

@Peters yorumuna dayanarak diziler için başka bir örtük dönüştürme eklemeye çalıştım, ancak başarısız oldum. Görünüm sınırlarını nereye ekleyeceğimi gerçekten anlamadım. @Rex, lütfen cevabınızı düzenleyip kodun dizilerle nasıl çalışacağını gösterir misiniz?
kiritsuku

29

Bu taahhüt itibariyle Scala koleksiyonlarını "zenginleştirmek", Rex'in mükemmel cevabını verdiği zamandan çok daha kolay. Basit durumlar için şöyle görünebilir,

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)

filterMaptüm GenTraversableLikee işlemlere "aynı sonuç türü" ekleyen

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

Ve sorudaki örnek için, çözüm şimdi şöyle görünüyor:

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)

Örnek REPL oturumu,

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)

Yine, aynı sonuç türü ilkesinin, groupIdenticaldoğrudan tanımlanmış olacağı şekilde tam olarak aynı şekilde gözlemlendiğine dikkat edin GenTraversableLike.


3
Yaşasın! Orada daha da bu şekilde takip etmek için büyülü parçaları, ama hepsi güzel birleştirir! Koleksiyon dışı hiyerarşi koleksiyonlarının her biri için endişelenmenize gerek kalmaması rahatlatıcı.
Rex Kerr

3
Çok kötü Yineleyici, tek satırlık değişikliğim reddedildiği için nedensizce hariç tutuldu. "hata: scala.collection.generic.FromRepr [Iterator [Int]] türündeki kanıt parametresi için örtük değer bulunamadı"
psp

Hangi tek satırlık değişiklik reddedildi?
Miles Sabin


2
Bunu usta olarak görmüyorum; buharlaştı mı, yoksa 2.10.0 sonrası şubeye mi düştü, yoksa ...?
Rex Kerr

9

İtibariyle bu taahhüt sihirli büyü hafifçe Miles mükemmel bir cevap verdi halinin değiştirilir.

Aşağıdakiler işe yarıyor, ancak kanonik mi? Umarım kanonlardan biri bunu düzeltir. (Daha doğrusu, büyük toplardan biri olan toplar.) Görüntü sınırı bir üst sınırsa, Array ve String'e uygulamanızı kaybedersiniz. Bağlantının GenTraversableLike veya TraversableLike olup olmadığı önemli görünmüyor; ancak IsTraversableLike size bir GenTraversableLike verir.

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)

Dokuz cana sahip bir kedinin derisini yüzmenin birden fazla yolu vardır. Bu sürüm, kaynağım bir GenTraversableLike'a dönüştürüldüğünde, GenTraversable'dan sonucu oluşturabildiğim sürece, bunu yapmanı söylüyor. Eski cumhuriyetimle ilgilenmiyorum.

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)

Bu ilk deneme, Repr'in GenTraversableLike'a çirkin bir şekilde dönüştürülmesini içerir.

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)
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.