Search
📖

[Book Review] 타입으로 견고하게 다형성으로 유연하게

Created
2023/12/22 08:29
Tags
Post
Review
Book
Last edited time
2024/08/22 08:06
Status
Done

[감상평]

타입검사는 "내가 작성한 프로그램을 실행해도 문제가 없는지 컴퓨터가 자동으로 검사해 주는 기능" 이라고 저자는 말한다. 타입검사가 제공하는 이점이 상당히 많기에 최근에는 타입검사를 제지원하지 않던 언어들에도 타입검사가 추가되고 있는 추세이다. 예를들어, JS에 타입검사를 추가해 TS가 만들어지거나, 파이썬에도 타입검사 관련 기능들이 배포되고 있는 상황이다.
객체지향, 함수형, DDD, TDD 등 특정 패러다임 등을 설명해준 유명한 책은 많지만, 이 책처럼, 오롯이 "타입"의 관점에 대해 설명한 책은 흔치 않다. 이 책은 타입과 다형성의 개념을 바탕으로 타입 검사의 장단점을 소개하며, 타입 검사를 어떻게하면 잘 활용할 수 있는지를 상세하게 설명하고 있다. 또한 각 챕터별로 소개되는 개념에 대하여 각 언어별로 어떻게 구현하였는지 예시도 상세히 작성되어있어서 독자들의 이해를 친절히 도와주고 있다.
정적 언어를 이전에도 많이 써왔던 개발자라면 당연한 얘기들로 가득해 다소 인사이트가 부족하다고 느낄지도 모르겠다. 다만 다형성의 종류에 따라 타입 검사가 어떻게 동작하는지를 한번더 숙지하며 개념을 한번 더 내재화하는것으로도 의미가 있을것 같다. 그외에도 자바스크립트나 파이썬과 같은 동적 타입 언어를 많이 사용하거나, 또는 타입 관련 기능을 어떻게 잘 사용해야할지 고민인 개발자들에게 추천할 수 있을 것 같다.

[내용 정리]

1. 타입 검사 훑어보기

타입
프로그램에 존재하는 값들을 그 능력에 따라 분류 한 것
버그
가장 흔한 원인 - 타입 오류
타입 검사기
타입 오류 자동 판단 프로그램
버그를 자동으로 찾아줌
타입 검사의 원리
작은 부품 → 큰 부품 으로 프로그램 검사
타입 추론
타입 표시 없이 타입 검사기가 타입 검사 할수 있게 하는 기능
타입 표시가 있을 때도 검사를 통과할 프로그램에 한해 타입 표시를 생략
타입 검사를 무력화 하는 기능들
예시) 코틀린의 !! 연산자
널을 String 타입으로 값 강제 취급 → 런타임에서 널 접근 오류 발생 가능
다형성
타입 검사기의 오판을 줄이는 안전한 기능
타입 안정성을 해치지 않음
타입 검사기의 오판을 획기적으로 줄일 수 있음
개발자가 쉽게 이해할 수 있는 오류 메세지 제공
다형성의 종류
서브타입에 의한 다형성
매개변수에 의한 다형성
오버로딩에 의한 다형성

2. 서브타입에 의한 다형성 (Subtype Polymorphism)

2.1. 객체와 서브타입

정의
A는 B다 가 사실이면 A는 B의 서브타입 이다.
타입 시스템
B 타입 (슈퍼타입) 의 자리에 A 타입(서브타입)이 오더라도 타입검사기에는 문제가 없음
사람 > 학생
// Person은 Student의 슈퍼타입 class Person { id: number; name: string; email: string; } // Student는 Person의 서브타입 class Student extends Person { grade: number; gpa: number; paid: boolean; }
TypeScript
복사
예시
sendEmail 함수에 Person 대신 Student 타입이 들어가도 정상 동작함
사람 > 학생
function sendEmail(person: Person, title: string, content: string) { const email = person.email; // TO BE IMPLEMENTED } const student: Student = { id: 1, name: 'harry', email: 'sample@gmail.com', grade: 5, gpa: 4, paid: true, }; sendEmail(student, 'title', 'content');
TypeScript
복사
타입 검사기가 객체 타입의 서브타입 관계를 판단할때 사용하는 규칙
이름에 의한 서브타입
구조에 의한 서브타입

2.1.1. 이름에 의한 서브타입 (nominal subtyping)

정의
타입검사기가 클래스 이름과 클래스 사이의 상속 관계만 고려
클래스 A가 클래스 B를 상속한다면 A는 B의 서브타입이다.
상속
직접 상속
간접 상속
간접 상속 예시)
StudentSchoolObject 의 서브타입이면서 Person의 서브타입임
class SchoolObject {} class Person extends SchoolObject {} class Student extends Person {}
TypeScript
복사

