스칼라 : 추상 타입과 제네릭


답변:


257

이 문제에 대한 좋은 견해는 다음과 같습니다.

Bill Venners와 Frank Sommers의
Part III, Martin Odersky와의 스칼라 타입 시스템 A 대화 의 목적
(2009 년 5 월 18 일)

업데이트 (2009 년 10 월) : 아래의 다음 무엇을 실제로 빌 베너 스에 의해이 새로운 기사에서 설명하고있다 :
스칼라에서 제네릭 형식 매개 변수 대 추상 형식 회원 (마지막에 요약 참조)


(2009 년 5 월, 첫 번째 인터뷰의 관련 내용은 다음과 같습니다.)

일반적인 원리

항상 두 가지 추상화 개념이있었습니다.

  • 매개 변수화
  • 추상 회원.

Java에는 둘 다 있지만 추상적 인 내용에 따라 다릅니다.
Java에는 추상 메소드가 있지만 메소드를 매개 변수로 전달할 수 없습니다.
추상 필드는 없지만 값을 매개 변수로 전달할 수 있습니다.
마찬가지로 추상 형식 멤버는 없지만 형식을 매개 변수로 지정할 수 있습니다.
Java에는 세 가지가 모두 있지만, 어떤 종류의 것들에 어떤 추상화 원칙을 사용할 수 있는지에 대한 구별이 있습니다. 그리고 당신은이 구별이 상당히 임의적이라고 주장 할 수 있습니다.

스칼라 웨이

우리는 세 종류의 구성원 모두에 대해 동일한 구성 원칙을 갖기로 결정했습니다 .
따라서 값 필드뿐만 아니라 추상 필드도 가질 수 있습니다.
메소드 (또는 "함수")를 매개 변수로 전달하거나 메소드를 추상화 할 수 있습니다.
유형을 매개 변수로 지정하거나 해당 유형을 추상화 할 수 있습니다.
우리가 개념적으로 얻는 것은 하나를 다른 하나의 관점에서 모델링 할 수 있다는 것입니다. 적어도 원칙적으로 모든 종류의 매개 변수화를 객체 지향 추상화 형태로 표현할 수 있습니다. 어떤 의미에서 스칼라는 더 직교적이고 완전한 언어라고 말할 수 있습니다.

왜?

특히 추상 유형이 당신을 구입하는 것은 이전에 이야기했던 이러한 공분산 문제에 대한 훌륭한 치료법입니다 .
오랫동안 존재해온 하나의 표준 문제는 동물과 음식의 문제입니다.
퍼즐은 몇 가지 음식을 먹는 Animal방법 으로 수업 을하는 것이 었습니다 eat.
문제는 우리가 Animal을 서브 클래 싱하고 Cow와 같은 클래스를 가지고 있다면, 그들은 임의의 음식이 아닌 잔디 만 먹을 것입니다. 예를 들어 소는 물고기를 먹을 수 없었습니다.
당신이 원하는 것은 암소가 잔디 만 먹고 다른 것은 먹지 않는 먹는 방법을 가지고 있다고 말할 수 있다는 것입니다.
사실, Java에서 그렇게 할 수는 없습니다. 왜냐하면 앞에서 이야기했던 Apple 변수에 과일을 할당하는 문제와 같이 건전하지 않은 상황을 만들 수 있기 때문입니다.

답은 추상적 인 타입을 Animal 클래스에 추가 한다는 입니다.
내 새 Animal 클래스의 유형은 SuitableFood알 수 없습니다.
추상적 인 타입입니다. 유형의 구현을 제공하지 않습니다. 그럼 당신은 eat먹는 방법이 SuitableFood있습니다.
그리고 다음에 Cow클래스 I는 OK, I 클래스 확장하는 암소가, 말을 Animal하고, 위해를 Cow type SuitableFood equals Grass.
따라서 추상 유형은 내가 모르는 수퍼 클래스에서 이러한 유형의 개념을 제공하며, 나중에 내가 아는 것으로 서브 클래스에서 채 웁니다 .

