이 문제를 이해하는 열쇠 는 컬렉션 라이브러리에서 컬렉션을 만들고 작업하는 두 가지 다른 방법 이 있음을 깨닫는 것입니다 . 하나는 모든 멋진 메소드가있는 공용 컬렉션 인터페이스입니다. 컬렉션 라이브러리 를 만드는 데 광범위하게 사용 되지만 외부에서 거의 사용되지 않는 다른 하나는 빌더입니다.
보강에 대한 우리의 문제는 동일한 유형의 컬렉션을 반환하려고 할 때 컬렉션 라이브러리 자체가 직면하는 것과 정확히 동일합니다. 즉, 컬렉션을 빌드하고 싶지만 일반적으로 작업 할 때 "컬렉션이 이미있는 것과 동일한 유형"을 참조 할 방법이 없습니다. 그래서 우리는 건축업자 가 필요합니다 .
이제 문제는 건축업자를 어디서 구할 수 있는가입니다. 명백한 장소는 컬렉션 자체입니다. 작동하지 않습니다 . 우리는 이미 일반적인 컬렉션으로 이동하면서 컬렉션의 유형을 잊어 버리겠다고 결정했습니다. 따라서 컬렉션이 원하는 유형의 더 많은 컬렉션을 생성하는 빌더를 반환 할 수 있지만 유형이 무엇인지 알 수 없습니다.
대신, 우리 CanBuildFrom
는 주위를 떠 다니는 암시 적 요소 로부터 빌더를 얻습니다 . 이는 입력 및 출력 유형을 일치시키고 적절한 유형의 빌더를 제공하기 위해 특별히 존재합니다.
따라서 우리는 두 가지 개념적 도약을해야합니다.
- 우리는 표준 컬렉션 작업을 사용하지 않고 빌더를 사용하고 있습니다.
- 이러한 빌더는
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 를 취하고 s를 빌드 C[A]
하고 다른 하나는 C[A]
s 를 취하고 s를 빌드 C[C[A]]
합니다. 의 유형 서명을 CanBuildFrom
보면
CanBuildFrom[-From, -Elem, +To]
이는 CanBuildFrom이 우리가 시작하는 컬렉션의 유형을 알고 싶어한다는 것을 의미합니다. 우리의 경우에는이고 C[A]
생성 된 컬렉션의 요소와 해당 컬렉션의 유형입니다. 그래서 우리는 그것들을 암시 적 매개 변수 cbfcc
와 cbfc
.
이것을 깨달은 것이 대부분의 작업입니다. 우리는 CanBuildFrom
s를 사용하여 빌더를 제공 할 수 있습니다 (적용하기 만하면됩니다). 그리고 한 빌더는를 사용하여 컬렉션을 구축하고 +=
궁극적으로 함께 있어야하는 컬렉션으로 변환하고 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]
에 대한 C
과 C[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, !!)