2. 스칼라 기본¶
2.1. 제네릭 타입의 변성 및 공변적인 파라미터 타입¶
스칼라에서는 타입 파라미터를 통해 다양한 타입 생성을 지원한다. 제네릭 클래스/트레이트 에 특정 타입을 넘겨주면 파라미터화된 타입을 생성할 수 있다. 파라미터화된 타입이나 평범한 타입(타입 파라미터가 없는)을 사용해 인스턴스를 생성할 수 있다.
타입 파라미터 예시:
object Main extends App {
val q = Queue(1,2,3,4,5,6,7).enqueue(10).tail
println(q.toString)
}
class Queue[T](private val leading: List[T],
private val trailing: List[T]){
private def mirror = if(leading.isEmpty) new Queue(trailing.reverse, Nil)
else this
override def toString = (leading ::: trailing.reverse).toString
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)
}
object Queue{
def apply[T](arr: T*) = new Queue[T](arr.toList, Nil)
}
제네릭 클래스나 트레이트 는 다음과 같이 정의할 수 있다.:
class Queue[T]{
}
파라미터화된 타입 은 new Queue[Int] 또는 new Queue[String] 와 같이 타입을 대괄호 안에 넣어서 타입을 생성할 수 있다. 스칼라에서는 타입 파라미터 덕분에 파라미터화된 타입들 간에 서브타입 관계 (subtyping relationship) 가 존재할 수 있다. 예를들어 Queue[String]은 Queue[AnyRef]의 서브타입이 될 수 있다. 만약 그렇다면 Queue 클래스는 타입 파라미터 T에 대해 공변적이라고 할 수 있다. (서브타입이 될 수도 안될 수도 있음)
스칼라에서는 기본적으로 타입 파라미터에 대해 무공변성을 갖는다. 즉 위의 Queue[String]은 Queue[AnyRef]의 서브타입이 아니다. 타입 파라미터 T에 대해 공변성을 띄게하고 싶다면 T앞에 변성표기 + 를 넣어서 공변성을 띄게 할 수 있다. - 를 넣는다면 반공변성을 띄게 된다. 타입 파라미터에 반공변적 이라면 임의의 타입 파라미터 S가 T의 서브타입일때 Queue[T]는 Queue[S]의 서브타입이 된다.
공변/반공변을 띄게 하는 +/- 표기는 변성 표기 라고 한다.
공변 타입이 항상 가능하지는 않다. 다음과 같은 설계는 불가능하다.:
class Cell[+T](init: T){
private[this] var v = init
def get = v
def set(x: T) { v = x }
}
위와 같이 변수 재할당이 있는 경우 공변 타입이 가능하지 않다. (실제로 타입 파라미터를 함수의 파라미터로 사용하는 것이 허용되지 않는다.) 예를들어 다음과 같은 코드가 있다고 가정할 경우 문제가 발생한다.:
val original: Cell[String] = new Cell("Hello World")
val post: Cell[Any] = original
post.set(1234)
val value: String = original.get
위 코드에서 문제가 되는 부분은 2번째 줄에서 Cell[String] 값을 Cell[Any] 변수에 할당하는 부분이다. 따라서 마지막 4번째 줄에서 String이 아닌 1234를 얻게되므로 이는 프로그램 오류이다. 서브타이핑 관점에서 Cell[String]은 Cell[Any]보다 할 수 있는 것이 적으므로 (Int 타입 set의 파라미터로 넘기는 등) 서브타이핑 관계가 될 수 없다. 이 코드는 컴파일 되지 않는다. 클래스의 공변적 타입 파라미터는 함수의 파라미터로 사용할 수 없다. (실제로 타입 T인 v라는 변수는 setter/getter 메소드 def v: T, def v_=(x: T)로 취급되므로 var 또한 사용할 수 없다.)
다음 예제도 타입 건전성을 위반하므로 컴파일 될 수 없다.:
object TestCovariant {
class Fruit
class Apple extends Fruit
class Orange extends Fruit
class Space[+T] (private val element: List[T]){
def add(x: T) = new Space(x :: element)
def get:List[T] = element
}
class DoubleSpace(val t: List[Int]) extends Space[Int](t){
override def add(x: Int) = super.add(x * x)
}
def main(args: Array[String]) ={
val any: Space[Any] = new DoubleSpace(List[Int](1234))
any.add("Can't double this string")
}
}
위와 같이 main문에서 잘못된 서브타이핑으로 문자열을 제곱하는 오류가 발생할 수 도 있다. 이는 Space[Any]가 Space[Int]보다 할 수 있는 것이 많기 때문이다. 즉 리스코프 치환 원칙(Liskov Substitution Principle)에 의해 Space[Int]는 Space[Any]의 서브타입이 될 수 없다. Space[Int]가 Space[Any]의 모든 연산을 지원하고 인자를 더 적게 요구하고, 더 많은 기능을 제공할 때 Space[Int]는 Space[Any]의 서브타입이 될 수 있다. 따라서 Space[Int]를 Space[Any]로 서브타이핑 했을때 이런 오류가 발생하는 것이다.
좀더 제약을 준다면 공변적 타입 파라미터를 함수의 인자로 문제없이 사용할 수 있다. 먼저 파라미터화 된 메소드로 바꾼 뒤 타입 파라미터 U의 하위바운드로 class의 타입파라미터 T를 지정 하는 것이다.:
object TestCovariant {
class Fruit
class Apple extends Fruit
class Orange extends Fruit
class Space[+T] (private val element: List[T]){
def add[U >: T](x: U) = new Space(x :: element)
def get:List[T] = element
}
class DoubleSpace(val t: List[Int]) extends Space[Int](t){
override def add[U >: Int](x: U) = super.add(x.toString.length + x.toString.length) //오버라이드를 하더라도 이러한 시그니쳐를 갖는다.
}
def main(args: Array[String]) ={
val ap: Space[Apple] = new Space[Apple](List(new Apple()))
val or = ap.add(new Orange)
println(or.get.toString)
}
}
Space는 공변적 T를 갖고 있으므로 T의 임의의 슈퍼타입으로 파라미터화된 Space로 서브타이핑할 수 있다. 또한 add를 공통 슈퍼타입을 갖는 다른 타입을 인자로 계속해서 호출할 수 있으며 추가할 수 있다. 결과적으로 Apple만 받는 Space가 아닌 좀더 일반적인 Space 타입이 되었다. 또한 호출할때마다 타입 Space[U]를 반환한다. 변성표기와 하위 바운드를 지정함으로써 더 유연한 모델을 만들 수 있으며 이는 타입 위주 설계(type-driven design)에 유용하다.
오히려 반 공변적일때는 타입파라미터를 메서드 파라미터로 사용할 수 있다.
class Fruit{
override def toString:String = {return "Fruit"}
}
class Apple extends Fruit{
override def toString:String = {return "Apple"}
}
class Orange extends Fruit{
override def toString:String = {return "Orange"}
}
class Out[-T]{
def write(x: T){
println(x.toString)
}
}
val f = new Out[Fruit];
f.write(new Apple());
f.write(new Orange());
val a: Out[Apple] = f;
a.write(new Apple())
Out[Fruit]과 같이 Fruit로 파라미터화된 타입이 실제로 동작할때 Apple과 Orange를 다 받아들일 수 있다. 즉 Out[Fruit]은 Out[Apple]로 서브타이핑 될 수 있다. 반대로 타입 T에 대해 공변적이라면 Out[Apple]은 Out[Fruit]로 서브타이핑이 되어야 하지만 Out[Apple]은 Out[Fruit]보다 더 적은 타입을 받아들이므로 불가능하다. 이는 리스코프 치환 원칙 과 관련이 있다.
공변성과 반공변성이 섞여있는 예제는 아래와 같다.:
class Animal(val name: String)
class Human(name: String) extends Animal(name)
object Dict{
val humans: Set[Human] = Set(new Human("changwon"), new Human("be y"))
def printNames(extract: Human => AnyRef){
for (human <- humans) println(extract(human))
}
}
def getName(h: Animal): String = h.name
Dict.printNames(getName)
printNames 메서드의 파라미터는 Human => AnyRef 이지만 실제로 사용자는 Animal => String을 전달할 수 있다. 함수타입 A => B는 Function1[A,B]로 바뀌게 된다. Function1의 정의를 보면 첫번째 타입파라미터는 반공변성을 띄고 두번째 파라미터는 공변성을 띄게된다.
이러한 변성이 문제없이 동작하는 이유는 함수의 파라미터에서 Human대신 더 많은 파라미터를 받을 수 있는 Animal이 사용되는 것은 타당하며 함수의 반환 타입인 AnyRef의 기능을 String이 모두 포함하고 있기 문맥상 Animal => String 으로 서브타이핑이 되어도 문제가 없기 때문이다. Human은 Animal의 서브타입이지만 Human => AnyRef는 Animal => String의 서브타입이 아니다. 즉 상속관계가 반대가 된다.