[scala] Covariant vs Contravariant

Sangwoo Park·2023년 11월 4일
0
post-thumbnail
post-custom-banner

흔히 공변성, 반변성 이라고 번역되는 covariant, contravaiant 에 대해 알아보겠습니다.

사전적 의미

variant 의 사전적 뜻은 변종, 이형(異形) 입니다.

~와 같이 라는 뜻의 co 와 붙은 covariant 는 같이 변한다(공변)는 뜻입니다.

~와 반대로 라는 뜻의 contra 와 붙은 contravariant는 반대로 변한다(반변)는 뜻이겠죠.

Up Casting, Down Casting

스칼라 타입 다이어그램과 같이 보면

모든 타입의 SuperType(조상)인 Any가 있고, 모든 타입의 SubType(자손)인 Nothing 이 있습니다.

SuperType 으로 갈수록 -, SubType으로 갈수록 + 라고 표현하겠습니다.

객체를 캐스팅할 때 - 방향으로 캐스팅하면 Up Casting 이라고 하고, + 방향으로 캐스팅하면 Down Casting 이라고 하죠.

abstract class Food
case class Egg()       extends Food
case class BoiledEgg() extends Egg()

val food: Food = Egg() //upcasting
val boiledEgg: BoiledEgg = Egg() // downcasting error!

음식, 계란, 삶은계란을 예로 들면
계란을 음식에 할당(업캐스팅)할 수 있지만
삶은계란은 (날)계란에 할당(다운캐스팅)할 수 없는게 일반적입니다.

Covariant

스칼라에서 Covariant 는 [+T] 라는 제네릭으로 표현됩니다.

covariant 는 업캐스팅과 비슷해서 이해하기가 쉽습니다.
흔히 보는 List[+T]covariant 이기 때문에 직관적이죠.
객체와 제네릭이 같은 방향으로 변하면
즉, T를 업캐스팅해서 T1 이 되었을 때 List[T]List[T1] 이 된다면 covariant 한 것입니다.
코드로 예시를 들어보면 아래와 같습니다.

class Box[+T]

val upcast1: Box[Food] = new Box[Egg]
val upcast2: Box[Egg]  = new Box[BoiledEgg]
val upcast3: Box[Food] = new Box[BoiledEgg]
val downcast1: Box[Egg]       = new Box[Food] //error!
val downcast2: Box[BoiledEgg] = new Box[Egg] //error!
val downcast3: Box[BoiledEgg] = new Box[Food] //error!

음식을 담는 상자는 계란을 담을 수 있지만,
삶은계란 전용 상자는 모든 음식을 담을 수 없는 것과 마찬가지죠.

Contravariant

스칼라에서 Contravariant 는 [-T] 라는 제네릭으로 표현됩니다.

contravariant 는 다운캐스팅과 유사합니다.
T를 업캐스팅해서 T1 이 되었을 때 List[T1]List[T] 이 된다면 contravariant 한 것입니다.
하지만 이는 직관적으로 이해하기가 어렵습니다.
코드 예시를 보겠습니다.

val upcast1: Eater[Food]        = new Eater[Egg] //error!
val upcast2: Eater[Egg]         = new Eater[BoiledEgg] //error!
val upcast3: Eater[Food]        = new Eater[BoiledEgg] //error!
val downcast1: Eater[Egg]       = new Eater[Food]
val downcast2: Eater[BoiledEgg] = new Eater[Egg]
val downcast3: Eater[BoiledEgg] = new Eater[Food]

모든음식을 먹을 수 있는 사람은 삶은계란을 먹을(consume) 수 있지만,
삶은계란만 먹을 수 있는 사람이 모든 음식을 먹을 수 있는 건 아니죠.

이처럼 contravariant 는 consume 의 측면에서 생각하면 쉽습니다.

언제 사용해야 할까?

개념적으로 접근하자면 클래스가 특정 타입의 출력을 생성하고 그 타입의 인스턴스를 변형하거나 소비(consume) 한다면 Contravariant를 선택할 수 있다. 이는 주로 매개변수 타입에서 일반적이다.
그렇지 않고 특정 타입의 인스턴스를 소비하지 않는다면 Covariant 를 선택할 수 있다. 이는 주로 반환 타입에서 일반적이다.

profile
going up
post-custom-banner

0개의 댓글