타입 파라미터화 => 제네릭 클래스/트레이트 사용 가능
Ex. 집합은 제네릭이며 타입 파라미터를 받기 때문에 타입이 Set[T]고 T를 반드시 명시해야 한다.
* 자바는 타입 파라미터를 쓰지 않아도 된다. ( Ex. ArrayList a = new ArrayList<>(); )
19.1 함수형 큐
- 세 연산을 제공하는 데이터 구조
1. head : 큐의 첫 원소를 반환한다.
2. tail : 큐의 첫 원소를 제외한 나머지를 반환한다.
3. enqueue : 인자로 주어진 원소를 큐의 맨 뒤에 추가한 새로운 큐를 반환한다.
- 변경 가능한 큐와 달리 원소를 추가해도 내용을 바꾸지 않는다.
- 효율적 구현을 통해 리스트와 마찬가지로 head, tail, enqueue가 모두 상수 시간이 걸리도록 한다.
=> head와 tail은 리스트 표현 타입을 통해 상수 시간 구현 가능 class SlowAppendQueue[T](elems: List[T]) { |
" enqueue 연산을 상수 시간에 하기 위해 리스트의 원소를 뒤집은 버전 " => head와 tail 비효율적 class SlowHeadQueue[T](smele: List[T]) { // Not efficient |
=> 위 두 가지 방법을 합친다.
=> leading과 trailing 두 리스트로 구현해 leading 리스트는 앞부분부터, trailing 리스트는 뒷부분부터 거꾸로 저장
전체 큐 내용은 leading ::: trailing.reverse
원소 추가 시 trailing 리스트에 넣고 head나 tail 연산 전에 빈 leading 리스트에 trailing 리스트를 복사(mirror 메소드)
class Queue[T]( private val leading: List[T], private val trailing: List[T] ) { private def mirror = // leading 리스트가 비어 있는 경우에만 원소 개수에 비례, 아닐 경우 상수 시간 if (leading.isEmpty) new Queue(trailing.reverse, Nil) else this def head = mirror.leading.head def tail = { val q = mirror new Queue(q.leading.tail, q.trailing) } def enqueue(x: T) = new Queue(leading, x :: trailing) } |
: tail 연산이 n번 호출될 때 mirror 메소드에서 n(원소 개수) 시간 소요 => 평균적으로 상수 시간
단, head/tail/enqueue가 거의 비슷한 빈도로 호출한다는 가정, head를 다른 두 연산보다 훨씬 더 많이 호출한다면, 매번 head를 실행할 때마다 비싼 비용을 들여 mirror 호출
19.2 정보 은닉
: Queue의 생성자에 누구나 접근할 수 있고 생성자 파라미터인 두 리스트 중 하나는 순서를 뒤집어야 하는 문제 => 클라이언트에게 코드를 감춰야 한다.
< 비공개 생성자와 팩토리 메소드 > => 클래스 초기화와 내부 표현을 감춘다.
class Queue[T] private ( private val leading: List[T], private val trailing: List[T] ) |
: 파라미터 목록 바로 앞에 private 수식자를 붙여서 주 생성자를 감출 수 있다.
scala> new Queue(List(1, 2), List(3)) :6: error: constructor Queue cannot be accessed in object $iw new Queue(List(1, 2), List(3)) |
=> 주 생성자를 호출할 수 없기 때문에
1. 보조 생성자 추가
def this() = this(Nil, Nil) def this(elems: T*) = this(elems.toList, Nil) |
2. 팩토리 메소드 추가
object Queue { // Queue(1,2,3) => Queue.apply(1,2,3) : 메소드가 객체에 속해 있지만 전역 메소드를 호출한 것과 같은 효과 |
< 대안 : 비공개 클래스 > => 클래스 자체를 감추고 클래스에 대한 공개 인터페이스만을 제공하는 트레이트를 외부로 노출
trait Queue[T] { def head: T def tail: Queue[T] def enqueue(x: T): Queue[T] } object Queue { def apply[T](xs: T*): Queue[T] = new QueueImpl[T](xs.toList, Nil) private class QueueImpl[T]( // 구현 클래스 전체를 감춘다. private val leading: List[T], private val trailing: List[T] ) extends Queue[T] { def mirror = if (leading.isEmpty) new QueueImpl(trailing.reverse, Nil) else this def head: T = mirror.leading.head def tail: QueueImpl[T] = { val q = mirror new QueueImpl(q.leading.tail, q.trailing) } def enqueue(x: T) = new QueueImpl(leading, x :: trailing) } } |
19.3 변성 표기
: 위에서 정의한 Queue는 트레이트이며 타입이 아니다. 타입이 아닌 이유는 타입 파라미터를 받기 때문
scala> def doesNotCompile(q: Queue) {} // Queue라는 타입의 변수를 만들 수 없다. :5: error: trait Queue takes type parameters def doesNotCompile(q: Queue) {} ^ |
scala> def doesCompile(q: Queue[AnyRef]) {} // Queue 트레이트는 Queue[Int], Queue[AnyRef] 처럼 파라미터화된 타입을 지정하도록 허용한다. |
=> Queue는 트레이트이고 Queue[String]은 타입 => Queue는 타입 생성자
: 타입 생성자인 Queue는 Queue[Int], Queue[String] 같은 일련의 타입을 생성한다.
: Queue는 제네릭 트레이트
* 제네릭 : 여러 타입을 고려해 포괄적으로 작성한 클래스/트레이트, 여러 구체적인 타입을 정의할 수 있다.
* 타입 파라미터를 받을 수 있는 클래스/트레이트를 제네릭, 그들이 만들어내는 타입은 파라미터화된 타입
=> Queue 트레이트는 제네릭 큐, Queue[Int]는 구체적 큐
: 스칼라에서 제네릭 타입은 기본적으로 무공변
=> Queue에서 원소 타입이 각기 다른 큐 사이에는 결코 서브타입 관계가 성립되지 않는다.
Ex. 위 deosCompile 메소드의 인자로 Queue[String]을 Queue[AnyRef] 대신 사용할 수 없다.
: 타입 파라미터 앞에 "+" 접두사를 붙이면 공변성을 요구할 수 있다.
trait Queue[+T] { ... } // 스칼라에게 Queue[String]을 Queue[AnyRef]의 서브타입으로 간주하라고 요구한다. 컴파일러는 Queue의 구현에서 해당 서브타입이 건전한지 검사한다. |
: 타입 파라미터 앞에 "-" 접두사를 붙이면 반공변성을 요구할 수 있다.
trait Queue[-T] { ... } // T가 S의 서브타입인 경우 Queue[S]가 Queue[T]의 서브타입 |
* 파라미터의 변성 : 어떤 타입 파라미터의 공변, 반공변, 무공변 여부
* 변성 표기 : 타입 파라미터에 붙일 수 있는 +나 - 기호
* 순수 함수형 세계에서 여러 타입은 태생적으로 공변적이지만 읽고 쓸 수 있는 변경 가능한 데이터는 그렇지 않다.
class Cell[+T](init: T) {
Cell.scala:7: error: covariant type T occurs in
val c1 = new Cell[String]("abc") |
< 변성과 배열 >
: 자바는 배열을 공변적으로 다룬다.
String[] a1 = { "abc" }; |
: 자바는 실행 시점에 원소의 타입을 저장 => 안전 X
* 제네릭이 나오기 전 배열을 제네릭하게 다룰 방법이 필요했기 때문, 제네릭이 생긴 이후에도 호환성 때문에 존재
Ex. void sort(Object[] a, Comparator cmp) { ... }
: 스칼라는 배열을 공변적으로 다루지 않는다.
scala> val a1 = Array("abc") a1: Array[java.lang.String] = Array(abc) scala> val a2: Array[Any] = a1 :5: error: type mismatch; found : Array[java.lang.String] required: Array[Any] val a2: Array[Any] = a1 |
: 자바와의 호환을 위해 T 타입의 배열을 T의 슈퍼타입 배열로 변환 가능
// 컴파일 시 항상 합법적, 자바 언어와 마찬가지로 공변적으로 다룬다. scala> val a2: Array[Object] = |
19.4 변성 표기 검사
" Queue가 공변적이라고 가정 "
class StrangeIntQueue extends Queue[Int] {
val x: Queue[Any] = new StrangeIntQueue
=> 실제 공변성을 가진 Queue를 컴파일하면 ( class Queue[+T] {
|
=> 제네릭 타입 파라미터가 메소드 파라미터의 타입이라면 그 메소드를 포함하는 클래스나 트레이트는 해당 타입 파라미터에 대해 공변적이지 않을 수 있다.
* 재할당 가능 필드도 마찬가지 ( var x : T 는 게터 메소드 def x: T와 세터 메소드 def x_=(y:T)로 취급 )
< 스칼라 컴파일러가 변성 표기를 검사하는 방법 >
: 컴파일러는 클래스 안에서 타입 파라미터를 사용하는 모든 부분이 긍정적/부정적/중립적인지 구분한다.
: +로 표시한 타입 파라미터는 긍정적인 위치에서만 사용 가능, -로 표시한 파라미터는 부정적 위치에서만 사용 가능, 아무 변성 표기가 없는 타입 파라미터는 아무 곳에서나 사용 가능
: 가장 바깥 스코프부터 내부 스코프로 긍정적/부정적/중립적인지 구분한다.
* 기본적으로 내부 스코프는 바깥 스코프와 구분이 같다.
* 메소드 값 파라미터, 메소드의 타입 파라미터는 바깥 스코프 구분과 반대
* 어떤 타입의 타입 인자의 구분은 해당 타입 파라미터의 변성에 따라 구분을 반대로 뒤집을 지 결정
Ex C[Arg]의 Arg에 +표기가 붙어 있으면 구분을 그대로 유지, -표기가 붙어 있으면 현재의 구분과 반대
abstract class Cat[-T, +U] { def meow[W-](volume: T-, listener: Cat[U+, T-]-) : Cat[Cat[U+, T-]-, U+]+ } |
19.5 하위 바운드
: Queue[T] 정의에서 T를 공변적으로 만들 수 없다. => T는 enqueue 메소드의 파라미터 타입인데 그 위치는 부정적
=> enqueue를 다형성(즉 enqueue에 타입 파라미터를 지정)으로 더 일반화하고, 타입 파라미터에 하위 바운드를 사용
class Queue[+T] (private val leading: List[T],
class Fruit
class StrangeQueue extends Queue[Apple](Nil, Nil ) {
val x: Queue[Fruit] = new StrangeQueue x.enqueue(new Orange) |
Ex. Fruit 클래스에 두 서브 클래스 Apple, Orange가 있을 때, Orange를 Queue[Apple]에 추가할 수 있다. 추가한 Queue의 타입은 Queue[Fruit]가 된다. => 더 일반적
: 자바는 사용자 위치 변성 => 어떤 클래스 설계가 단독으로 존재하고 실제 와일드 카드를 채워 넣는 것은 그 클래스를 사용하는 쪽이다. => 사용자 쪽에서 타입 다룰 때 조심(사용자 쪽에서 잘못 사용하면 중요한 인스턴스 메소드를 더 이상 사용할 수 없다.)
class Test<T>{} public class Sample { public static void main(String[] args) { Test<? extends String> test = new Test(); } } |
: 스칼라는 선언 위치 변성 => 설계할 때 타입을 넣음으로써 사용자가 타입으로 인한 실수를 하지 않도록 하고 컴파일러에서 메소드를 사용 가능한지 확인해준다.(타입 위주 설계)
19.6 반공변성
" 반공변 출력 채널 " trait OutputChannel[-T] { |
: OutputChannel[AnyRef]의 출력 채널은 OutputChannel[String]을 출력하는 채널의 서브 타입
: OutputChannel[AnyRef] 채널에는 아무 객체나 쓸 수 있지만 OutputChannel[String] 채널에는 오직 문자열만 쓸 수 있다. => OutputChannel[AnyRef]가 필요한 곳에 OutputChannel[String]을 바꿔 넣는 것은 안전 X
=> U 타입의 값이 필요한 모든 경우를 T 타입의 값으로 대치할 수 있다면, T 타입을 U 타입의 서브타입으로 가정해도 안전 : 리스코프 치환 원칙
=> T가 U의 모든 연산을 지원하고, 모든 T의 연산이 그에 대응하는 U의 연산에 비해 요구하는 것은 더 적고 제공하는 것은 더 많은 경우 리스코프 치환 원칙이 성립
Ex. OutputChannel[AnyRef]와 OutputChannel[String]이 같은 write 연산을 제공하고 OutputChannel[AnyRef]에 있는 연산이 OutputChannel[String]에 비해 더 적은 것을 요구하기 때문에 리스코프 치환 원칙 성립(더 적다는 건 전자는 단지 AnyRef만을 요구하는 반면, 후자는 제약이 더 많고 구체적인 String을 요구한다.)
" 한 타입 안에 공변성과 반공변성이 섞여 있는 함수 트레이트 " trait Function1[-S, +T] { |
: 인자는 함수가 요구하는 것, 결과는 함수가 제공하는 것 => S는 반공변, T는 공변
class Publication(val title: String) class Book(title: String) extends Publication(title) object Library { val books: Set[Book] = Set( new Book("Programming in Scala"), new Book("Walden") ) def printBookList(info: Book => AnyRef) { for (book <- books) println(info(book)) } } object Customer extends App { def getTitle(p: Publication): String = p.title Library.printBookList(getTitle) } |
19.7 객체의 비공개 데이터
: Queue에서 leading이 비어있는 데 head를 여러 번 호출했을 때 mirror 연산이 trailing 리스트를 leading 리스트로 반복해서 복사하는 문제 => 부수 효과를 활용해 개선
class Queue[+T] private ( private[this] var leading: List[T], // 재할당 가능한 var 변수 => 동반 객체에도 보이지 않는다. private[this] var trailing: List[T] // 재할당 가능한 var 변수 ) { private def mirror() = if (leading.isEmpty) { while (!trailing.isEmpty) { leading = trailing.head :: leading // 현재의 큐 변경 trailing = trailing.tail // 현재의 큐 변경 } } def head: T = { mirror() leading.head } def tail: Queue[T] = { mirror() new Queue(leading.tail, trailing) } def enqueue[U >: T](x: U) = new Queue[U](leading, x :: trailing) } |
: leading, trailing이 비공개 변수여서 부수효과는 순전히 Queue 내부에서만 나타난다. => 순수 함수형 객체
: private[this]는 그들이 정의된 객체에서만 접근 가능해 변성에 아무 문제를 일으키지 않는다.(변성이 타입 오류를 발생시키는 경우를 만들어내려면 어떤 객체가 정의되는 시점의 타입보다 정적으로 더 약한 타입의 객체에 대한 참조가 객체 내부에 필요하다/ 하지만 객체 비공개 값에 접근할 경우 이런 일이 불가능하다.)
=> 타입 파라미터를 사용하는 부분이 긍정적/부정적/중립적인지 private[this]를 제외하고 검사한다.
19.8 상위 바운드
" Ordered 트레이트를 혼합한 Person 클래스 " class Person(val firstName: String, val lastName: String)
" 상위 바운드를 사용한 병합 정렬 - Ordered 트레이트를 믹스인한 모든 타입에 대해 정렬 수행 가능 " * Int는 Ordered[Int]의 서브타입이 아니기 때문에 제한 def orderedMergeSort[T <: Ordered[T]](xs: List[T]): List[T] = { |
* 스칼라는 비즈니스 로직에 초점을 맞추지 않는다.(내부 비즈니스 로직은 고차함수를 통해 미리 제공되어 있다.) 타입에 초점을 맞춰야 한다. 그래서 클래스나 트레이트를 쉽게 정의할 수 있는 방법 존재 => 설계의 시작은 타입
* 믹스인 트레이트는 기능에 불과 => 믹스인 트레이트 타입 대신 상위 바운드 사용
'스칼라' 카테고리의 다른 글
스칼라 21장 암시적 변환과 암시적 파라미터(Programming in Scala, 3rd) (0) | 2019.06.23 |
---|---|
스칼라 20장 추상 멤버(Programming in Scala, 3rd) (0) | 2019.06.07 |
스칼라 18장 변경 가능한 객체(Programming in Scala, 3rd) (0) | 2019.06.07 |
스칼라 17장 컬렉션(Programming in Scala, 3rd) (0) | 2019.06.06 |
스칼라 16장 리스트(Programming in Scala, 3rd) (0) | 2019.06.02 |