2.1.2. 구조에 의한 서브타입 (structural subtyping)

정의
클래스 A가 클래스 B에 정의된 필드와 메서드를 모두 정의한다면 A는 B의 서브타입이다.
특징
타입검사기가 클래스 사이의 상속관계 대신 클래스의 구조(필드와 메서드)를 고려
이름에 의한 서브타입을 사용하지 못하는 경우 활용
예) 서로 다른 라이브러리의 클래스를 사용하는 경우
구현
구조를 드러내는 타입 사용
객체가 가지는 필드와 메서드를 나열
별도의 클래스를 정의하지않고 항상 잘 작동하게 사용 가능
예시)
class PersonByStructureType { email: string; } class StudentByStructureType { email: string; grade: number; } const studentByStructureType: StudentByStructureType = { email: 'sample@gmail.com', grade: 4, }; function sendEmailByStructureType(person: { email: string }) { const email = person.email; // TO BE IMPLEMENTED } sendEmailByStructureType(studentByStructureType);
TypeScript
복사

2.2. 집합론적 타입

2.2.1. 최대 타입 (top type)

가장 큰 타입
모든 타입은 최대 타입의 서브타입
예시)
타입스크립트: unknown
C++: any

2.2.2. 최소 타입 (bottom type)

예외를 다루는 데 유용한 타입
error / assertNonezero
계산이 끝나기전에 예외 발생시켜 즉시 실행이 끝남
계산이 끝날 기회를 얻지 못하는 것
최소 타입
계산을 끝마치지 못한다는 표현 할 수 있는 타입
어떤 값도 속하지 않는 타입
모든 타입의 서브타입
절대로 발생시키지 않는 값을 나타낼때 사용
예시)
타입스크립트: never
코틀린: Nothing
function error(): never { throw new Error(); } function assertNoneZero(num: number): number { return (num != 0) ? num : error(); } function assertShort(str: string): string { return (str.length <= 10) ? str : error(); }
TypeScript
복사
cf) 타입스크립트에서의 최대타입과 최소타입
최대타입(unknown) > 모든 타입(string, number, boolean) > 최소타입(never)

2.2.3. 이거나 타입 (union type)

A이거나 B (A or B)
아무 타입 A와 B가 있을때 언제나 A와 B 모두 A | B의 서브타입
위치에 민감한 타입검사
위치에 민감한 타입 검사가 잘 작동하도록 구조가 단순하게 만들어야 함
functin write(data: string | number): void { if (typeof data === 'string'){ let str: string = data; } else { let num: number = data; } } write("a"); write(1);
TypeScript
복사

2.2.4. 이면서 타입 (intersection type)

다중상속에 유용
교집합 타입
아무 타입 A와 B가 있고, A의 서브타입이면서 B의 서브타입인 C가 있는 경우, C는 A이면서 B이다.
interface Student { grade: number; } interface Teacher { course: string; } class TA implements Student, Teacher { grade: number; course: string; } class Volunteer implements Student, Teacher { grade: number; course: string; } function getGrade(st: Student & Teacher): number { let grade: number = st.grade; let course: string = st.course; } getGrade(new TA()); getGrade(new Volunteer());
TypeScript
복사

2.3. 함수와 서브타입

