좋은 프로그래밍 언어는 세 가지 표현 방식을 갖고 있다.
기본 식: 언어에서 가장 단순한 것을 의미
엮어내는 수단: 간단한 것을 모아 복잡한 것을 만든다.
요약하는 수단: 복잡한 것에 이름을 붙여 하나로 다룰 수 있게끔 간추린다.
1.1.1 식(Lisp 의 방언 Scheme 언어로 구성)
486 -> 486 : 가장 간결한 식으로 프로시저가 적용 되지 않은 수를 나타낸 식
(+ 137 349) -> 486
기호를 사용하여 여러 식을 엮어 만든 복잡한 식
이와 같이 괄호로 묶어 리스트를 만들고 프로시저 적용을 뜻하도록 엮어 놓은 식을 엮은식(combination) 이라 한다.
리스트에서 맨 왼쪽에 있는 식은 연산자, 나머지 식은 피연산자가 된다.
엮은식을 계산한 값은 인자(피연산자)에 프로시저(연산자)를 적용하여 얻는다.
연산자를 피연산자 왼쪽에 두는 방식을 앞가지 쓰기(prefix notation)이라고 하는데 여러 장점이 있다.
장점1. (+ 21 35 12 7) -> 75 와 같이 인자가 많아져도 따로 문법을 만들 필요가 없다.
장점2. (+ (* 3 5) (- 10 6)) -> 19 와 같이 식 속에 다시 식을 넣어 여러 겹으로 엮어 늘리기가 편하다.
복잡한 식을 표현할 때, 한 줄에 나열하는 것이 아니라 인자를 중심으로 줄을 맞추고 알맞게 들여쓰는 방식을 가지런히 쓰기(pretty-printing)라고 한다.
ex)(+ (* 2 4)
(+ 3 5))
1.1.2 이름과 환경
프로그래밍 언어에서 가장 중요한 기능 가운데 하나는 계산 물체에 이름을 붙이는 수단이다. 이때 이름은 변수가 되고, 그 변수의 값은 계산 물체가 된다.
ex) (define pi 3.141592)
어떤 값에 이름을 붙여 두었다가 뒤에 그 이름으로 필요한 값을 찾을 수 있다는 것은 이름-물체 쌍을 저장해 두는 메모리가 있다는 뜻이다. 이런 기억 공간을 환경이라고 한다.
1.1.3 엮은식을 계산하는 방법
Lisp 에서 다음과 같이 처리한다고 하자.
1. 엮은식에서 부분식을 먼저 셈한다.
2. 엮은식의 왼쪽에 있는 식은 프로시저가 되고, 나머지 식은 인자가 되어 프로시저를 인자에 적용해 엮은식의 값을 구한다.
위와 같이 복잡한 엮은식을 프로세스로 재귀를 통해 계산해나갈 수 있는데, 이를 통해 복잡한 프로세스를 쉽게 트리 형태로 나타낼 수 있다.
엮은식은 다른 엮은식의 인자가 되어야 하므로 (defind x 3) 은 엮은식이 아니다. 이렇게 계산 규칙이 따로 밝혀져 있어야 하는 문법을 특별한 형태(special form)라 한다.
1.1.4 묶음 프로시저
프로시저란 복잡한 연산에 이름을 붙여서 쓰는 방법으로, 큰 프로그램을 짤 때에 쓰기 좋다.
'제곱' 을 프로시저로 나타내보자.
(define (square x) (* x x))
위와 같이 프로시저를 정의하면 square 라는 프로시저를 사용할 수 있고, 이는 (* x x) 라는 다른 프로시저를 하나로 묶어 만들어낸 묶음 프로시저이다.
묶음 프로시저는 기본 프로시저와 구별할 수 없다. 기본 실행기가 내재한 것인지, 직접 정의한 것인지 알 수 없다는 의미이다.
(* (sum-of-squares 4 3) (square 3))
1.1.5 맞바꿈 계산법으로 프로시저를 실행하는 방법
위에서 정의한 프로시저를 살펴보자.
(square 3) 이라는 것은 (* x x) 에 x 대신 3을 대입하여 연산하라는 의미이다. 이와 같은 계산법을 맞바꿈 계산법(substitution model)이라 한다.
참고. 실행기가 맞바꿈 계산법을 사용한다는 것은 아니다. 이후에 살펴볼 최적화된 방식을 사용하며, 이는 가장 단순한 계산법이다.
정의대로 계산법(normal-order evaluation)과 인자 값 먼저 계산법(applicative-order evaluation)
아래 식을 살펴보자.
(square (square 2))
정의대로 계산법은 피연산자를 계산하지 않고, 쭉 늘어트린 뒤 한 번에 계산하는 방식으로 아래와 같이 계산한다.
(square (* 2 2)) -> (* (* 2 2) (* 2 2))
인자 값 먼저 계산법은 아래와 같이 계산한다.
(square (* 2 2)) -> (square 4) -> (* 4 4)
정의대로 계산법은 2*2 연산을 두 번, 인자 값 먼저 계산법은 한 번에 처리하는 것을 확인할 수 있다.
Lisp 는 1.여러번 되풀이하여 계산하는 일이 없고 2.맞바꿈 계산법이 통하지 않는 프로시저는 정의대로 계산법을 적용하기 어렵기 때문에 인자 값 계산법을 사용한다.
정의대로 계산법도 매우 쓸모 있는 방식이므로 이후에 해당 사례를 찾아보겠다.
1.1.6 조건 식과 술어
Lisp 에서 조건식은 cond 라는 문법을 사용하여 작성한다.
(define (abs x)
(cond ((> x 0) x)
((= x 0) 0)
((< x 0) (- x))))
cond 라는 이름 뒤에는 두 식을 괄호로 묶어 놓은 절(clause)이 여러 개 온다. 절을 이루는 두 식 가운데 첫 식을 술어(predicate)라 하는데, 답은 항상 참 또는 거짓이다.
술어가 참이면 뒤의 결과 식을, 거짓이라면 다음 절의 술어를 확인하며 동작한다.
cond의 마지막 절에 else 를 넣어 술어가 참인 식이 하나도 없을 때 결과 값을 정의할 수 있다.
특별한 형태인 (if <predicate> <consequent> <alternative>) 를 사용하여 조건식을 정의할 수도 있다.
predicate 자리에는 기본 연산자 <, =, > 뿐만 아니라, and, or, not도 지원한다.
1.1.7 연습: 뉴튼 법으로 제곱근 찾기
수학 함수와 컴퓨터 프로시저는 매우 비슷하다. 그러나 아주 큰 차이점이 있는데, 함수와 달리 프로시저는 반드시 효율성을 갖추어야 한다는 것과, 함수는 무엇이 어떤 성질을 지니는지 밝히는 일을 하고, 프로시저는 그 무엇을 어떻게 만들지, 구할지 나타내는 일이라고 할 수 있다.
예시로 제곱근 구하는 문제를 살펴보자.
수학 함수: y >= 0 이고, y^2 = x 면 y = sqrt(x) 다.
컴퓨터 프로시저: 뉴튼 법 (x 의 제곱근에 가까운 값 y가 있을 때, x와 x/y의 평균을 구하여 진짜 제곱근과 가까운 값을 찾는 방법) 을 재귀적으로 호출하여 특정 값 이내로 들어오면 계산을 종료하여 제곱근을 얻을 수 있다.
1.1.8 블랙박스처럼 간추린 프로시저
1.1.4 에서 만든 square 프로시저는 속을 들여다보지 못하는 '블랙박스'와 같다. 제곱을 반환하는 프로시저라는 것은 알 수 있으나, 어떻게 값을 구하는 것인지는 define 절을 보기 전까지는 알 길이 없다.
square 프로시저를 사용하는 쪽에서 보면 어떻게 계산하는지는 알 필요가 없다. square 는 프로시저라기보다 프로시저를 간추려 묶어놓은 이름일 뿐이다. 이것을 프로시저 요약하기(procedure abstraction)라고 한다.
큰 프로그램을 여러 프로시저로 나누어 짤 때는 어떻게 계산되는지 드러나지 않도록 하는 것이 중요하다.
갇힌 이름(local name)
Lisp 의 프로시저는 블랙박스처럼 간추리기 위해 인자 이름을 프로시저에 바인딩한다. 프로시저 내부에서 인자를 어떻게 사용하든 외부와는 단절되어 있다는 것이다. 이와 같이 바인딩 된 변수를 매인 변수(bound variable) 라고 한다. 또, 매인 변수를 쓸 수 있는 식을 한데 일컬어 유효 범위(scope) 라고 한다.
그에 반해 프로시저에 매이지 않은 변수는 자유 변수(free variable) 라고 한다.
안쪽 정의와 블록 구조
위 뉴튼 법으로 제곱근을 구하는 과정에서 good-enough? 와 improve 라는 프로시저가 쓰였는데, 큰 프로그램에서 해당 이름들을 단 한 번만 사용해야할까?
(define (sqrt x) (define ...)) 와 같이 외부에서 사용할 프로시저만 노출하고 내부에 프로시저를 정의와 동시에 사용하여 프로시저 정의를 겹쳐 쓰는 모양을 블록 구조라고 하는데, 위 방법으로 name-packaging 문제를 해결할 수 있다.
또, 위와 같은 방법을 사용하게 되면 따로 따로 프로시저를 정의할 때는 해당 변수들이 프로시저에 바인딩되는데, 블록 구조를 사용하면 상위 프로시저에 매인 변수를 내부적으로 자유 변수로 사용할 수 있게 된다. 이를 두고, 문법에 따라 변수가 보이는 넓이가 정해지는 렉시컬 스코프(lexical scope) 규칙을 따른다고 한다.