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


92

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로 알려졌습니다.


1
좋은 질문입니다! 그리고 더 좋은 점은 답이 있습니다! :-)
Daniel C. Sobral 2011 년

2
@ 다니엘-두 개 이상의 답변이 나오는 것에 대해 이의가 없습니다!
Rex Kerr

2
잊어 버려, 친구. 나는 이런 일을해야 할 때마다 찾아보기 위해 이것을 북마크하고 있습니다. :-)
Daniel C. Sobral 2011 년

답변:


74

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

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

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

대신, 우리 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]]합니다. 따라서 우리는 두 개의 빌더가 필요합니다. 하나는 As 를 취하고 s를 빌드 C[A]하고 다른 하나는 C[A]s 를 취하고 s를 빌드 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]]any이며 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, !!)

<%를 사용하여 어레이에 대한 지원을 추가 할 수 있습니다.
Anonymous

@Anonymous-하나는 그렇게 의심 할 것입니다. 하지만이 경우에 시도 했습니까?
Rex Kerr 2011 년

@Rex : "연속으로 두 개의 암시 적 변환이 필요합니다."라고하면 stackoverflow.com/questions/5332801/… 여기에 적용 할 수 있습니까?
Peter Schmitz 2011 년

@ 피터-아마도! 그래도 <% 체인에 의존하기보다는 명시 적 암시 적 변환을 작성하는 경향이 있습니다.
Rex Kerr

@Peters 주석을 기반으로 배열에 대한 또 다른 암시 적 변환을 추가하려고 시도했지만 실패했습니다. 뷰 경계를 추가 할 위치를 정말로 이해하지 못했습니다. @Rex, 답변을 편집하고 코드를 배열과 함께 사용하는 방법을 보여줄 수 있습니까?
kiritsuku 2011

29

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

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)

다시 말하지만, 동일한 결과 유형 원칙이 groupIdentical에서 직접 정의 되었을 때와 똑같은 방식으로 관찰 되었습니다 GenTraversableLike.


3
예이! 이런 식으로 추적 할 수있는 마법의 조각 이 더 많이 있지만 모두 멋지게 결합됩니다! 컬렉션이 아닌 각 컬렉션에 대해 걱정할 필요가 없다는 것은 안심입니다.
Rex Kerr

3
너무 나쁜 Iterator는 한 줄 변경이 거부되었으므로 무상으로 제외됩니다. "오류 : scala.collection.generic.FromRepr [Iterator [Int]] 유형의 증거 매개 변수에 대한 암시 적 값을 찾을 수 없습니다."
psp

어떤 한 줄 변경이 거부 되었습니까?
Miles Sabin


2
나는 이것을 마스터에서 보지 않는다; 증발 했나요, 아니면 2.10.0 이후 브랜치로 끝났나요, 아니면 ...?
Rex Kerr

9

현재 이 커밋 마법의 주문이 약간 마일 그의 뛰어난 답을 주었을 때 그것이 무엇 변경됩니다.

다음은 작동하지만 표준입니까? 나는 대포 중 하나가 그것을 수정하기를 바랍니다. (또는 큰 총기 중 하나 인 대포입니다.) 뷰 바운드가 상한이면 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)

9 명의 목숨을 가진 고양이를 가죽으로 만드는 방법은 여러 가지가 있습니다. 이 버전은 내 소스가 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)
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.