일급 함수
값으로 사용되는 함수
정의
함수 타입은 매개 변수 타입의 서브 타입 관계를 뒤집음
AB의 서브타입일때, B → CA → C의 서브타입이다.
함수 타입은 결과 타입의 서브타입 관계를 유지함
AB의 서브타입일때, C ⇒ AC ⇒ B의 서브타입이다.
예시)
Person ⇒ BooleanStudent ⇒ Boolean의 서브타입
사람을 인자로 받을 수 있는 함수는, 학생을 인자로 받을 수 있는 함수이다.
function startMentoring(select: (s: Student) => Person): void { const st: Student = { email: 'sample@gmail.com', grade: 1 }; const mentor: Person = select(st); // TO BE IMPLEMENTED } function selectStudentMentor(st: Student): Student { return st; } startMentoring(selectStudentMentor); function sendEmails(needEmail: (s: Student) => boolean): void { const st: Student = { email: 'sample@gmail.com', grade: 1 }; // TO BE IMPLEMENTED } function isHacked(pr: Person): boolean { return true; } sendEmails(isHacked);
TypeScript
복사

2.4. 타입 검사기의 서브타입 관계 판단 규칙

객체 타입
이름에 의한 서브타입
클래스 A가 B를 상속하는 경우 A는 B의 서브타입이다.
구조에 의한 서브타입
A가 B에 있는 필드와 메서드를 모두 가지고 있는 경우 A는 B의 서브타입이다.
집합론적 타입
최대 타입
모든 타입은 최대 타입의 서브타입이다. (Unknown)
최소 타입
최소타입은 모든 타입의 서브타입이다. (never)
이거나 타입
A와 B는 A | B의 서브타입이다.
이면서 타입
C가 A의 서브타입이면서 B의 서브타입이면 C는 A&B의 서브타입이다.
함수 타입
AB의 서브타입일때, B → CA → C의 서브타입이다.
AB의 서브타입일때, C ⇒ AC ⇒ B의 서브타입이다.

3. 매개변수에 의한 다형성 (parametric polymorphism)

3.1. 제네릭 함수

매개변수에 의한 다형성 = 제네릭
타입 검사기가 제네릭 함수 정의를 검사하는 방식
인자로 받은 값이 함수 안에서 특정 능력이 필요한 경우 검사 통과 하지 못함
예시)
타입 인자 추론
제너릭은 매번 타입 인자를 써줘야하는 불편함이 있음
타입 추론의 일종으로 제너릭 함수나 제너릭 메서드를 호출할때 타입 인자를 생략할 수 있도록 하는 기능
코드가 복잡해지면 타임검사기가 오동작할 수도 있음
힌들러-밀너 타입 추론
제너릭 함수를 정의할때 타입 추론
타입 매개변수를 쓰지 않아도 자동으로 제너릭 함수가 될 수 있음
choose v1 v2 = if ... then v1 else v2 str :: String str = choose "Korean" "Foreginer" num :: Int num = choose 1 2
Haskell
복사

3.2. 제너릭 타입

타입에 타입 매개변수를 추가한 경우
let : Array<number> = [1,2]; let m: Map<number, string> = new Map([[1, "one"], [2, "two"]]);
TypeScript
복사
제너릭 클래스도 정의 가능

3.3. 무엇이든 타입 (universally quantified type)

