ETC

공변성과 반공변성, 무공변성

partner_jun 2018. 6. 5. 17:16

스칼라는 공변성(variance)와 반공변성(contravariance), 무공변성(invariance)을 지원한다.

이 세 가지 성질에 대해 위키백과 설명을 보면 이해하기 어렵지만, 정리해서 보면 쉽게 이해할 수 있다.



타입 T를 받았을 때,



 [ +T ] 공변성 : 타입 T를 확장한 타입에 대해서 허용

 [   T ] 무공변성 : 타입 T만 허용

 [  -T ] 반공변성: 타입 T의 상위(부모) 타입에 대해서 허용



이 포스트에는 스칼라를 이용한 공변성 예제를 살펴보고, ECMAScript를 이용해 간단하게 만들어본 공변성-타입 제한 배열을 적어놓는다.




스칼라를 이용한 공변성 예제

스칼라는 언어 레벨에서 세 개의 성질을 지원한다. 

/**
* 예시 클래스.
* People <- Student <- StudentPresident
*/
class People { }
class Student extends People { }
class StudentPresident extends Student { }

예시를 위한 클래스 선언



/**
* 공변성. 타입 T를 확장한 타입에 대해 허용한다.
*/
class Covariant[+T] {}

val people1 = new Covariant[People]
val student1 = new Covariant[Student]
val studentPresident1 = new Covariant[StudentPresident]

// 메소드 테스트
def say(user: Covariant[Student]) = println(user)

// say(people1) // Compile Error!
say(student1)
say(studentPresident1)

// 형 변환 테스트
val coPeople: Covariant[People] = new Covariant[StudentPresident]

// val coStudentPresident: Covariant[StudentPresident] = new Covariant[People] // Compile Error!

입력받은 타입 T를 확장한 타입을 허용하는 공변성



/**
* 반공변성. 타입 T의 상위(부모) 타입에 대해 허용한다.
*/
class Contravariant[-T] {}

val people2 = new Contravariant[People]
val student2 = new Contravariant[Student]
val studentPresident2 = new Contravariant[StudentPresident]

// 메소드 테스트
def say(user: Contravariant[Student]) = println(user)

say(people2)
say(student2)
// say(studentPresident2) // Compile Error!

// 형 변환 테스트
// val contraPeople: ContraVariant[People] = new ContraVariant[StudentPresident] // Compile Error!
val cotraStudentPresident: Contravariant[StudentPresident] = new Contravariant[People]

입력받은 타입 T의 상위 타입에 대해 허용하는 반공변성



/**
* 무공변성. 같은 타입만 허용한다.
*/
class Invariant[T] {}

val people3 = new Invariant[People]
val student3 = new Invariant[Student]
val studentPresident3 = new Invariant[StudentPresident]

// 메소드 테스트
def say(user: Invariant[Student]) = println(user)

// say(people3) // Compile Error!
say(student3)
// say(studentPresident3) // Compile Error!

// 형 변환 테스트
// val inPeople: Invariant[People] = new Invariant[StudentPresident] // Compile Error!
// val inStudentPresident: Invariant[StudentPresident] = new Invariant[People] // Compile Error!

입력받은 타입 T만 허용하는 무공변성 테스트





자바에서의 반공변성

자바에서는 공변성만을 지원한다. 제네릭과 함께 추가된 몇 가지 키워드가 있지만, 기존부터 LSP를 따르고 있었기에 별 차이가 없다.

// People <- Student <- StudentPresident
People people = new People();
Student student = new Student();
StudentPresident studentPresident = new StudentPresident();

// ?는 Student보다 큰(Student가 확장된) 클래스
List<? super Student> covarianceList = new ArrayList<>();
// covarianceList.add(people); // Compile Error
covarianceList.add(student);
covarianceList.add(studentPresident);

List<Student> covarianceList2 = new ArrayList<>();
// covarianceList2.add(people); // Compile Error
covarianceList2.add(student);
covarianceList2.add(studentPresident);

// true.
System.out.println(covarianceList.equals(covarianceList2));

공변성을 이용한 리스트.

뭔가 특별한걸 기대하지 말자.







ECMAScript로 만들어본 공변성 지원 배열

타입을 제한하고 공변성/반공변성/무공변성을 지원하는 배열을 간단하게 만들어 보았다.

기존 배열을 확장(extend)하지 않고 구성하도록 구현했고, 배열 메소드 중 값을 삽입하는 'push'와 'unshift' 메소드는 타입 체크 함수를 호출한 후 호출한다.

class TypedArray {
/**
* 타입을 제한하는 배열인 TypedArray 생성자.
* @param variance string 공변/반공변성. + 혹은 -, 공백으로 입력한다.
* @param type class 제한하고자 하는 타입.
*/
constructor(variance, type) {
const array = [];

/**
* 공변/반공변 값을 넣지 않은 경우
*/
if (!type) {
this.type = variance;
} else {
this.variance = variance;
this.type = type;
}

/**
* 값이 타입에 대해 반공변성을 가졌는지 체크하는 함수.
* @param value 추가하려는 값
* @param type 확인할 타입
* @returns {*}
*/
const superTypeCheck = (value, type) => {
if (!type) return false;
else if (value.constructor.name === type.name) return true;
else return superTypeCheck(value, type.__proto__);
};

/**
* 값이 선언된 TypedArray 객체에서 허용하는 타입인지 검사하는 함수.
* @param value 추가하려는 값
* @returns {*} 추가할 수 있는지 여부
*/
const typeCheck = (value) => {
const typeOfValue = typeof(value);
const thisType = this.type.name;

switch (thisType) {
// primitive type
case 'number' :
case 'symbol' :
case 'string' :
return typeOfValue === thisType.toLowerCase();

default :
if (this.variance === '+' && value instanceof this.type) return true;
else if (this.variance === '-') return superTypeCheck(value, this.type);
else if (value.constructor.name === thisType) return true;
else return false;
}
};

/**
* 배열의 프로토타입을 복사한다.
* 배열에 값을 추가하는 push, unshift 함수는 타입을 체크한다.
*/
for (let field of Object.getOwnPropertyNames(Array.prototype)) {
this[field] = (value) => {
if (field === 'push' || field === 'unshift') {
if (!typeCheck(value)) throw 'IllegalTypeException';
}
return array[field].call(array, value);
};
}

/**
* 리스트를 반환하는 메소드
* @returns {Array} 복사된 원본 리스트
*/
this['get'] = () => array.slice();

}
}

배열 클래스 선언




/**
* 예시 클래스
* People <- Student <- StudentPresident
*/
class People {
constructor(name) {
this.name = name;
}
}
class Student extends People { }
class StudentPresident extends Student { }

// 객체 생성
const people = new People('Jack');
const student = new Student('john');
const studentPresident = new StudentPresident('quill');

예시 클래스 및 배열 객체 생성


// 공변성 배열 테스트
const varianceArr = new TypedArray('+', Student);
// varianceArr.push(people); // Exception!
varianceArr.push(student);
varianceArr.push(studentPresident);
// 무공변성 배열 테스트
const invarianceArr = new TypedArray(Student);
invarianceArr.push(people); // Exception!
invarianceArr.push(student);
invarianceArr.push(studentPresident); // Exception!
// 반공변성 배열 테스트
const contravarianceArr = new TypedArray('-', Student);
contravarianceArr.push(people);
contravarianceArr.push(student);
// contravarianceArr.push(studentPresident); // Exception!

테스트.