매개 변수화와 동일합니까?

실제로 당신은 할 수 있습니다. 먹는 음식의 종류를 동물 클래스에 매개 변수로 지정할 수 있습니다.
그러나 실제로 많은 다른 것들로 그렇게 할 때, 그것은 매개 변수의 폭발로 이어지고 일반적으로 더 많은 매개 변수의 경계로 이어 집니다.
1998 년 ECOOP에서 Kim Bruce, Phil Wadler와 저는 알지 못하는 것들의 수를 늘리면 전형적인 프로그램이 2 차적으로 성장할 것이라는 논문을 보았습니다 .
따라서 이차 폭발을 일으키지 않기 때문에 매개 변수를 수행하지 말고 이러한 추상 멤버를 갖는 이유는 매우 좋습니다.


thatismatt 는 의견을 묻습니다.

다음은 공정한 요약이라고 생각하십니까?

  • 추상 유형은 'has-a'또는 'uses-a'관계에 사용됩니다 (예 : a Cow eats Grass).
  • 제네릭은 보통 관계 '의'(예를 그대로 곳 List of Ints)

추상 형식이나 제네릭을 사용하는 것의 관계가 다른지 확실하지 않습니다. 다른 점은 다음과 같습니다.

  • 그것들이 어떻게 사용되는지, 그리고
  • 파라미터 바운드 관리 방법

마틴은 오는 "매개 변수의 폭발, 보통, 무엇에, 더 경우에 대해 말하는 것을 이해하기 위해 매개 변수의 범위 "및 추상 형식이 제네릭을 사용하여 모델링하는 경우 그 이후 차적으로 성장, 당신은 종이 "고려할 수있는 확장 가능한 구성 요소 추상화 "Partcom 프로젝트 (2007 년에 완성)에서 발간 된 Martin Odersky와 OOPSLA 2005의 Matthias Zenger에 의해 작성되었습니다 .

관련 추출물

정의

추상 유형 멤버 는 구체적인 유형의 구성 요소를 유연하게 추상화 할 수있는 방법을 제공합니다.
추상 형식은 SML 서명 에서의 사용과 유사하게 구성 요소의 내부에 대한 정보를 숨길 수 있습니다 . 상속을 통해 클래스를 확장 할 수있는 객체 지향 프레임 워크에서 유연한 매개 변수화 수단으로 사용될 수도 있습니다 (종종 가족 다형성이라고 함, 예를 들어이 웹 로그 항목Eric Ernst가 작성한 논문 참조 ).

(참고 : 재사용 가능하지만 형식이 안전한 상호 재귀 클래스를 지원하기위한 솔루션으로 객체 지향 언어에 대해 패밀리 다형성이 제안되었습니다. 가족 다형성
의 핵심 아이디어는 상호 재귀 클래스를 그룹화하는 데 사용되는 패밀리 개념입니다.)

바운드 타입 추상화

abstract class MaxCell extends AbsCell {
type T <: Ordered { type O = T }
def setMax(x: T) = if (get < x) set(x)
}

여기서 T형식 선언은 클래스 이름 Ordered 및 refinement로 구성된 상한 형식 바운드로 제한됩니다{ type O = T } .
상한은 서브 클래스에서 T의 특수화가 유형 멤버 O가 있는 Ordered의 서브 타입으로 제한 equals T됩니다.
이러한 제약 때문에 <Ordered 클래스 의 메서드는 수신자와 T 유형의 인수에 적용 할 수 있습니다.
이 예에서는 경계 형식 멤버 자체가 경계의 일부로 나타날 수 있음을 보여줍니다.
(즉, Scala는 F-bounded polymorphism을 지원합니다 )