무엇이든 타입 = 보편양화타입
제너릭 함수의 타입
제너릭 함수를 값으로 사용하면 그 타입은 무엇이든 타입이 됨
Void simulate(forall T. (List<T> => T) rand) { Int number = rand<Int>(List<Int>(30, 35, 40, 45)); ... String species = rand<String>(List<String>("Gazelle", "Lion", "Zebra"')) ; } simulate(randUniform); simulate(randGeometric);
Java
복사
무엇이든 타입이 없는 언어라도 제너릭 메서드를 통해 유사하게 코드 작성 가능
class RandUniform { T rand<T>(List<T> Ist) { ... } } class RandGeometric { T rand<T>(List<T> Ist) { ... } } Void simulate({ T rand<T>(List<T> lst); } r) { ... Int number = r.rand<Int>(List<Int>(30, 35, 40, 45)); String species = r.rand<String>(List<String>("Gazelle" "Lion", "Zebra"')); ... }
TypeScript
복사

3.4. 무엇인가 타입 (existentially quantified type)

존재양화타입
예시)
Timestamper 클래스의 타입이 Int 라는것이 라이브러리 사용하는 쪽에 공개되어있을 경우, 라이브러리를 향후 수정할때 제약이 발생할 수 있음 (타입을 Int → Str로 변경)
타입스탬프의 타입을 라이브러리 사용자에게 숨겨야함 → 무엇인가 타입
class Timestamper { Int init() { return 0; } Int next(Int t) { return t + 1; } Bool cmp(Int t1, Int t2) { return t1 < t2; } } exists T.{ T init(); T next (T t); Bool cmp(T t1, T t2); } create() { return Timestamper); }
Java
복사

4. 두 다형성의 만남

4.1. 제너릭 클래스와 상속

전통적인 다형성
서브타입에 의한 다형성 = 객체 지향 언어
매개변수에 의한 다형성 = 함수형 언어
최근 두 다형성을 모두 제공하는 언어가 흔해짐
제너릭 클래스가 있을 때 타입들 사이의 서브타입 관계
A가 B를 상속하면 A가 B의 서브타입임
이는 제너릭 클래스에도 동일하게 적용됨
ArrayList<T>와 LinkedList<T> 는 List<T>의 서브타입
abstract class List<T> { T get(Int idx); ... } class ArrayList<T> extends List<T> { T get(Int idx) { ... } ... } class LinkedList<T> extends List<> { T get(Int idx) { ... } ... }
Java
복사
타입스크립트 예시
abstract class List<T> { abstract get(idx: number): T; } class ArrayList<T> { get(idx: number): T {...} } class BitVector { get (idx: number): boolean {...} } let l1: List<string> = new ArrayList<string>(); let l2: List<boolean> = new ArrayList<boolean>(); let l3: List<boolean> = new BitVector();
Java
복사

4.2. 타입 매개변수 제한

제너릭 함수의 조건
여러 타입으로 사용할 수 있는 함수
특별한 능력이 요구되는 곳에만 사용되어야 함
특별한 능력이 요구된다면 그 함수는 여러 타입으로 사용될 수 있는 함수가 아님. (제너릭 함수 X)
위 논리의 가정: 타입에는 두가지 능력이 있음
모든 타입이 가지고 있는 특별한 능력 = 제너릭 함수
하나의 타입만 가지고 있는 특별한 능력 = 특정 타입만 받는 함수
그러나, 모든 타입이 가지고 있지는 않지만, 그렇다고 한 타입만 가지고 있지도 않은 몇몇 타입이 가진 능력도 존재
예시) elder 함수
class Person { Int age; ... } class Student extends Person { ... } Person elder(Person p, Person q) { return (p.age > q.age) ? p : q; }
Java
복사
위 코드를 바탕으로 p1p2Person 타입의 변수인 경우 잘 동작함
그러나 p1p2Student 타입의 변수인 경우 타입 검사를 통과하지 못함
elder의 결과타입은 Person인데, PersonStudent의 서브타입이 아님.
Person p = elder(p1, p2); Student s = elder(s1, s2);
Java
복사
따라서, 타입 매개변수에 제한이 필요
제너릭 함수를 정의하되 제너릭 함수에 모든 타입이 아니라 특정 타입의 서브타입만 가능하도록 타입 검사기에게 알려줘야함
타입 매개변수 제한이 적용된 예시)
class Person { age: number; } class Student extends Person { grade: number; } function elder<T extends Person>(p: T, q: T): T{ return (p.age => q.age) ? p : q; } let p: Person = elder<Person> (new Person (), new Person()); let s: Student = elder<Student>(new Student (), new Student ()); class Group<T extends Person> { p: T; sortByAge (): void { let age: number = this.p.age; ... } }
TypeScript
복사

4.3. 가변성

가변성
제너릭 타입 사이에 서브타입 관계를 추가로 정의하는 기능
하나의 제너릭 타입에서 타입 인자만 다르게하여 얻은 타입들 사이의 서브타입관계를 만듬
제너릭 타입과 타입 인자 사이의 관계
cf) 상속
서로 다른 제너릭 타입 사이의 서브타입 관계를 만듬
예) List 와 ArrayList
가변성의 종류
공변
불변
반변(반공변)

4.3.1. 공변(covariance)

제너릭 타입이 타입 인자의 서브타입관계를 보전하는 것
BA의 서브타입일 때, List1<B>List1<A>의 서브타입이다.
타입인자가 A 에서 서브타입 B로 변할때, List1<A> 역시 서브타입 List1<B>로 변함

4.3.2. 불변(invariance)

제너릭 타입이 타입 인자의 서브타입 관계를 무시하는 것
BA의 서브타입이더라도 List2<B>List2<A>와는 아무 관계가 없음
타입 인자가 A 에서 서브타입인 B 로 변할때 List2<A>는 그냥 다른 타입인 List2<B>가 됨. 이때 List2<B>List2<A>의 서브타입이 아님
타입 인자가 서브타입으로 변하더라도, 제너릭 타입은 서브타입으로 변하지 않음

4.3.3. 반변/반공변(contravariance)

