by Robert W. Sebesta
프로그래밍 언어론을 읽고 내맘대로 정리해보았다.
[기본적인 사항]
- 프로그래밍 언어의 어휘항목은 수치 리터럴, operator, 특수어를 포함한다.
- 프로그램은 문자보다는 어휘항목들로 구성된 문자열이라고 할 수 있다.
- 단언(assertion)
- 문장 앞에 위치하면 프로그램의 그 지점에서 프로그램의 변수들에 대한 제약 사항이다.
- 문장 뒤에 위치하면 그 문장이 실행된 후 해당 변수에 대한 새로운 제약 사항을 기술한다.
- 부프로그램(subprogram)은 전체 프로그램을 구성하는 조각이며, 함수 하나라고 생각할 수도 있을 것 같다.
- 컴퓨터 과학에서 스칼라(scalar) 라고 지칭할 때는, 배열이든 문자열이든 상관없이 ‘그것’들 중의 하나를 의미한다.
- 프로그래밍 언어는 다음 세 가지 방법으로 구현(implementation)될 수 있다.
-
(1) Compiler implementation
- 컴퓨터에서 직접 실행될 수 있는 기계어로 번역될 수 있다.
- 컴파일러가 번역하는 언어는 원시언어(source language)이다.
- 컴파일 단계는 대체로 아래 순서를 거친다.
원시 프로그램 -> 어휘 분석기 -> 구문 분석기 -> 중간 코드 생성기 -> 코드 생성기
- 컴파일러에서는 주석을 사용하지 않으므로, 어휘 분석기는 원시 프로그램에 포함된 주석을 무시한다.
- 구문 분석기는 어휘들을 가져와 구문 구조를 표현한 파스 트리를 생성한다.
- 중간 코드 생성기는 원시 프로그램과 컴파일러의 최종 출력인 기계어 프로그램 간의 중간 수준에 위치한다.
- 코드 생성기는 프로그램의 최적화된 중간 코드 버전을 동등한 기계어 프로그램으로 변환한다.
-
(2) Pure interpretation
- 프로그램은 어떠한 번역 과정 없이 인터프리터에 의해 해석된다.
- 원시 수준의 디버깅 연산을 쉽게 구현할 수 있다.
- 그러나 고급 언어 문장을 해석(decode)해야 하므로, 컴파일된 시스템보다 10배 ~ 100배 실행 시간이 느리다.
- 문장이 몇 번 실행되는지 상관없이, 그 문장은 매번 해석되어야 한다.
-
(3) Hybrid implementation system
- 고급 언어 프로그램을 용이한 해석이 가능하도록 설계된 중간 언어로 번역한다.
- 따라서 원시 언어 문장을 한 번만 해석(decode)되므로 순수 해석보다 빠르다.
Perl
프로그램은 해석 전에 오류를 탐지하고 부분적으로 컴파일된다.- JIT(Just In Time) 방식은 프로그램을 중간 언어로 번역하고, 실행 중에 중간 언어 메소드가 호출되면 그 메소드를 기게 코드로 번역한다.
-
변수
-
변수는 아래의 요소들로 구성된다.
-
이름(name)
- 프로그램에서 어떤 개체를 식별하기 위해 사용되는 문자열이다.
- 특수어(special word)는 수행될 행동들을 미리 정해 놓은 것이다.
- 대부분의 언어에서 특수어는 예약어이다.
- 어떤 언어에서 특수어는 단지 키워드이다.
- 키워드(keyword)는 문맥에 따라 달라질 수 있다. ex) javascript의 this
-
주소(address)
- 그 변수와 연관된 기계 메모리 주소이다.
- 두 개 이상의 변수 이름이 동일한 메모리 위치를 접근하는 데 사용되면, 그 변수들은
별칭(alias)
이다.
-
타입(type)
- 그 변수가 저장할 수 있는 값들의 범위와 타입에 따른 연산들의 집합을 결정한다.
-
값(value)
- 그 변수에 연관된 추상적인 메모리 셀, 혹은 셀들의 내용이다.
-
바인딩(binding)
- 하나의 속성과 하나의 개체 간의 연관관게이다.
- ex) 변수와 그 타입이나 값 사이, 연산과 기호 사이 등…
- ex)
*
기호는 보통 언어 설계 시간에 곱셈 연산에 바인딩된다. - ex) C의
int
데이터 타입은 언어 구현 시간에 가능한 값들의 범위에 바인딩된다.
정적(static) 바인딩
: 실행 시간(runtime)이 시작되기 전에 일어나고, 프로그램 실행 전체에서 변하지 않는 상태로 남아있다.- 변수 이름과 타입이 동시에 바인딩된다.
동적(dynamic) 바인딩
: 런타임 중에 일어나거나, 프로그램 실행 과정에서 변할 수 있다.- 변수 이름이 타입에 일시적으로 바인딩된다.
- 동적 타입 바인딩을 사용하는 언어는 수치 데이터를 제네릭으로 작성한다.
- 동적 타입 바인딩은 컴파일러 오류 탐지 능력이 떨어지므로 덜 신뢰적이다.
- 하나의 속성과 하나의 개체 간의 연관관게이다.
-
존속기간(lifetime)
-
변수가 특정 메모리 위치에 바인딩되어 있는 기간이다.
-
- 정적
- 프로그램 실행이 시작되기 전에 메모리 셀에 바인딩되어, 종료될 때까지 동일한 메모리 셀에 바인딩된다.
-
- 스택-동적
- 선언문이 구현될 때 메모리에 바인딩되고, 타입은 정적으로 바인딩된다.
ex) javascript의 함수 내부에 선언된 지역변수 -> 따라서 재귀 호출이 가능해진다.
-
- 힙-동적
- 실행 시간에 할당되고 회수되는 익명의 추상 메모리 셀에 바인딩되고, 포인터나 참조 변수로 참조한다.
-
- 묵시적 힙-동적
- 값이 할당될 때마다 변수의 모든 속성이 힙 메모리에 바인딩된다.
-
-
영역(scope)
- 그 변수를 참조할 수 있는 문장들의 범위이다.
- 변수가 프로그램 단위나 블록 내부에 정의되면 지역적(local)이다.
정적 영역(static scope)
- 변수의 영역이 실행 전에 결정될 수 있다.
- 많은 언어에서 새로운 정적 스코프를 실행 코드 중간에 정의할 수 있다.
- 블록(block)으로 지정된 코드에 변수가 진입할 때 메모리에 할당, 빠져나올 때 회수된다.
- 블록에 의해 생성된 영역은 subprogram(함수)에 의해 생성된 영역과 동일하게 취급된다.
- javascript는 중첩된 함수에 대해 정적 스코프를 사용하지만, 블록 스코프는 그렇지 않다.
- javascript에서 지역 변수는 함수의 어느 위치에서도 선언될 수 있지만, 변수의 영역은 항상 함수 전체이다.
- 하지만 es6에서 블록-레벨 스코프인 let과 const가 나왔다!
- 전역 변수의 영역은 그 선언으로부터 프로그램 끝까지이다.
- 참고로 javascript의 전역 변수는 동일한 이름을 갖는 지역 변수를 선언한 함수에서 그 전역 변수에 접근할 방법이 없다. (shadow variable)
- 정적 영역을 사용할 때 소프트웨어는 계속 재구조화되어야 하므로, 변수와 함수에 대한 접근을 제어하기 위해 전역 변수 등을 많이 사용하게 된다.
동적 영역(dynamic scope)
함수들이 배치된 관계가 아니라 호출 시퀀스에 기반하며, 실행 시간에 결정된다.function big() { function sub1() { var x = 7; } function sub2() { var y = x; var z = 3; } var x = 3; }
- 위의 sub2에서 참조되는 식별자 x의 의미는 동적이며, 컴파일 시간에 결정될 수 없다.
- 어떠한 지역 선언에 대한 탐색이 실패하면, 그 함수의 부모에서 탐색되고, 이 과정을 반복한다. 이때 어떠한 부모에서도 발견되지 않으면 실행-시간 오류이다.
- 동적 영역은 비지역 변수에 대한 참조를 정적으로 타입 검사할 수 없다.
- 동적 영역은 참조의 의미를 결정하기 위한 함수 호출 시퀀스를 알아야 하므로 프로프램 판독이 힘들어진다.
-
(참고) 이름 상수(named constant)는 단지 한 번만 컴파일 시간에 값에 바인딩되는 변수이다.
-
(참고) 초기화(initialization)는 변수가 메모리에 바인딩되는 시점에 값에 대한 변수의 바인딩이다.
데이터 타입
-
데이터 값들의 모임과 그 값들에 대해 미리 정의된 연산들을 의미한다.
-
타입 시스템은 오류 탐지, 모듈 간 인터페이스 보장(cross-module), 문서화(documentation)로써 중요하다.
-
식별자를 변수로 생각할 수 있으나, 어떤 언어에서는 데이터 타입을 갖지 않으므로 변수 속성 중 하나이다.
-
Numeric, Boolean, String, Enum, Array, Tuple, List, Union, Pointer, Reference
-
문자 스트링 타입(Character string type)
- 값이 일련의 문자들로 구성된다.
- 부분 스트링 참조는 주어진 문자열에서 부분 문자열에 대한 참조이며, 배열에서 논의될 때는 slice이다.
- Perl, Javascript, Ruby, PHP는 패턴 매칭 연산자를 포함한다. 패턴 매칭 식은 수학적 정규식에 기초하고 있어서 정규식(regular expression)이라고 부른다.
정적 길이 스트링(static length string)
- 스트링이 생성될 때 설정된다.
- ex) Python이나 Java의 String
제한된 동적 길이 스트링(limited dynamic length string)
- 0부터 최대 길이까지의 임의의 문자들을 저장한다.
동적 길이 스트링(dynamic length string)
- 최대 길이의 제한이 없는 가변 길이를 갖는다.
- 유연하지만, 동적인 메모리 할당과 회수로 복잡한 기억공간 관리가 필요한데, 3가지 방법이 있다.
- 연결 리스트에 저장 -> 링크에 대한 여분의 메모리가 필요하며, 스트링 연산이 복잡하다.
- 스트링을 힙에 할당된 개개의 문자들을 가리키는 포인터들의 배열로 저장 -> 연결 리스트보다는 스트링 연산이 빠르다.
- 스트링 전체를 인접한 메모리 셀들에 저장 -> 스트링이 늘어날 수록, 메모리 영역을 찾고 이전 셀을 회수하는 관리가 필요하다.
- ex) Javascript, Peral, C++의 String
-
열거 타입(Enumeration type)
- 이름이 있는 상수들이 열거되어, 열거 상수(enumeration constants)들을 정의하고 그룹핑하는 타입이다.
- 값을 할당하지 않아도 전형적으로 정수 값이 묵시적으로 할당된다.
enum Hello { First, // 0 Second, // 1 }
-
배열 타입(Array)
- 동질적인 데이터 원소의 집합이며 C, C++, Java에서 배열의 모든 원소는 동일한 타입이어야 한다.
- Javascript, Python, Ruby에서는 변수가 객체나 데이터 값에 대한 타입이 없는 참조이기 때문에, 배열의 원소들은 다른 타입들을 참조할 수 있다.
- 배열의 첨자(subscript)는 그 배열의 값을 가리키는 값이다. ex) A[1]에서 1
- Javascript 배열에서는 존재하지 않는 원소에 대해 참조하면 undefined를 반환한다.
- 배열의 슬라이드(slice)는 배열의 어떤 부분 구조이며, 주어진 첨자의 범위를 갖는 원소들로 구성된 배열을 반환한다.
- 배열 원소에 접근하는 코드는 컴파일에 생성되어야 하고, 런타임에 원소의 주소를 생성하도록 실행되어야 한다.
- 연관 배열(associative array)
- key-value 구조이다.
- key를 통해 연관되는 값을 얻을 수 있는 자료구조이다.
- Perl, Ruby 등에서 hash, Lua에서는 table타입이이 연관 배열이다.
Union type vs Intersection type
- 유니온 타입은 명시한 모든 타입들의 합집합이다.
ex) const test: A | B -> test는 A 또는 B타입 - 인터섹션 타입은 명시한 여러 타입을 하나의 단일 타입으로 결합한 일종의 교집합이다.
ex) const test: A & B -> test는 A와 B타입 각각의 멤버를 1개 이상씩 가져야 한다.
- 유니온 타입은 명시한 모든 타입들의 합집합이다.
-
포인터 타입(Pointer)
- 포인터는 동적으로 할당되는 힙 메모리의 한 위치를 접근하는 데 사용된다.
- 이미 회수된 힙-동적 변수의 주소를 가리키는 문제가 있다. (dangling pointer)
- 이때 힙-동적 변수를 garbage라고 부르며, 메모리 누수를 일으킨다.
- 이러한 문제로 최근의 언어들은 포인터를 참조 타입으로 대체해, 메모리 회수 문제를 최소화한다.
-
참조 타입(reference type)
- 포인터는 메모리의 주소를 참조하지만, 참조 변수는 메모리의 객체나 값을 참조한다.
- 객체 지향 언어인 python, ruby, lua의 모든 변수는 참조 변수이다.
- 포인터와 참조 타입에서 쓰레기 회수
- (1) 참조 계수기(reference counter)
- (2) 표시-수집(mark-sweep)
-
(참고) 변수가 타입에 대한 바인딩이 정적이면 타입 검사도 항상 정적이며, 동적이면 런타임에서 타입 검사를 한다.
-
(참고) 프로그래밍 언어는 타입 오류가 항상 탐지되면 강 타입 언어(strongly typed language)이다.
식과 배정문(Expression and assignment statement)
- 식(expression)은 어떠한 단일 값으로 표현될 수 있는 코드이다.
- 문(statement)은 실행가능한 독립적인 코드 조각이다.
- 선택문(selection statement), 반복문(iterative statement)…
- 무조건 분기문(unconditional branch statement)은 실행 제어를 프로그램상에서 명시된 위치로 이동시킨다. 보통 루프 탈출로 사용하는데, 고급언어에서는 많이 사용하지 않는다.
:test echo "hi" goto test ...
- 많은 오류가 식 평가 과정에서 발생한다.
- 가장 공통된 오류는 연산의 결과가 저장되어야 하는 메모리 셀에 표현될 수 없을 때 발생한다.
- ex) 그 결과가 너무 큰지 작은지에 따라 오버플로(overflow), 언더플로(underflow)이다.
- 소수점의 오버플로, 언더플로, 0에 의한 나누기는 런타임 오류이다.
- 런타임에서의 오류를 예외(exceptions) 라고도 부른다. 에러(error)와 다르다!
- 할당문은 변수(상태)의 값을 변경하는 부수 효과(side effect)를 야기한다.
- 함수에서의 사이드 이펙트는 함수가 자신의 매개변수들 중 하나 혹은 전역 변수를 변경할 때 발생한다.
- 프로그램에서 동일한 값을 갖는 임의의 두 식이 프로그램 동작에 영향을 미치지 않으면서 임의의 위치에서 서로 다른 식으로 대체될 수 있다면, 그 프로그램은 referential transparet하다.
result1 = (fun(a) + b) / (fun(a) - c); temp = fun(a); result2 = (temp + b) / (temp - c);
위 코드에서 함수 fun이 사이드 이펙트가 없다면 result1과 result2는 동일해야 한다.
- 타입 강제 변환은 컴파일러가 수행하는 묵시적 타입 변환(implicit type conversion)이다.
- 명시적인 타입 변환은 명시적 타입 변환(Explicit Type Conversion) 또는 캐스트(cast)이다.
부프로그램(Subprogram)
-
overloaded subprogram은 동일한 참조 환경에서 다른 이름을 갖는다.
function test(a: number); function test(a: boolean); function test(a) { return a; } test(1); test(true); test("1"); // No overload matches this call
-
generic subprogram은 호출할 때마다 다른 타입의 데이터에 게산을 수행한다.
function test<T>(a: T) { return a; } test<number>(1); test<boolean>(1); // type error
-
부프로그램의 header는 헤더 다음에 오는 구문이 어떤 종류(특히 프로시저, 함수)인지 명시한다.
-
간접적으로 부프로그램을 호출하는 일반적인 에시는
콜백(callback)
이다. -
부프로그램의 형식 부분인 배치(layout)를 활성화 레코드(activation record)라고 하며, 정적 바인딩 된다.
-
일단 Javascript에서의 부프로그램은 함수라고 볼 수 있으므로, 함수라고 지칭하려고 한다.
-
함수는 구조적으로 프로시저를 닮았지만 의미적으로는 수학 함수가 모델이다.
-
함수는 새로운 사용자 정의 연산자를 정의한다. 예를 들어, 어떤 언어에 지수 연산자가 없다면 이 연산을 할 수 있는 함수를 작성할 수 있다. 아래 result의 결과는 동일하다.
// c++ float power(float base, float exp) { float result = 1; while (exp != 0) { result *= base; --exp; } return 0; } result = 3.4 * power(10.0, x) result = 3.4 * 10.0 ** x
-
매개변수
- 매개변수로 전달된 데이터는 함수의 지역 변수로 접근된다.
- 접근하는 데이터가 비지역 변수라면 함수의 호출 사이에 그 비지역 변수에 새로운 값을 할당해야 한다.
-> side effect! - 메소드는 비지역 참조와 매개변수를 통해 외부 데이터에 접근한다.
-> side effect!- 메소드가 처리하는 데이터는 메소드가 호출된 객체이다.
- ex) javascript의 객체 내부에 선언된 함수를 호출
- Haskell과 같은 순수 함수형 프로그래밍 언어는 변화할 수 있는 데이터를 갖지 않는다.
- 따라서 어떤 방법으로든 메모리를 변화시킬 수 없다.
- 단순히 계산을 하고 결과 값(또는 함수, 함수도 값이다)을 반환한다.
parameter: 부프로그램 헤더에 명시된 매개변수
argument: 부프로그램을 호출할 때 그 파라미터에 바인딩되는 매개변수, 인수
- parameter는 디폴트 값을 갖는다.
- ex) function test(a, b = 1)
-
지역 참조 환경
- 지역 변수는 함수 내부에 정의되고, 스코프가 함수의 본문에 제한된다.
- 지역 변수가 스택-동적이면 함수가 실행될 때 메모리에 바인딩되고 끝날 때 해제된다.
- 재귀 함수의 지역 변수는 스택-동적이어야만 한다.
- 스택-동적 지역 변수에 대한 접근은 간접 주소 방식이다.
- 지역 변수가 스택의 어디에 위치할지 런타임에서만 결정할 수 있기 때문이다.
- 대부분의 현대 언어에서 함수의 지역 변수는 디폴트로 스택-동적이다.
-
함수
-
대부분의 언어에서 함수는 값을 전달하거나 참조 전달되는 매개변수를 갖는다.
-> side effect, alias 발생 -
특정 언어에서 부프로그램은 일급 객체이다.
- 매개변수로 전달될 수 있고, 함수로부터 반환될 수 있고, 변수에 배정될 수 있다.
- ex) javascript의 function
-
클로저(closure)는 중첩된 부프로그램의 참조 환경이다.
- 부프로그램이 프로그램의 임의의 위치에서 호출되는 것을 허용한다.
- 따라서 이때 변수의 존속기간은 정의된 함수가 아니라 전체 프로그램의 활성화 기간이다.
- 거의 모든 함수형 프로그래밍 언어는 클로저를 지원한다.
- 이 언어들은 정적 스코프이며, 중첩 함수를 허용하고, 함수가 매개변수로 전달될 수 있다.
- 아래 예시에서 makeAdder가 호출될 때 생성되는 x의 존속기간은 프로그램 존속기간이 된다.
function makeAdder(x) { return function (y) { return x + y; }; } var add10 = makeAdder(10); document.write(add10(20));
-
-
코루틴
- 코루틴은 특별한 종류의 부프로그램이다.
- 코루틴의 시작은 호출(call)이라기보다는 리쥼(resume)이라고 불린다.
- 부프로그램의 특징처럼, 코루틴도 주어진 시점에서는 하나의 코루틴만 실제로 실행된다.
- 모든 코루틴이 구성되었을 때 마스터 프로그램은 하나를 리쥼한다.
- 나머지 코루틴들은 작업이 끝날 때까지 어떠한 순서로 서로서로 리쥼한다.
- 코루틴 실행이 코드 끝부분에 도착하면 제어는 이 코루틴을 생성한 마스터로 전달되어 끝난다.
- 따라서 코루틴은 병렬로 실행하는 것처럼 보인다.
- ex) javascript의 generator와 async/await
동시성(Concurrency)
- 소프트웨어에서 동시성은 다음의 4가지 수준에서 발생한다.
- 명령어, 문장, 단위, 프로그램
- 프로그래밍 언어에서는 단위(함수) 수준과 문장 수준에서 논의된다.
- ex) 웹 브라우저는 동시에 많은 함수를 실행해야 한다.
- 종류
물리적 동시성
: 동일한 프로그램으로부터 여러 프로그램 단위를 동시에 실행논리적 동시성
: 실제로는 단일 프로세서로 프로그램을 실행하나, 다수의 프로세서가 있다고 가정해 동시에 실행- 단일 프로세서에서 인터리브(interleave) 방식을 통한다.
- 이는 데이터가 인접하지 않게 배열하는 방식이다.
- ex) Python, Ruby 같은 인터프리팅 언어는 논리적 동시성 뿐, 컴퓨터가 다중 프로세서이더라도 1개를 초과해 실행될 수 없다.
- 제어의 스레드(thread of control)
- 프로그램이 실행되면서 도달된 지점들의 순서 열이다.
- 태스크(task)
- 동일한 프로그램 딘위들과 함께 동시 실행 상태에 있을 수 있는 단위이다.
- 태스크는 때때로 프로세스(process)라고 부른다.
- Java, C# 등의 언어에서 어떤 메서드는 태스크로 동작하고, 이는 스레드 객체에서 실행된다.
- 태스크의 상태는 다음과 같다.
- 신규(new)
- 준비(ready): 여기서 실행상태로 바꾸기 위해 dispatch가 일어난다.
- 실행(running)
- 종료(terminated)
- 봉쇄(block)
- 2개 이상의 태스크가 공용 자원을 위해 경쟁하는 문제(race condition)가 발생할 수 있다.
교착 상태(deadlock)
는 태스크 A는 X, B는 Y를 소유하고 있을 때 서로의 자원을 요청하는 상황이다.- 이러한 상황들을 해결하기 위해 세마포어, 모니터, 메시지 전달과 같은 방법이 있다.
-
세마포어
- 보호(guard)는 보호된 코드가 특정 조건에서만 실행되는 것을 허용하는 장치이다.
- 세마포어는 이러한 보호의 구현이다.
- ex) javascript 어떤 프로미스를 동시에 1개만 실행할 때
let count = 1; function withSemaphore() { if (count <= 0) return; test(); } function test() { count -= 1; somePromise().finally(() => (count += 1)); }
-
모니터
- 공유 데이터 구조를 추상 데이터 타입으로 만들어 은폐한다.
- 이를 통해 동기화에 대한 책임을 런타임으로 이전한다.
- 공유 데이터가 클라이언트 단위가 아니라 모니터 내에(버퍼에) 상주해야 한다.
예외 처리와 이벤트 처리
- 하드웨어로는 탐지할 수 없지만, 컴파일러가 생성한 코드 기반으로 탐지하는 오류들이 있다.
- 배열의 첨자 범위 오류는 프로그램 실행에서 가끔 나중에 인식되는 치명적인 오류이기도 하다.
- 예외(exception)
- 하드웨어나 소프트웨어로 탐지될 수 있다.
- 특별한 조치가 필요하거나 필요하지 않을 수 있는 독특한 이벤트이다.
- 언어가 예외 처리 기능이 있으면, 예외를 전파할 수 있어 재사용할 수 있다.
- ex) api호출 시 에러가 발생하면 throw하는 핸들러
- 미리 정의된 예외는 묵시적이지만, 사용자-정의 예외는 사용자 코드에 의해 명시적으로 발생한다.
- 독특하지만 오류가 아닌 이벤트에 관련된 상황에서는 실행의 연속이 가장 좋은 선택이다.
- 딱히 프로그램에 문제를 일으키지는 않으므로.
- 이러한 설계를 실행 재개(resumption) 라고 한다.
- ex) javascript Promise.reject()
- 실행이 어떻게 종료되었든 무조건 완료하는 것이 필요할 수도 있다.
- ex) javascript Promise.finally()
- ex) javascript try catch finally
- 이벤트(event)
- 그래픽 버튼의 마우스 클릭처럼 무언가 특별한 것이 발생했다는 동작
- 엄격하게 말하면, 이벤트는 사용자 작용에 대한 반응으로 묵시적으로 생성되는 객체
- 이벤트 핸들러는 프로그램이 사용자 동작에 응답하도록 한다.
- 최근 웹은 event-drievven 형태의 렌더링 방식으로,
addEventListener()
메서드를 이용한다. - 이를 통해 이벤트 emitter로 수많은 객체를 정의하고, 이 객체들이 여러 Event Type을 정의한다.
- 최근 웹은 event-drievven 형태의 렌더링 방식으로,
추상 데이터 타입과 캡슐화 구조
- 추상 데이터 타입
- 한 개의 특별한 데이터 타입의 데이터 표현과 그 타입의 연산을 제공하는 부프로그램만을 포함하는 클로저
- 객체의 데이터 멤버는 클라이언트로부터 은폐되어야 한다.
- 그러나 멤버로 접근하는 경우가 많아, 일반적으로 접근자 메서드인
getter, setter
를 사용한다. - 고려해야할 4가지 연산이 있다.
iterator
accessor
: 직접 접근이 금지된 데이터에 대한 접근 제공constructo
: 새롭게 생성된 객체의 일부분을 초기화하는 데 사용destructor
: 사용된 힙 기억 공간(heap storage)을 다시 회수하는 데 사용
- C++에서의 클래스는 타입이며, 정적, 스택-동적, 힙-동적일 수 있다.
- 스택-동적 클래스 인스턴스의 존속기간은 그 선언의 영역 끝에 도달할 때 끝난다.
- 힙-동적 클래스는 new 연산자로 생성되고 delete 연산자로 소멸된다.
- Java에서 모든 객체들은 힙에서 할당되고 참조 변수를 통해 접근된다.
- Ruby에서 인스턴스 변수의 이름은 @(at sign) 으로 시작하는 특수한 구문이다.
- 생성자는 initialize 라는 명칭이며, 클래스당 하나만 존재한다.
- 메서드 접근 제어는 동적이어서, 런타임에서 비로소 탐지할 수 있다.
- 캡슐화 구조
- 규모가 큰 프로그램은 재컴파일 비용이 중요해진다.
- 따라서 논리적으로 관련된 코드와 데이터 집합으로 조직화하여 각 집합을 재컴파일하지 않게 해야 한다.
- 이때 각 집합이 바로 캡슐화인 것이다.
- 캡슐화는 종종 라이브러리에 사용하고, 캡슐화가 쓰여진 프로그램 외의 다른 프로그램에서도 재사용된다.
- ex) Java의 package, Ruby의 module
객체 지향 프로그래밍
- 객체 지향 언어는 추상 데이터 타입, 상속, 메서드에 대한 메서드 호출의 동적 바인딩을 지원해야 한다.
- 상속
- 객체 지향 언어에서 추상 데이터 타입은 일반적으로 class라고 불린다.
- 클래스의 객체에 연산을 정의하는 부프로그램은 method라고 불린다.
- 어떠한 클래스에서 분화된 서브클래스는 상속받은 메서드의 동작을 조작할 수 있다.
오버라이딩
의 목적은 부모 클래스에 있는 연산과 유사하지만 서브클래스 객체에 적합한 연산을 하기 위함이다.
- 동적 바인딩
- 메시지를 메서드 정의에 동적 바인딩해서 메서드 호출에 대한 다형성을 제공해야 한다.
- 아래 상황과 같을 때 draw 메서드가 다형 참조를 통해 호출된다면, 런타임에서 어느 메서드인지를 결정해야 한다.
- 이것이 동적 바인딩이며 객체 지향 프로그래밍에서 반드시 설계되어야 하는 것이다.
public class A { draw() {...} } public class B extends A { draw() {...} }
- 다중상속의 사용은 복잡한 프로그램 구조가 되기 쉽다.
- Java는 단일 상속만 지원하나, 인터페이스를 제공하여 다중 상속을 간접적으로 할 수 있다.
- Ruby에서 모든 변수는 다형이고 메서드에 대한 메서드 호출의 모든 바인딩이 동적이다.
함수형 프로그래밍
- 수학 함수에 기초하여 비명령형 스타일이 설계 기준이다.
- 명령형으로 작성된 프로그램의 기본 특성 중 하나는 런타임에서 변하는 상태를 갖는다는 것이다.
- 그 변하는 상태가 바로 변수로 표현된다.
- 하지만 함수형 언어는 변수나 상태를 갖지 않는다.
- 수학에서의 함수는 정의역을 치역으로 mapping한 것이다.
- Alonzo Church가 고안한 람다 기호는 무기명 함수를 정의하는 방법이다.
람다 표현식 (lambda expression)
: 매개변수와 함수와의 사상(mapping)이다.- ex) Haskell은 순수한 함수형 언어이다.
sub n | n < 10 = 0 | n > 100 = 2 | otherwise = 1
- 함수형에서 반복은 일반적인 순차와 반복보다는 재귀와 조건식으로 제어된다.
- 반복 구조는 변수로 제어되기 때문이다.
- 함수의 재귀 호출이 함수의 마지막 연산이라면
꼬리 재귀(tail recursive)
라고 한다. - 반복을 위해 재귀를 사용하는 많은 함수는 꼬리 재귀가 아니다.
- 아래 함수의 마지막 연산은 곱셈이므로 꼬리 재귀가 아니다.
// scheme (DEFINE (factorial n) (IF (<= n 1) 1 (* n (factorial (-n 1))) ))
- 프로그래밍 언어는 모든 실 매개변수가 완전히 평가되기를 요구한다면 strict하다고 한다.
- 함수의 값이 매개변수가 평가되는 순서에 종속하지 않음을 보증한다.
- 그렇지 않다면 nonstrict하며, 지연 평가를 할 수 있다.
- 지연 평가는 엄밀히 말하면 무한 데이터 구조를 정의하는 것을 허용한다.
지연 평가(lazy evaluation)
: 값이 필요할 때만 표현식이 평가된다.
- 과거 명령형 언어의 가장 중요한 제한은 고차함수를 지원하지 않는 것이었다.
- 그러나 람다표현식과 같은 무기명 함수는 js, python, ruby, C#의 일부가 되었다.
- 최근에는 대부분의 함수형 언어를 위한 컴파일러가 있어서 명령형 언어와의 실행 속도 차이는 크지 않다.
논리형 프로그래밍
- 논리형 프로그램은 절차적이기보다는 선언적이다.
- 선언적인 것은 결과를 도출하기 위한 절차가 아니라 원하는 결과의 명세가 서술된다는 것을 의미한다.
- 명령형과 함수형 언어는 기본적으로 절차적이다. 즉, 컴퓨터는 명령을 따르는 장치로만 취급된다.
- 논리형은 결과가 계산되는 방법을 정확히 서술하지 않고 결과의 형식을 기술한다.
명제(proposition)
: 참이거나 거짓인 논리적 문장- 기본 명제는 복합 항으로 구성된다.
- 관계를 명명하는 함수 기호인 functor와 관계의 원소들을 나타내는 순서 리스트로 구성된다.
functor
- 같은 구조에서 새로운 결과를 도출하기 위해 순회되는 구조를 의미한다.
- 즉, 아래 예시에 선언된 Wrapper 클래스는 functor라고 할 수 있다.
- ex) js Array, Class…
class Wrapper { constructor(v) { this.value = v; } map(f) { return new Wrapper(f(this.value)) } } new Wrapper(39).map(addThree);
- 논리형 프로그래밍 언어
Prolog
- 사실(fact)과 규칙(rules)들을 제공하여 데이터베이스를 만들고 질의를 던져서 프로그램을 수행한다.
- 아래는 bob은 아버지라는 사실과, X가 아버지라면 X는 남자라는 규칙이다.
- 둘의 체이닝을 통해 X를 bob으로 치환하여 명제가 도출된다.
father(bob) man(X) :- father(X).