IT story

Enrich-my-library 패턴을 Scala 컬렉션에 어떻게 적용합니까?

hot-time 2020. 9. 1. 08:02
반응형

Enrich-my-library 패턴을 Scala 컬렉션에 어떻게 적용합니까?


Scala에서 사용할 수있는 가장 강력한 패턴 중 하나는 enrich-my-library * 패턴으로, 암시 적 변환을 사용하여 동적 메서드 확인없이 기존 클래스에 메서드를 추가 하는 것처럼 보입니다 . 예를 들어, 모든 문자열에 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과 일반 요소 유형을 사용하는 Enrich-my-library 패턴에 이상적인 후보 인 것 같습니다 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[C[A]]C[A]목록에서 (또는 허공에서) 어떻게 습니까? 그리고 우리는 어떻게 대신 라인 C[C[A]]에서 되 찾을 수 있습니까?same +:Seq[Seq[A]]

* 이전에는 pimp-my-library로 알려졌습니다.


이 문제를 이해하는 열쇠 는 컬렉션 라이브러리에서 컬렉션을 만들고 작업하는 두 가지 다른 방법 이 있음을 깨닫는 것입니다 . 하나는 모든 멋진 메소드가있는 공용 컬렉션 인터페이스입니다. 컬렉션 라이브러리 만드는 데 광범위하게 사용 되지만 외부에서 거의 사용되지 않는 다른 하나는 빌더입니다.

보강에 대한 우리의 문제는 동일한 유형의 컬렉션을 반환하려고 할 때 컬렉션 라이브러리 자체가 직면하는 것과 정확히 동일합니다. 즉, 컬렉션을 빌드하고 싶지만 일반적으로 작업 할 때 "컬렉션이 이미있는 것과 동일한 유형"을 참조 할 방법이 없습니다. 그래서 우리는 건축업자 가 필요합니다 .

이제 문제는 건축업자를 어디서 구할 수 있는가입니다. 명백한 장소는 컬렉션 자체입니다. 작동하지 않습니다 . 우리는 이미 일반적인 컬렉션으로 이동하면서 컬렉션의 유형을 잊어 버리겠다고 결정했습니다. 따라서 컬렉션이 원하는 유형의 더 많은 컬렉션을 생성하는 빌더를 반환 할 수 있지만 유형이 무엇인지 알 수 없습니다.

대신, 우리 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들과 빌드 C[A]들, 그리고 소요 한 C[A]들 및 빌드 C[C[A]]들. 의 유형 서명을 CanBuildFrom보면

CanBuildFrom[-From, -Elem, +To]

이는 CanBuildFrom이 우리가 시작하는 컬렉션의 유형을 알고 싶어한다는 것을 의미합니다. 우리의 경우에는이고 C[A]생성 된 컬렉션의 요소와 해당 컬렉션의 유형입니다. 그래서 우리는 그것들을 암시 적 매개 변수 cbfcccbfc.

이것을 깨달은 것이 대부분의 작업입니다. 우리는 CanBuildFroms를 사용하여 빌더를 제공 할 수 있습니다 (적용하기 만하면됩니다). 그리고 한 빌더는를 사용하여 컬렉션을 구축하고 +=궁극적으로 함께 있어야하는 컬렉션으로 변환하고 result자체를 비우고에서 다시 시작할 준비를 할 수 있습니다 clear. 빌더는 비어있는 상태에서 시작하여 첫 번째 컴파일 오류를 해결하고 재귀 대신 빌더를 사용하므로 두 번째 오류도 사라집니다.

실제로 작업을 수행하는 알고리즘 이외의 마지막 세부 사항은 암시 적 변환에 있습니다. new GroupingCollection[A,C]not 을 사용 [A,C[A]]합니다. 이는 클래스 선언이 C하나의 매개 변수 에 대한 것이기 때문이며 A전달 된 매개 변수로 자체적으로 채워집니다 . 그래서 우리는 그것에 유형을 건네 C주고 그것을 생성 C[A]하도록합니다. 사소한 세부 사항이지만 다른 방법을 시도하면 컴파일 시간 오류가 발생합니다.

여기서는 "equal elements"컬렉션보다 좀 더 일반적인 메서드를 만들었습니다. 오히려이 메서드는 순차 요소 테스트가 실패 할 때마다 원본 컬렉션을 분리합니다.

우리의 방법이 작동하는 것을 보자 :

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

여기에 Iterable[A]from 을 제공하는 암시 C적을 추가 했습니다. 대부분의 컬렉션에서 이것은 ID 일뿐 (예 : List[A]이미 Iterable[A])이지만 배열의 경우 실제 암시 적 변환이됩니다. 결과적으로 우리는 C[A] <: Iterable[A]기본적으로 <%명시 적 요구 사항을 만들었 기 때문에 컴파일러가 우리를 대신하여 채우는 대신 명시 적으로 사용할 수 있다는 요구 사항을 삭제 했습니다. 또한 컬렉션 컬렉션이 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, !!)

현재 이 커밋 은 렉스 그의 훌륭한 대답을 준 때보다 "풍부하게"스칼라 컬렉션에 훨씬 쉽다. 간단한 경우에는 다음과 같이 보일 수 있습니다.

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모든 동작 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)

Again, note that the same result type principle has been observed in exactly the same way that it would have been had groupIdentical been directly defined on GenTraversableLike.


As of this commit the magic incantation is slightly changed from what it was when Miles gave his excellent answer.

The following works, but is it canonical? I hope one of the canons will correct it. (Or rather, cannons, one of the big guns.) If the view bound is an upper bound, you lose application to Array and String. It doesn't seem to matter if the bound is GenTraversableLike or TraversableLike; but IsTraversableLike gives you a 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)

There's more than one way to skin a cat with nine lives. This version says that once my source is converted to a GenTraversableLike, as long as I can build the result from GenTraversable, just do that. I'm not interested in my old 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)

This first attempt includes an ugly conversion of Repr to 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)

참고URL : https://stackoverflow.com/questions/5410846/how-do-i-apply-the-enrich-my-library-pattern-to-scala-collections

반응형