함수 타입의 매개변수 타입
매기변수 타입의 서브타입 관계를 뒤집고, 결과 타입의 서브타입 관계를 유지함
함수 타입과 결과 타입사이는 공변
함수 타입과 매개변수 타입의 관계는 반변/반공변
결과 타입을 C로 고정할 때 BA의 서브타입이면 B ⇒ CA ⇒ C의 슈퍼타입이다
타입 인자가 A에서 서브타입인 B로 변할 때 A ⇒ C 는 타입인자와는 반대 방향으로 움직여 B ⇒ C로 변한다고 볼 수있음
제너릭 타입이 타입 인자와 반대로 변하므로 반변/반공변이라고 부름
각 제너릭 타입의 가변성을 결정하는 방법
제너릭 타입 G , 타입 매개변수 T
GT 를 출력에만 사용하면 공변, 입력에만 사용하면 반변, 출력과 입력 모두에 사용하면 불변
G에 해당하는 타입
T를 출력에 사용
T를 입력에 사용
가변성
List1<T>
O
X
공변
List2<T>
O
O
불변
Int ⇒ T
O
X
공변
T ⇒ Int
X
O
반변

4.3.4. 타입 검사기의 서브타입 판단 방법

1) 정의할 때 가변성을 지정하기 (declaration-site variance)
제너릭 타입을 정의할때 가변성 지정
예시1) 타입스크립트
구조에 의한 서브타입을 사용하므로 가변성을 명시적으로 지정할 필요가 없음
타입 검사기가 매번 타입에 정의된 필드와 메서드를 고려하여 서브타입 결정하므로 자연스럽게 가변성을 알아냄
class Person {} class Student { grade: number; } class ReadOnlyList<T> { get: (idx: number) => T = ...; } class Map<T, S> { get: (t: T) => S = ... ; add: (t: T, s: S) => void = ...; } let l: ReadOnlyList<Person> = new ReadOnlyList<Student>(); let m: Map<Student, number> = new Map<Person, number> ();
TypeScript
복사
예시 2) C#
모든 제너릭 클래스는 불변
제너릭 인터페이스만 공변/반변으로 만들 수 있음
따라서 ReadOnlyListMap을 인터페이스로 정의
class Person {} class Student : Person {} interface ReadOnlyList<out T> { T get(int idx); } interface Map<in T, S> { S get(T t); void add(T t, S s); } ReadOnlyList<Person> foo(ReadOnlyList<Student> l) { return l; } Map<Student, int> bar(Map<Person, int> m) { return m; }
C#
복사
2) 사용할 때 가변성 지정하기 (use-site variance)
공변: out 타입 사용
반변: in 타입 사용
예시) 코틀린
open class Person(val age: Int) class Student(age: Int) : Person(age) class List<T> { fun length(): Int = ... fun get(idx: Int): T = ... fun add(t: T): Unit = ... } fun averageAge(people: List<out Person>): Int { val len: Int = people.length() val age: Int = people.get(...).age ... } fun addStudent(students: List<in Student>): Unit { if (students.length() < ...) { students.add(Student(...)) } } averageAge(List<Student>()) addStudent(List<Person>())
Kotlin
복사

5. 오버로딩에 의한 다형성 (ad hoc polymorphism)

5.1. 오버로딩

함수 오버로딩
함수가 여러 탕비의 인자를 받아야할때, 타입보다 훨씬 간단하고 직관적인 해결책 제공
같은 이름의 함수를 여러개 정의(단, 매개변수 타입은 달라야함)
오버로딩 종류
함수 오버로딩
메서드 오버로딩
연산자 오버로딩

5.1.1. 함수 오버로딩 / 함수 선택

함수 선택(function dispatch)
함수가 오버로딩 되어 있을 때 호출할 함수를 자동으로 고르는 것
함수 선택 규칙
1.
인자의 타입에 맞는 함수를 고름
2.
(인자의 타입에 맞는 함수가 여럿이면) 인자의 타입에 가장 특화된(most specific) 함수를 고름
3.
함수를 고를 때는 인자의 정적타입만 고려
인자의 타입에 가장 특화된(most specific) 함수를 고름
한 함수가 더 다른 하나보다 더 특화되었다는 말
= 한 함수의 매개변수 타입이 다른 함수의 매개 변수 타입의 서브타입
타입 검사기가 서브타입의 관계를 바탕으로 어느 함수가 더 특화되었는지 판단
예시)
class Vector { List<Int> entries; } class SparseVector extends Vector { List<Int> nonzeros; } Int length(Vector v) { v.entries[i] ... } Int length(SparseVector v) { v.entries[v.nonzeros[i]] ... } SparseVector v = SparseVector(...); // 첫번째 함수보다 더 특화된 두번째 함수가 호출 됨. length(v);
Java
복사
타입 종류
정적 타입
타입 검사기가 알고 있는 타입
실행하기 전에 타입 검사를 통해 알아낸 타입 (컴파일 환경)
동적 타입
프로그램을 살행할때 그 부품을 계산하면 실제로 나오는 가장 정확한 타입
실행하는 중에 진짜 값을 보고 알아낸 타입 (런타임 환경)
// 정적타입: Vector // 동적타입: SparseVector Vector v = SparseVector(...);
Java
복사
정적 선택 (static dispatch)
정적 타입을 바탕으로 함수를 선택하는 것
타입 검사를 통해 인자의 정적 타입을 알아낸 뒤 실행하기 전에 호출할 함수를 미리 선택하는 것
정적 선택의 결과에 따라 내 의도와 다르게 동작하는 일이 생길 수 있음
예시)
정적타입: Vector
동적타입: SparseVector
실제로 들어온 타입인 SparseVector 에 따라 희소 벡터만 처리하는 두번째 length 함수가 호출되는게 바람직하나 실제로는 정적 선택에 따라 첫번째 length 함수가 호출됨
Vector v = SparseVector(...); length(v);
Java
복사
결론
함수 오버 로딩 사용시, 동적 타입을 잘 이해해야함
함수 오버 로딩은 완전히 서로 다른 타입들의 값을 인자로 받는 함수를 정의하는 용도로 사용 권장