(참고 : Peter Canning, William Cook, Walter Hill, Walter Olthoff 논문 :
Cardelli 및 Wegner는 지정된 유형의 모든 하위 유형에 대해 균일하게 작동하는 타이핑 기능의 수단으로 경계 수량화를 도입했습니다.
간단한 "객체"모델을 정의했습니다 . "속성". 지정된 세트를 가진 모든 객체에 메이크업 감각 있고 입력 체크하는 기능을 제한된 정량을 사용
의 요소 개체 수있는 것이 객체 지향 언어의보다 현실적인 표현 재귀 적 정의 유형 .
이러한 맥락에서, 경계 정량화는 더 이상 의도 된 목적에 부합하지 않으며, 지정된 일련의 분석법을 가지고 있지만 Cardelli-Wegner 시스템에 입력 할 수없는 모든 객체에 적합한 기능을 쉽게 찾을 수 있습니다.
객체 지향 언어로 형식화 된 다형성 함수에 대한 기초를 제공하기 위해 F- 바운드 정량화를 소개합니다)

같은 동전의 두 얼굴

프로그래밍 언어에는 두 가지 주요 추상화 형태가 있습니다.

  • 매개 변수화
  • 추상 회원.

첫 번째 형식은 기능적 언어에 일반적이며 두 번째 형식은 일반적으로 객체 지향 언어에 사용됩니다.

전통적으로 Java는 값에 대한 매개 변수화와 조작에 대한 멤버 추상화를 지원합니다. 제네릭이 포함 된 최신 Java 5.0은 유형에 대해서도 매개 변수화를 지원합니다.

스칼라에 제네릭을 포함시키는 데 대한 주장은 두 가지입니다.

  • 첫째, 추상 형식으로 인코딩하는 것은 수작업으로 간단하지 않습니다. 간결함의 손실 외에도 형식 매개 변수를 에뮬레이트하는 추상 형식 이름간에 우연한 이름 충돌 문제가 있습니다.

  • 둘째, 제네릭과 추상 형식은 일반적으로 스칼라 프로그램에서 고유 한 역할을합니다.

    • 제네릭은 하나의 단지 필요로 할 때 일반적으로 사용되는 유형의 인스턴스를 하는 반면,
    • 추상 유형 은 일반적으로 클라이언트 코드에서 추상 유형참조 해야 할 때 사용됩니다 .
      후자는 특히 두 가지 상황에서 발생합니다.
    • 클라이언트 코드에서 형식 멤버의 정확한 정의를 숨기고 SML 스타일 모듈 시스템에서 알려진 일종의 캡슐화를 얻을 수 있습니다.
    • 또는 패밀리 다형성을 얻기 위해 서브 클래스에서 유형을 공변 적으로 재정의 할 수 있습니다.

경계 다형성이있는 시스템에서 추상 형식을 제네릭으로 다시 작성 하면 형식 경계2 차 확장 이 수반 될 수 있습니다 .


2009 년 10 월 업데이트

스칼라의 추상 형식 멤버와 일반 형식 매개 변수 (빌 공급 업체)

(강조 광산)

지금까지 추상 형식 멤버 에 대한 나의 관찰 은 다음과 같은 경우에 일반 형식 매개 변수보다 더 나은 선택이라는 것입니다.

  • 당신은 사람들이 특성을 통해 이러한 유형의 정의를 혼합 하게하고 싶습니다 .
  • 형식 멤버 이름을 명시 적으로 언급하면 ​​코드 가독성에 도움이된다고 생각합니다 .

예:

세 개의 다른 조명기 객체를 테스트에 전달하려면 그렇게 할 수 있지만 각 매개 변수마다 하나씩 세 가지 유형을 지정해야합니다. 따라서 type 매개 변수 접근 방식을 취했으면 스위트 클래스가 다음과 같이 보일 수 있습니다.

// Type parameter version
class MySuite extends FixtureSuite3[StringBuilder, ListBuffer, Stack] with MyHandyFixture {
  // ...
}

타입 멤버 접근 방식은 다음과 같습니다.

// Type member version
class MySuite extends FixtureSuite3 with MyHandyFixture {
  // ...
}

추상 형식 멤버와 일반 형식 매개 변수의 또 다른 사소한 차이점은 일반 형식 매개 변수를 지정하면 코드 판독기에 형식 매개 변수의 이름이 표시되지 않는다는 것입니다. 따라서 누군가이 코드 줄을 보았습니다.

