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
는 주위를 떠 다니는 암시 적 요소 로부터 빌더를 얻습니다 . 이는 입력 및 출력 유형을 일치시키고 적절한 유형의 빌더를 제공하기 위해 특별히 존재합니다.
따라서 우리는 두 가지 개념적 도약을해야합니다.
- 우리는 표준 컬렉션 작업을 사용하지 않고 빌더를 사용하고 있습니다.
- 이러한 빌더는
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, !!)
답변
현재 이 커밋 은 렉스 그의 훌륭한 대답을 준 때보다 “풍부하게”스칼라 컬렉션에 훨씬 쉽다. 간단한 경우에는 다음과 같이 보일 수 있습니다.
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
.
답변
현재 이 커밋 마법의 주문이 약간 마일 그의 뛰어난 답을 주었을 때 그것이 무엇 변경됩니다.
다음은 작동하지만 표준입니까? 나는 대포 중 하나가 그것을 수정하기를 바랍니다. (또는 큰 총기 중 하나 인 대포입니다.) 뷰 바운드가 상한이면 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)