5.1.2. 메서드 오버로딩 / 메서드 선택

한 클래스에 이름이 같은 메서드를 여럿 정의하는 것
오버로딩 대상이 함수에서 메서드로 바뀐 것
메서드 선택(method dispatch)를 통해 실제로 호출될 메소드가 정의됨
class Cell { ... Void write(String str){ ... } Void write(Int num){ ... } } Cell c1 = ...; Cell c2 = ...; c1.write("Hello"); c2.write(42);
Java
복사

5.2. 메서드 오버라이딩

특화된 동작을 정의하는 가장 좋은 방법
클래스를 상속해 자식 클래스에 메서드를 새로 정의할 때 메서드의 이름과 매개변수 타입을 부모 클래스에 정의된 메서드와 똑같이 정의하는 것
정적타입과 동적 타입이 다른 경우?
동적타입 기반으로 동작함. 함수 오버로딩의 이슈에 대응 가능
서브타입을 위해 더 특화된 동작을 정의하고 정적타입과 상관없이 특화된 동작이 사용되도록 만듦
예시)
동적타입인 SparseVectorlength 메서드가 호출 됨
Vector v = SparseVector(...); v.length();
Java
복사

5.2.1. 메서드 선택(method dispatch)

메서드 오버라이딩을 사용한 경우에도 메서드 선택 발생
같은 이름의 메서드가 정의된 부모 클래스와 자식 클래스 중 그중 무엇을 호출할지 선택
메서드 선택은 메서드 오버로딩 뿐만 아니라 메서드 오버라이딩까지 고려해서 메서드 선택
cf) 함수 선택과 메서드 선택 비교
함수 선택
인자의 정적 타입만 고려
메서드 선택
인자의 정적 타입 고려
수신자(receiver: 메서드 호출 시 메서드 이름 앞에 오는 객체)의 동적 타입 고려
동적 선택(dynamic dispatch)
수신자의 동적 타입을 고려하여 실행중에 메서드를 고름
메서드 선택 규칙
1.
인자의 타입에 맞는 메서드를 고른다.
2.
(인자의 타입에 맞는 메서드가 여럿이면) 인자의 타입에 가장 특화된 메서드를 고른다.
3.
메서드를 고를때는 인자의 정적 타입을 고려한다.
4.
메서드를 고를때는 수신자의 동적 타입도 고려한다.

5.2.2. 메서드 선택의 한계

수신자의 동적 타입만 고려하고 인자의 동적타입을 고려하지 않음 → 개발자의 기대와 다른 메서드가 호출 될 수 있음
언어 수준에서 도와주지 않으므로 효율적인 메서드가 호출 되도록 개발자가 코드를 잘 작성해야함
예시)
class Vector { ... Vector add(Vector that) { ... } Vector add(SparseVector that) { ... } } class SparseVector extends Vector { ... Vector add(Vector that) { ... } Vector add(SparseVector that) { ... } } // 메서드 선택시, 수신자의 동적 타입에 따라 SpraseVector에 정의된 첫번째 메서드 호출 -> 정상 Vector v1 = SparseVector(...); Vector v2 = Vector(...); v1.add(v2); // 인자의 동적타입을 고려하지 않음. Vector의 첫번째 메서드가 호출되어버림 // 덜 효올적인 메서드 -> 비정상 Vector v1 = Vector(...); Vector v2 = SparseVector(...); v1.add(v2);
Java
복사