// Type parameter version
class MySuite extends FixtureSuite[StringBuilder] with StringBuilderFixture {
  // ...
}

그들은 StringBuilder로 지정된 type 매개 변수의 이름이 그것을 찾지 않고 무엇인지 알 수 없었습니다. 형식 매개 변수의 이름은 추상 형식 멤버 접근 방식의 코드에 있습니다.

// Type member version
class MySuite extends FixtureSuite with StringBuilderFixture {
  type FixtureParam = StringBuilder
  // ...
}

후자의 경우 코드 독자 StringBuilder는 "fixture parameter"유형 임을 알 수 있습니다 .
그들은 여전히 ​​"fixture parameter"가 무엇을 의미하는지 알아 내야하지만, 문서를 보지 않고도 최소한 타입의 이름을 얻을 수 있습니다.


61
당신이 와서 이것을 할 때 스칼라 질문에 대답하여 업장 점수를 어떻게 얻습니까 ??? :-)
Daniel C. Sobral

7
Hi Daniel : 매개 변수화에 비해 추상 유형의 장점을 보여주는 구체적인 예가 있어야한다고 생각합니다. 이 글타래에 글을 올리는 것이 좋은 시작이 될 것입니다.)
VonC

1
당신이 다음을 생각 하는가하는 것은 공정한 요약이다 : 추상 유형에 사용되는 것은 또는 '-A가'을 '사용-A'제네릭 관계 '의'보통으로 관계 (예 : 암소 먹는 잔디) (예를 들어, 목록하는 int)
thatismatt

추상 형식이나 제네릭을 사용하는 것의 관계가 다른지 확실하지 않습니다. 다른 점은 이들이 사용되는 방식과 매개 변수 경계가 관리되는 방식입니다. 잠시 후 내 답변에 더 많은.
VonC

1
자기 소개 : 2010 년 5 월 블로그 게시물 : daily-scala.blogspot.com/2010/05/…
VonC

37

스칼라에 대해 읽을 때도 같은 질문이있었습니다.

제네릭 사용의 이점은 유형 패밀리를 작성한다는 것입니다. 아무도 서브 클래 싱 할 필요가 없습니다 Buffer- 그들은 그냥 사용할 수 있습니다 Buffer[Any], Buffer[String]

추상 유형을 사용하는 경우 사람들은 서브 클래스를 작성해야합니다. 사람들은 같은 클래스가 필요합니다 AnyBuffer, StringBuffer

특정 요구에 더 적합한 것을 결정해야합니다.


18
음 엷게이 전면에 많이, 당신은 단지 요구할 수 개선 Buffer { type T <: String }또는 Buffer { type T = String }필요에 따라
에두아르도 파레 Tobes

21

추상 매개 변수를 유형 매개 변수와 함께 사용하여 사용자 정의 템플리트를 설정할 수 있습니다.

세 가지 특성이 연결된 패턴을 설정해야한다고 가정 해 봅시다.

trait AA[B,C]
trait BB[C,A]
trait CC[A,B]

형식 매개 변수에 언급 된 인수가 AA, BB, CC 자체를 존중하는 방식으로

다음과 같은 코드가 제공 될 수 있습니다.

trait AA[B<:BB[C,AA[B,C]],C<:CC[AA[B,C],B]]
trait BB[C<:CC[A,BB[C,A]],A<:AA[BB[C,A],C]]
trait CC[A<:AA[B,CC[A,B]],B<:BB[CC[A,B],A]]

유형 매개 변수 본드 때문에이 간단한 방식으로 작동하지 않습니다. 올바르게 상속하려면 공변량으로 만들어야합니다.

trait AA[+B<:BB[C,AA[B,C]],+C<:CC[AA[B,C],B]]
trait BB[+C<:CC[A,BB[C,A]],+A<:AA[BB[C,A],C]]
trait CC[+A<:AA[B,CC[A,B]],+B<:BB[CC[A,B],A]]

