본문 바로가기

Languages/Scala

[Scala Cats] 타입 공변성/반공변성 다루기

반응형

이미지 출처 : Pixabay

지난 글에서는 Cats 라이브러리의 타입 클래스에 대해 다루었습니다.

 

[Scala Cats] Cats의 타입 클래스

지난번 포스트에서 타입 클래스를 스칼라에서 어떻게 구현하는지를 알아보았습니다. [Scala Cats] 스칼라 타입 클래스 기초 귀여운 고양이로 시작합니다. 스칼라 Cats는 스칼라에서의 Functional한 프

sean-ma.tistory.com

타입을 다룰 때 가장 조심해야 할 부분중에 하나가 상/하위 타입을 같이 사용해야 하는 경우입니다.

이번 글에서는 타입 간의 계층 관계에 대해서 다루어 보겠습니다.

레퍼런스는 Scala with Cats [무료 PDF] 입니다.

 

 


타입 변성

스칼라에서 타입 간의 계층 관계를 처리할 때, 항상 따라오는 문제 중 하나가 타입의 변성(Type Variance)에 대한 내용입니다.

빠른 이해를 돕기 위해 자바를 사용해 먼저 예를 들겠습니다.

class Person {}
class Employee extends Person {}
class Developer extends Employee {}
class Designer extends Employee {}

public class varianceTest {
	public static void main(String[] args) {
		List<Employee> employeeList = new ArrayList<>();
        
		employeeList.add(new Developer()); //ok
		employeeList.add(new Designer()); //ok
	
		Person p = employeeList.get(0); //ok

		Designer d = employeeList.get(0); //error!
		employeeList.add(new Person()); //error!
	}
}

 

위와 같이 4개의 클래스를 정의해 주었으며, 각 클래스간의 상속 관계를 파악할 수 있습니다.

Employee의 리스트인 employeeList를 생성하고, 리스트 안에 각 객체의 인스턴스를 생성하여 삽입합니다.

직원 명부에 우리 회사 직원인 개발자와 디자이너는 넣을 수 있지만, 우리 회사 직원인지 여부도 모르는 일반 사람을 적어 넣을 수는 없습니다. 반대로, 직원 명부에 있는 사람은 개발자인지 디자이너인지 직원 이름만 적힌 명부를 보고는 확인이 불가능하겠죠?

즉, Employee를 상속받은 Developer, Designer는 employeeList에 삽입할 수 있으나, Employee의 상위 클래스인 Person은 employeeList에 삽입할 수 없습니다. employeeList에서 가져온 원소를 해당 클래스의 상위 클래스로 캐스팅하는것은 허용되나 하위 클래스로 캐스팅하는것은 허용되지 않습니다.

이처럼 값을 넣는 과정에서는 일반적으로 공변성(Covariant) 을 지원하며, 값을 가져오는 과정에서는 반공변성(Contravariant) 을 지원합니다. 변환을 허용하지 않는 것은 무공변성(Invariant) 이라 합니다.

 

타입 변성은 리스코프 치환 원칙(Liskov Substitution Principle)과 관련이 깊으니 궁금하신 분은 다음 링크를 읽어 보시길 바랍니다.

위키피디아만 읽고서 이해하기는 너무 힘드니 나중에 따로 정리해볼 계획 : https://ko.wikipedia.org/wiki/리스코프_치환_원칙

 

리스코프 치환 원칙 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 치환성(영어: substitutability)은 객체 지향 프로그래밍 원칙이다. 컴퓨터 프로그램에서 자료형 S {\displaystyle S} 가 자료형 T {\displaystyle T} 의 하위형이라면 필요한

ko.wikipedia.org

 

스칼라 타입 시스템에서는 공변성, 반공변성, 무공변성을 다음과 같이 표시합니다.

변성 타입 표기 설명
공변성 (Covariant) [+T] C[T']는 C[T]의 하위 클래스
반공변성 (Contravariant) [-T] C[T]는 C[T']의 하위 클래스
무공변성 (Invariant) [T] -

 

스칼라로 작성한 예시는 다음과 같습니다.

class Root
class Trunk extends Root
class Branch extends Trunk

class Covariance[+T]
class Contravariance[-T]

val cov1: Covariance[Trunk] = new Covariance[Branch]
val cov2: Covariance[Trunk] = new Covariance[Root] //error!

val contv1: Contravariance[Trunk] = new Contravariance[Root]
val contv1: Contravariance[Trunk] = new Contravariance[Branch] //error!

 

타입 변성(Variance)에 대해서 좀 더 자세히 알고 싶다면 다음 링크로 :

https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)

 

Covariance and contravariance (computer science) - Wikipedia

From Wikipedia, the free encyclopedia Jump to navigation Jump to search Many programming language type systems support subtyping. For instance, if the type Cat is a subtype of Animal, then an expression of type Cat should be substitutable wherever an expre

en.wikipedia.org

 

 


타입 클래스와 공변성 / 반공변성

타입 클래스를 정의할 때, 타입 파라미터에 공변성/반공변성에 대한 어노테이션 (variance annotation)을 제공함으로써 컴파일러가 암시적으로 인스턴스를 선택(select instances during implicit resolution) 하도록 할 수 있습니다.

다음과 같은 두 클래스를 정의하고, 두 클래스에 대한 타입 클래스를 생성해 보겠습니다.

class A
class B extends A

 

타입 클래스에서 공변성을 사용한다면 타입 클래스 F[B]가 타입 클래스 F[A]의 하위 타입이 된다는 것을 의미합니다.

해당 사용 예는 스칼라의 컬렉션에서 확인할 수 있습니다.

 

trait List[+A]
trait Option[+A]

 

스칼라 컬렉션에서의 공변성은 List[int]가 List[Any]의 하위 타입이 되는 것을 성립하게 해 줍니다.

하지만 반공변성을 사용한다면 어떨까요?

trait JsonWriter[-A] {
	def write(value: A): string
}

 

이제 두 개의 타입과 각각의 타입에 맞는 Writer를 정의하고, format 함수를 선언합니다.

class Shape
class Circle extends Shape

val shapeWriter: JsonWriter[Shape] = ???
val circleWriter: jsonWriter[Circle] = ???

def format[A](value: A, writer: JsonWriter[A]): Json = 
	writer.write(value)

 

이 format 함수에 넘겨줄 수 있는 value와 writer의 조합은 어떤 게 있을까요?

Circle 타입은 Shape이기 때문에 shapeWriter와 circleWriter 모두와 함께 사용할 수 있습니다.

하지만 Shape 타입은 Circle 타입이 아니기 때문에 shapeWriter만 사용할 수 있게 됩니다.

이 때 JsonWriter[Shape]이 JsonWriter[Circle]의 하위 타입이 됩니다.

 

즉, A가 B를 상속하는 형태일 때, F[-A]로 타입 클래스를 반공변적으로 정의한다면 F[B]가 F[A]의 하위 타입이 되는 것이 아니라 F[A]가 F[B]의 하위 타입이 됩니다.

 

컴파일러가 implicit 스코프를 탐색할 때 각 타입 클래스의 성격에 맞도록 매칭되는 타입 혹은 서브 타입을 찾게 됩니다.

따라서 우리는 변성 어노테이션을 사용함으로써 타입 클래스 인스턴스 선택 과정을 컨트롤할 수 있게 되는 것입니다.

타입 클래스 속성 무공변성 (Invariant) 공변성 (Covariant) 반공변성 (Contravariant)
상위 타입 인스턴스가 사용되는가? 아니요 아니요
더 상세한 타입의 사용이 선호되는가? 아니요 아니요

 

반응형