5.2.3. 메서드 오버라이딩과 결과 타입

메서드 오버라이딩 할 경우, 결과 타입은 부모 클래스의 결과 타입의 서브타입이어야함
동적 선택을 사용하면서 타입 안정성을 지키기 위함임
예시 1)
List 가 공변인 경우 정상
List 가 불변인 경우, List<Student>List<Person> 의 서브타입이 아니므로 타임 검사 통과 X
class Person { ... List<Person> colleagues() {...} } class Student extends Person { ... List<Student> colleagues() {...} }
Java
복사
예시 2)
런타임 환경에서는 v1sparseVector 객체임
따라서 실제로는 SpraseVector 클래스의 add가 호출되며 Vector 객체가 반환됨
Vector 객체는 nonzeros 필드가 없으므로 오류 발생
class Vector { ... SparseVector add(SparseVector that) { ... } } class SparseVector extends Vector { ... Vector add(Vector that) { ... } } Vector v1 = SparseVector(...); SpraseVector v2 = SparseVector(...); v1.add(v2).nonzeros ....
Java
복사
동적 선택으로 인해 아래 메서드가 다를 수 있음
컴파일 환경에서 타임검사기가 참고하는 메서드 =! 런타임 환경에서 실제 호출되는 메서드
타임 검사기는 정적타입밖에 모르므로 수신자의 정적타입을 바탕으로 참고할 메서드 선택
런타임 환경에서는 수신자의 동적 타입이 호출되는 메서드를 결정
따라서, 타입검사기가 참고한 메서드가 다른 메서드가 호출되더라도 참고한 메서드의 결과 타입이 지켜져야함
자식 클래스에 있는 메서드의 결과 타입이 부모 클래스에 있는 메서드의 결과 타입의 서브타입 이어야함
class Vector { add(v: SparseVector): Vector {...} } class SparseVector extends Vector { add(v: SparseVector): SparseVector {...} }
Java
복사

5.3. 타입 클래스