이 샘플은 컴파일되지만 분산 규칙에 대한 강력한 요구 사항을 설정하며 경우에 따라 사용할 수 없습니다

trait AA[+B<:BB[C,AA[B,C]],+C<:CC[AA[B,C],B]] {
  def forth(x:B):C
  def back(x:C):B
}
trait BB[+C<:CC[A,BB[C,A]],+A<:AA[BB[C,A],C]] {
  def forth(x:C):A
  def back(x:A):C
}
trait CC[+A<:AA[B,CC[A,B]],+B<:BB[CC[A,B],A]] {
  def forth(x:A):B
  def back(x:B):A
}

컴파일러는 다양한 분산 검사 오류로 이의를 제기합니다.

이 경우 추가 특성의 모든 유형 요구 사항을 수집하고 그 밖의 특성을 매개 변수화 할 수 있습니다.

//one trait to rule them all
trait OO[O <: OO[O]] { this : O =>
  type A <: AA[O]
  type B <: BB[O]
  type C <: CC[O]
}
trait AA[O <: OO[O]] { this : O#A =>
  type A = O#A
  type B = O#B
  type C = O#C
  def left(l:B):C
  def right(r:C):B = r.left(this)
  def join(l:B, r:C):A
  def double(l:B, r:C):A = this.join( l.join(r,this), r.join(this,l) )
}
trait BB[O <: OO[O]] { this : O#B =>
  type A = O#A
  type B = O#B
  type C = O#C
  def left(l:C):A
  def right(r:A):C = r.left(this)
  def join(l:C, r:A):B
  def double(l:C, r:A):B = this.join( l.join(r,this), r.join(this,l) )
}
trait CC[O <: OO[O]] { this : O#C =>
  type A = O#A
  type B = O#B
  type C = O#C
  def left(l:A):B
  def right(r:B):A = r.left(this)
  def join(l:A, r:B):C
  def double(l:A, r:B):C = this.join( l.join(r,this), r.join(this,l) )
}

이제 우리는 설명 된 패턴에 대한 구체적인 표현을 작성하고 모든 클래스에서 left 및 join 메소드를 정의하고 무료로 오른쪽 및 이중을 얻을 수 있습니다

class ReprO extends OO[ReprO] {
  override type A = ReprA
  override type B = ReprB
  override type C = ReprC
}
case class ReprA(data : Int) extends AA[ReprO] {
  override def left(l:B):C = ReprC(data - l.data)
  override def join(l:B, r:C) = ReprA(l.data + r.data)
}
case class ReprB(data : Int) extends BB[ReprO] {
  override def left(l:C):A = ReprA(data - l.data)
  override def join(l:C, r:A):B = ReprB(l.data + r.data)
}
case class ReprC(data : Int) extends CC[ReprO] {
  override def left(l:A):B = ReprB(data - l.data)
  override def join(l:A, r:B):C = ReprC(l.data + r.data)
}

따라서 추상 유형과 유형 매개 변수는 모두 추상을 작성하는 데 사용됩니다. 둘 다 약점과 장점이 있습니다. 추상 유형은보다 구체적이며 모든 유형 구조를 설명 할 수 있지만 자세하고 명시 적으로 지정해야합니다. 형식 매개 변수는 여러 유형을 즉시 만들 수 있지만 상속 및 형식 범위에 대한 추가 걱정을 줄 수 있습니다.

그것들은 서로에게 시너지 효과를주고 그것들 중 하나만으로는 표현 될 수없는 복잡한 추상화를 생성하기 위해 함께 사용될 수 있습니다.


0

나는 여기에 큰 차이가 없다고 생각합니다. 유형 추상 멤버는 다른 기능적 언어의 레코드 유형과 유사한 실존 적 유형으로 볼 수 있습니다.

예를 들면 다음과 같습니다.

class ListT {
  type T
  ...
}

class List[T] {...}

그런 다음 ListT과 동일합니다 List[_]. 타입 멤버의 확신은 명시적인 구체적 타입없이 클래스를 사용할 수 있고 너무 많은 타입 파라미터를 피할 수 있다는 것입니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.