07. Type Systems
- Python: type 지정하지 않음
- java, c: type 지정
→내부적으로 타입을 가지고 있다.
Purposes
- 많은 연산자들을 위한 implicit context를 제공한다. → 명시하지 않아도 알 수 있게 해준다.
- C언어의 경우,
a+b
에서 두 변수가 정수 타입일 때, 정수 덧셈해준다.
- double/float인 경우 floating-point 덧셈
- Pascal 의 new p: block of storage를 heap에 생성, 위치 가리키도록 함 (pointer이므로 가능)
- C++, Java, C#: new my_type() 공간할당 및 생성자를 호출할 수 있다.
- 수행할 수 있는 작업에 제한을 걸 수 있다.
- 구조체에 Character를 더한다? → 불가능
- 정수를 인자로 받으려고 하는 함수에 file을 넘긴다? → 불가능 !
- explicit type (C언어, Java): 읽거나 이해하는데에 쉽게 해준다.
- 컴파일 시 타입을 알게 된다면, 최적화가 가능하다.
- Basic:
a=3
→ 찐으로 타입이 없다, 무조건 가장 큰 double 로 만듦
7.1 Overview
- bits 자체는 당연히 타입이 없다.
- 어셈블리 또한 타입이 중요하지 않다.
- high level의 경우 값에 타입을 연관짓는다.
- 타입 시스템의 구성요소
- 타입을 정의하고 이를 특정 언어 구조와 연결하는 메커니즘
- type equivalence, type compatibility, type inference 존재
- language constructs: 값을 가지는 것들, 값을 가지고 있는 것을 가리킬 수 있는 경우
- 서브루틴은 타입으로 고려되는 경우가 있다.
- first or second class 일 때 → 인자 전달 가능 first - 1. 변수 저장 2. 반환 3. 인자 전달 second - 3 가능, 1 or 2 third - 3 불가
- 함수의 인자가 다 같을 때 함수가 같은 것을 파악할 수 있음
- statically scoped language에서 서브루틴에 대한 레퍼런스를 동적으로 만들어 내지 않는 경우, 컴파일러는 함수 이름을 제대로 파악할 수 있음
Rules
- Type equivalence :두 값의 타입이 같은 지
- Type compatibility: 주어진 상황에 주어진 값이 사용될 수 있는지?
- Type inference: 표현식을 구성하는 요소들, 주변 환경을 가지고 표현식 자체의 타입을 유추하는 것
→ 다형성의 경우, 상황에 따라 달라질 수 있으므로 타입을 구별하는 것은 중요하다.
Type checking
- type clash: 규칙 위반
- strongly typed: 타입을 잘못 사용하고 있을 때 checking 해줌
- 1970년대 중반: strongly typed
- c언어 점점 strongly typed → 중간에 아닌 경우 존재
- non converting type casts: float a; ((int*) &a); (float → int)
- union: int라 쓰고 float / pointer array 섞어쓰는 경우
- c언어는 보통 compile 상 type checking
- statically typed: strongly + compile
- c언어, c++, java 포함 → 대부분의 타입체킹이 compile, 나머지는 runtime
- late binding: 다형성 쓸 때 주로 사용함
- 변수의 타입이 프로그램 중간에 변경될 수 있음
- 대부분 스크립트 언어
7.1.1 The Meaning of "Type"
- Denotational point of view: 집합에 속해있어야한다.
- Structural point of view: built-in type, composite type
- Abstraction-base point of view: 잘 정해지고, 일반적인 의미를 가지고 있는 operation들로 구성되어있는 interface ex) class, 모듈
7.1.2 Polymorphism
- 다형성은 여러 개의 형태를 가지고 있는 타입
- 공통적인 특성을 가지고 있어야하며 다른 특성에 의존해서는 안됨 Parent: 공통적인 특성 Parent p = new Child(); 의 경우 p에서 호출할 수 있는 함수는 Parent
- Parametric polymorphism: generics
- explicit: statically typed language 에 나타난다.
- implicit: compile 에 만들어진다.
- Subtype polymorphism: 상속 관계
- static typing이랑 같이 일어날 경우, 대부분이 컴파일 때 일어남
- 실질적으로 호출되는 함수는 runtime에 결정됨
- Smalltalk, Python, Ruby: single mechanism → 특별히 구분하지 않고 run time상에서 타입체크
7.1.3 Orthogonality
- higly orthogonal language 의 경우 쉽게 사용할 수 있고, 이해가 쉽다.
- C, Algol68
-
statement, expression의 구별을 제거
-
procedure 형태로 사용할 수 있는 함수 제공, void로 모두 받을 수 있다.
-
값 반환하는 함수를 사용하는 경우 있을 수 있음 (side effect 위해서) → 반환 값을 void 인 것처럼 타입 캐스팅해서 사용한다.
→ procedure 부르듯이 function을 부르지 못하는 경우를 막아서 orthogonality를 높게 해줌.
- 변수 값을 지우는 경우
- pointer types: null 반환
- enumerations: none of the above
- int, char 의 경우 무슨 값? → 빈 값을 각기 다른 값을 가지고 있다.
- Optional 지원
- 예시 - OCaml
- None 을 통해서 어떤 타입이 들어와도 같은 값을 반환
- Composite type의 초기화 (class, union, array)
- c, java 에서는 초기화할 때 위처럼 사용가능 하지만 Ada는 값 변경시에도 사용 가능
7.1.4 Classification of Types
- int, integer, double 등 타입에 따라서 명칭이 다르다.
- 대부분 언어는 빌트인 타입을 제공한다
- integers, characters, Booleans, real
- Character: 아스키 1바이트 사용 → 최근은 유니코드 2바이트
Numeric Types
- 기본적인 숫자 타입
- c, fortan: 정수, 실수 구별해서 범위 지정하는 경우도 존재한다.
- int < long < longlong
- 하지만 이식성이 떨어진다.
- C, C++, C#: signed/unsigned 구별
- Fortan, C99, Common Lisp: 복소수 존재
- discrete
- integers, Booleans, characters
- enumerations {MON, TUE}
Enumeration Types
- 가독성 좋은 프로그램을 위해서 만들어짐
- 원소들의 집합
Pascal
- 순서 존재
- int, enumeration 혼용해서 사용한다면 type crash 발생
C
- 순서대로가 아님 (java 도 정수 지정 가능)
Ada
weekday'pos(mon) = 1
weekday'val(1) = mon
java
enum arm_special_regs { fp(7), sp(13), lr(14), pc(15);
private final int register;
arm_special_regs(int r) { register = r; }
public int reg() { return register; }
}
...
int n = arm_special_regs.fp.reg();
- 클래스로 사용, set, get 가능
- Pascal, C의 경우 같은 스코프에서 두 개 이상 같은 이름의 enumeration type을 사용할 수 없다.
- Java, C# 은 가능하다.
- Ada: 컴파일러가 유추 가능할 경우 가능
- C++
enum class Days { sun, mon, tue, wed, thu, fri, sat };
Days d = Days::sun;
Subrange Types
- pascal에서 처음 사용
- discrete base type 이어야 한다.
- type: new 와 함께 사용, 섞어 쓸 수 없다.
- subtype: weekday, workday섞어쓸 수 있다. (부분 집합의 개념)
- 가독성이 좋은 프로그램, byte 타입이므로 최적화 가능
Composite Types
- records, unions, arrays, sets, pointers, lists, files
- records: field의 집합, cobol에 의해서 소개됨
- unions: 공간 하나로 공유
- arrays: 함수라고 생각해도 된다. index로 값 연결, characters의 집합을 string 취급하기도 한다.
- sets: 같은 값을 중복해서 가질 수 없다.
- pointers: l-values (int*: int를 가리키는 주소값), recursive data types에 주로 사용 (구조체에서 recursive하게 사용가능 하도록 해줌)
- lists: 길이가 정해져 있지 않은 경우
- files: 타입으로 들어가 있는 경우도 존재
7.2 Type Checking
- Type compatibility: 특정 type의 object가 사용될 수 있는지
2 + 3.2
: 타입이 다르지만 허용해준다.
- type conversion: 강제 형변환
- type coercion: 자동 형변환
- non-converting type casts: 시스템에서 주로 사용, 한 가지 종류의 타입에 대한 비트를 다른 값으로 변경 (메모리 두고 다르게 변환 / int → char)
2+3
= 5과 같은 새로운 expression의 타입은?
- type inference: 값을 유추한다.
- pascal 의 경우 type casting 필요
7.2.1 Type Equivalence
: 새로운 타입 정의시 타입이 같다는 것을 어떻게 정의할 것인가?
- Structural equivalence: 같은 컴포넌트로 구성되어 있을 때, 같다
- Name equivalence: 코드가 어떻게 보여지는지, definition: 새로운 타입이다. (대부분 지원)
Structural equivalence
언어마다 정의가 조금씩 다르다.
Pascal
→ a, b 순서가 달라짐, ML에서는 다르다고 보고 대부분의 언어는 같다고 본다.
→ 대부분언어는 다르다고 보는데, 일부 언어는 compatible하다고 볼 수 있다.
- 메모리 구조 측면에서 보면 더 직관적으로 볼 수 있다.
- 프로그래머 입장에서는 다른 타입, 언어 입장에서는 같은 타입
- 즉 마지막 줄은 에러가 안난다.
Name equivalence
두 개 따로 타입을 정의 → 구조가 같더라도 다르게 본다.
- new_type: alias
- 두 개를 같은 타입으로 볼 것인가? → alias 지만 다른 타입으로 봐야하는 경우가 있다.
- strict name equivalence : alias 타입은 다른 타입이다
- loose name equivalence : alias 타입은 같은 타입이다 (대부분 pascal)
- Ada: strict/ loose 둘 다 지원 → derived: 새로운 타입, subtype: compatible
→ 타입을 정의할 때 기준, 즉 strict 인 경우에는 p, q 당연히 같고, r 과 u는 타입이 지정된 alink를 말하므로 같고 t는 다른 줄에서 타입을 정의하므로 다르다.
→ loose 의 경우, alias도 같다고 보므로, alink의 alias인 blink도 포함시키는 것.
- strict name equivalence: p,q 같음 r,u 같음
- loose name equivalence: r, s, u 같은 타입
- structural equivalence: 다 같음
Equivalence Examples
- C 언어: struct, union 은 nonequivalent types, 나머지는 Structurally equivalent
- Java: class, interface 는 nonequivalent types, Structural → int, Arrays ⇒ Structurally equivalent
Type Conversion and Casts
: 정적 타이핑(compile 시점)되는 언어에서 특정 타입의 값이 기대되는 많은 맥락이 존재한다.
a := expression
: 두 변수의 타입이 같다고 예상
a+b
: 일반적으로 같은 타입
foo(arg1, arg2, ..., argN)
: formal parameters(함수에 정의된 인자) 과 인자들의 타입이 일치할 것이라고 예상
- 예상과 제공한 타입이 완전히 일치하려면 강제형변환이 필요하다. (type cast: 강제형변환, coercion: 자동형변환)
- conversion이 runtime 실행할 때, 특별한 코드가 실행되는 경우, 실행되지 않는 경우가 존재한다.
-
형변환을 해야하는 타입들이 structurally equivalent 하며 name equivalence 한 언어를 사용하는 경우
= 메모리 구조는 같지만 이름이 다른 경우
-
타입들이 서로 다른 값을 가지고 있지만 교집합의 value가 같은 방식으로 나타날 경우
ex) 0..20
10..30
: subrange 에서 겹치는 경우 존재
- 유효한 범위에 있는지 runtime 상 check 해줘야한다.
- 실패 → dynamic semantic error
- 성공 → low level의 기존 값 변동없이 사용
- 안전하지 않더라도 속도를 높이기 위해서 disable 가능
-
타입이 저장하는 방식이 다르지만 (int, float과 같이 low - level 자체가 다름), 서로의 값들 사이에서 일부의 일치점을 정의하는 경우
- 32 비트 정수 → a floating point number 작은 범위 → 큰 범위이므로 손상 없다.
- 실수 → 정수 큰 범위 → 작은 범위, overflow 발생 machine instruction 필요로 한다.
- integer 종류가 다른 경우 (char, short, int) 8bit → 16bit 인 경우 확장한다. machine instruction 필요하는 것은 아니다.
Nonconverting Type Casts
: 변수의 format을 바꾸는 것이 아니라 있는 그대로를 사용하겠다는 것임
- 값의 손실이 발생하지 않지만 위험한 상황인지 감지하기가 어려워진다.
- float 데이터 포맷을 유지하면서 int 를 바꾸므로 위험하다. float 2.3f; 를 int 2;로 바꾸는 것처럼 데이터 포맷을 바꾸는 것이 아님.
- ex) 구조체 → byte 단위로 처리하는 경우
- c++ static_cast: 타입 conversion 수행, 기존 방식 (int) 처럼 사용 reinterpret_cast: nonconverting type cast 수행 float → int
*((int*)&f)
dynamic_cast: 다형성 타입의 포인터 방식 사용 Parent p; Child q = new Child*(); p = q; 가능 p를 child 로 바꾼다면? static을 사용한다면 p라는 것이 처음부터 child 인지 parent인지 알 수 없다. child* x = dynamic_cast<child*>p;
: 바꿀 수 있는지 확인해줌, 바꿀 수 없다면 null이 들어간다.
7.2.2 Type Compatibility
: 사용할 수 있는지 없는지 따진다.
- Assignment statement: 호환 가능한 경우인지?
- operands of '+' 타입: +를 지원하는 일반적인 타입들이어야 한다.
- subroutine 호출: subroutine 에 전달되는 타입은 선언시 사용한 파라미터 타입과 호환되어야 한다.
→ 언어마다 호환성 범위에 따라서 허용 범위는 다르다.
Ada
- 타입이 같을 때
- S가 T의 subtype, 두 개가 같은 Parent
- 같은 배열에서 같은 타입인 경우
Coercion
: 자동형 변환
- 실행 시점에 수행되는 dynamic semantic 확인 코드가 필요하다
- low-level에서 변경이 필요하므로 런타임상 변환 필요
short int s;
unsigned long int l;
char c;
float f;
double d;
s=l;
l=s;
s=c;
f=l;
d=f;
f=d;
- 프로그래밍할 때 정확한 명시를 하지 않아도 타입을 섞어쓸 수 있다는 장점은 있지만 type security를 낮춘다.
Universal Reference Types
: 어떤 타입이든 저장할 수 있는 컨테이너 타입
ex) Jave: Object
, C/C++ : void *
- void * p = q 인 경우, p의 값을 모른다. → 컴파일러는 object에 대한 어떤 연산을 허용하지 않는다. → 객체형변환 후 연산 가능
- integer에 대해 reference 가지고 있는 것을 다른 타입의 reference로 재할당하는 것도 까다롭기 때문에 타입 safety가 필요하다.
Class Cast
객체지향언어에서, 왼쪽 객체가 오른쪽 객체 할당에 지원이 가능한지 유효성 검사 필요하다.
Student s = new Object();
(더 일반적 → 덜 일반적 )
→ 상속받은 Student 는 더 많은 기능이 존재할텐데, Object에 없는 내용을 지원할 것인가?
class A {
public void print() {
System.out.println("class A"); }
}
public class Main {
public static void main(String[] args) {
A a = new A();
Object o = new String("hello");
a = (A)o;
}
}
int main() {
A* a;
B* b = new B;
C* c = new C;
a = dynamic_cast<A*>(c);
print(a);
a = dynamic_cast<A*>(b);
print(a);
delete c;
delete b;
}
- type tag 가 없다면 → type check 자체가 불가, runtime 시 확인할 방법이 없다. type conversion을 해줘야한다.
7.2.3 Type Inference
: 타입 추론, 전체적인 결과 타입은 뭐가 되는가? 2+2.3=?
- 산술 연산 경우, 피연산자 타입을 따라간다. 피연산자 타입이 다를 경우, 다른 한 피연산자의 타입을 형변환한다.
- 비교연산자의 결과는 boolean
- 함수 호출의 결과는 header에 정의된 타입이다.
- 결과는 왼쪽 type에 따른다.
Declarations
Ada
- 반복문의 인덱스 변수는 for 내에 사용할 수 있도록 해준다.
- ada에서는 max bound에 타입을 맞춰준다.
- c는 for(int i = 0, ~) 처럼 타입 명시해준다.
Scala, C# 3.0, C++11, Go, and Swift
C++
- decltype
- 확장된 개념으로, 객체의 타입이 정해지지 않은 상태에서 임의의 결과의 타입을 사용하는 것이다.
7.3 Parametric Polymorphism
: 컴파일 시점에 타입 추론을 허용하지 않는 언어의 경우, run time에 실행해야 한다.
Scheme
(define min (lambda (a b) (if (< a b) a b)))
- 타입이라는 개념이 없기때문에, 지원한다는 가정하에 실행시점까지 미룬다.
- 지원하지 않는 타입이 들어간다면 런타임 에러 발생하도록 한다.
Smalltalk, Objective C, Swift, Python, Ruby
: 어떤 메소드가 호출되든간에 지원한다고 가정한다.
: 객체가 현재 사용된 메소드가 지원되는 메소드면 사용할 수 있다고 가정
duck typing: 객체가 요구되는 메소드를 지원할 때 수용가능한 타입을 가지고 있을 때
7.3.1 Generic Subroutines and Classes
: 다형성은 런타임 체크가 필요하다 → 시간이 많이 걸린다.
: 클래스 선언시에 타입을 명시해서 컴파일 시점에 검사할 수 있다.
(Java, C++, C# 등)
객체지향 언어에서의 일반화
: 클래스를 파라미터화 한다.
- generics를 사용하지 않는다면 type cast가 필요하고, 컴파일 시점 체크가 힘들어진다.
- 컴파일 시점에 T에 따른 코드를 만든다라고 생각해도 좋다.
- 함수까지 따로 만들 필요 없으므로, 같은 set of argument에 대해서는 공유하긴 한다.
- 같은 크기를 가지고 있는 int, float 또한 union처럼 공유할 수 있지 않을까?
Java
- T를 Object로 바꿔서 사용한다.
- 결국엔 Object 기반이다. → 컴파일러가 타입을 미리 알고 있으므로 Typecast를 알아서 해준다.
C#
- C++ , Java 섞음
- C++ 처럼 primitive or value types인 경우, 서로 다른 인스턴스를 만든다.
- Java처럼 Class 타입이 들어간다면 하나의 코드를 가지고 사용한다.
Generic Parameter Constraints
: 일반화는 추상화이므로 모든 정보를 제공하는 것이 중요하다.
: 제네릭스 인자에 대해 제약을 거는 방식이 있다.
: 행해질 수 있는 operation들을 명확하게 처리할 수 있도록 해줘야한다.
Java, C#
public static <T extends Comparable<T>>
void sort(T A[]) {
...
if (A[i].compareTo(A[j]) >= 0)
...
}
C++
Implicit Instantiation
: 클래스는 타입이므로 사용하기 전 인스턴스를 생성해줘야 한다.
- C++, Java, C#에서는 오버로딩의 개념이기때문에 따로 생성해주지 않는다.
Generics의 C++: 다양한 기능 구현해준다.
Java, C# : 다형성을 사용, 컨테이너 생성까지만 일반화 사용하도록 한다.
7.4 Equality Testing and Assignment
: 구조체, 리스트, 클래스의 경우 비교 같은 경우에 애매한 경우가 많다.
- aliase면 같다고 할 수 있는가?
- 문자열의 내용이 bit by bit로 같아야 하는가? → 문자열만 일치하면 되는가?
- 전체 10칸이 다 같아야하는가?
- 출력했을 때 같으면 같다고 하는건가?
→ (2)의 경우에 전체가 일치하다고 한다면 너무 low level이며 가비지 값이 들어간다면 fail한 경우가 많아진다.
String s1 = "hello";
String s2 = "hello";
String s3= new String("hello);
Shallow comparison: 위치 비교만 한다, a= b;
Deep comparison: 실제 오브젝트가 같은지 확인한다, 복사본을 가리키도록 한다.
Value model 의 경우, 값을 복사한다. value가 pointer라면 shallow 인 경우이다.
→ 대부분 Shallow 를 사용한다.
class A {
public:
bool operator==(const A& c) {
return (a == c.a && b == c.b);
}
A& operator=(const A& c) {
a = c.a;
b = c.b;
}
private:
int a;
int b;
}