추상 클래스
특정 타입이 어떤 메서드를 가진다는 사실 표현
타입 클래스
특정 타입을 위한 어떤 함수를 가진다는 사실 표현
클래스와는 무관
예)
어떤 타입 TComparable 타입 클래스에 속하려면 매개 변수 타입이 (T, T)이고 결과 타입이 Boolean 인 함수가 있어야한다
typeclass Comparable<T> { Boolean gt(T v1, T v2); }
Java
복사
특정 타입을 어떤 타입클래스에 속하게 하려면 타입클래스 인스턴스 정의 필요
instance Comparable<Int> { Boolean gt(Int v1, Int v2) { return v1 > v2; } } instance Comparable<String> { Boolean gt(String v1, String v2) { ... } } instance Comparable<Int> { Boolean gt(Person v1, Person v2) { ... } }
Java
복사
타입클래스 인스턴스에 정의된 함수는 오버로딩된 함수처럼 사용 가능
gt(1,2); gt("a", "b"); gt(Person(...), Person(...));
Java
복사
매개변수에 의한 다형성(제너릭)에 조합하기
Void sort<T>(List<T> lst) requires Comparable<T> { ... if (gt(lst[i], lst[j)) {...} } sort<Int>(List<Int>(4, 1, 2, 3)); sort<String>(List<String>("b", "c", "a")); sort<Person>(List<Person>(...));
Java
복사
타입클래스의 장점
라이브러리에 정의된 타입을 특정 타입 클래스로 속하게 만들 수 있음
제너릭 타입 다룰때 활용도 높음
// AS IS instance Comparable<List<Int>> { Boolean gt(List<Int> v1, List<Int> v2) { return v1.length > v2.length; } } instance Comparable<List<String>> { Boolean gt(List<String> v1, List<String> v2) { return v1.length > v2.length; } } // TO BE instance <T> Comparable<List<T>> { Boolean gt(List<T> v1, List<T> v2) { return v1.length > v2.length; } }
Java
복사
제너릭 타입이 타입 인자에 상관없이 항상 만족하는 성질을 표현 + 특정 타입 인자를 받은 경우에만 만족하는 성질 역시 표현 가능
instance <T> Comparable<List<T>> requires Comparable<T> { Boolean gt(List<T> v1, List<T> v2) { return gt(v1[i], v2[i) ... } }
Java
복사
타입클래스의 키워드
러스트: trait
스칼라: trait
하스켈: class

5.4. 카인드

타입의 타입
Void addUntil<L, T>(L<T> lst, T v, Int len){ while (length<T>(lst) < len){ add<T>(lst, v); } } // Int<String> 과 같은 타입은 있을 수 없음 addUntil<Int, String>
Java
복사
타입의 종류
일반적인 타입
타입 인자를 받을 필요 없이 매개 변수 타입이나 결과 타입으로 사용할 수 있는 타입
* 라는 카인드에 속함
제너릭 타입
일부는 타입 인자를 필요로 하는 타입이 존재함
예) ArrayList<T>, LinkedList<T>
Void addUntil<L, T>(L<T> lst, T v, Int len){ while (length<T>(lst) < len){ add<T>(lst, v); } } // Int<String> 과 같은 타입은 있을 수 없음 addUntil<Int, String>
Java
복사
ArrayList, LinkedList
* => * 카인드에 속함
Map
(*, *) => * 카인드에 속함
카인드 표시 활용하여 개선된 코드 예시
Void addUntil<(* => *) L, * T>(L<T> lst, T v, Int len){ while (length<T>(lst) < len){ add<T>(lst, v); } } // 통과 addUntil<ArrayList, Int>(...) // 거부 addUntil<,Int String>(...)
Java
복사
제너릭 함수 정의할 때 대부분의 타입 매개변수에는 * 카인드가 붙음. 다른 형태의 카인드는 매우 드뭄. 따라서 매번 * 를 붙이는게 번거롭기 때문에 언어레벨에서 일반적으로 많이 생략함

6. Summary

6.1. 다형성의 종류

서브타입에 의한 다형성
매개변수에 의한 다형성
오버로딩에 의한 다형성

6.2. 서브타입에 의한 다형성

객체 타입
AB이면, AB의 서브타입이다.
종류
이름에 의한 타입 - 상속
구조에 의한 타입 - 필드, 메서드 동일 구현
함수 타입
매개변수의 서브타입을 뒤집음
AB의 서브타입일때, B → CA → C의 서브타입이다.
결과 타입의 서브타입 관계는 유지
AB의 서브타입일때, C ⇒ AC ⇒ B의 서브타입이다.

6.3. 매개변수에 의한 다형성

제너릭
제너릭 타입
타입에 타입 매개변수를 추가한 경우 List<T>
제너릭 클래스
무엇이든 타입
무엇인가 타입

6.4. 서브타입 + 매개변수에 의한 다형성

전통적인 다형성
객체 지향 언어 = 서브타입에 의한 다형성
함수형 언어 = 매개변수에 의한 다형성
최근에는 두 다형성을 모두 지원하는 언어가 대부분
타입 매개 변수 제한
가변성
종류
공변: 제너릭 타입이 타입 인자의 서브타입관계를 보전하는 것
불변: 제너릭 타입이 타입 인자의 서브타입 관계를 무시하는 것
반변(반공변): 제너릭 타입이 타입 인자의 서브타입 관계를 뒤집는 것
제너릭 타입의 가변성
제너릭 타입 G , 타입 매개변수 T
GT 를 출력에만 사용하면 공변, 입력에만 사용하면 반변, 출력과 입력 모두에 사용하면 불변
G에 해당하는 타입
T를 출력에 사용
T를 입력에 사용
가변성
List1<T>
O
X
공변
List2<T>
O
O
불변
Int ⇒ T
O
X
공변
T ⇒ Int
X
O
반변
타입 검사기의 제너릭 타입의 서브타입 판단 방법
정의할때 가변성 지정하기
사용할때 가변성 지정하기

6.5. 오버로딩에 의한 다형성

오버로딩 종류
함수 오버로딩
메서드 오버로딩
연산자 오버로딩
함수 오버로딩
함수 선택
함수 오버로딩 시 호출할 함수를 자동으로 고르는 것
더 특화된 함수를 고름
정적 선택
함수를 고를때는 인자의 정적타입만 고려
정적 선택에 따라 의도와 다르게 동작할 수 있음. 따라서 서로 다른 타입들의 값을 인자로 받는 함수를 정의하는 용도로만 사용 권장
메서드 오버로딩
메서드 오버라이딩
동적 선택으로 동작 → 함수의 오버로딩 이슈 대응 가능
메서드 선택
인자의 정적 타입 고려 + 수신자의 동적 타입 고려
인자의 동적타입을 고려하지 않으므로 의도와 다르게 동작할 수 있음
자식 클래스에 있는 메서드의 결과 타입이 부모 클래스에 있는 메서드의 결과 타입의 서브타입 이어야함
타입 클래스
카인드