튜링상 관련 업적

프로그래밍 언어와 수학적 표기법에서 그의 선구적 노력은 APL이라는 컴퓨팅 분야를 낳았다. 대화형 시스템 구현, APL의 교육적 활용, 프로그래밍 언어 이론과 실천 등에 기여한 점도 주목하자.

1979년 튜링상 선정 이유​1​

우리가 서로 소통하기 위해 사용하는 언어는 말하는 사람의 의사를 제대로 전달해야 한다. 그 목적을 달성하기 위해 언어가 어떤 형태를 가져야 하는지에 대한 정답은 없다. 그래서 지구상에는 다양한 언어가 존재한다.

한국어와 중국어는 형태와 사용 방법이 판이하게 다르지만 어찌 되었든 의사소통이라는 목적을 훌륭히 달성한다. 아랍어는 심지어 오른쪽에서 왼쪽으로 써 내려가므로 한국어 사용자 입장에서는 황당할 수도 있지만 역시나 중동 지역에서는 의사소통의 도구로 잘 사용되고 있다.

프로그래밍 언어의 목적은, 어떤 문제를 해결하기 위한 알고리듬을 컴퓨터가 실행할 수 있도록 기술하는 데 있다. 프로그래밍 언어로 작성된 프로그램 소스 코드는 컴파일러를 통해 기계어로 변환되거나 아니면 인터프리터를 통해 해석되어 컴퓨터에서 실행된다. 이 목적을 달성하기 위한 프로그래밍 언어의 형태가 한 가지만 있으라는 법은 없다. 그래서 우리 주변에는 다양한 문법과 사용법을 가진 프로그래밍 언어들이 존재한다.


1940년대 말에 전자식 컴퓨터가 처음 등장하고 1950년대 초에는 저장형 프로그래밍 방식의 전자식 컴퓨터가 속속 모습을 드러내면서 프로그래밍 언어에 대한 고민도 함께 이루어졌다. 아직 ‘전형적’인 프로그래밍 언어라는 것이 존재하지 않던 시절이므로 백가쟁명식으로 다양한 시도가 이루어졌다. 그러다가 포트란이라는 히트작이 나왔고 알골이라는 표준이 등장하면서 ‘전형적’인 프로그래밍 언어의 모습이 만들어졌다.

흔히 절차형 프로그래밍 언어라고 부르는 이 전형적인 프로그래밍 언어는, 변수와 함수를 사용하고 조건문과 반복문을 지원하며 스코프scope 규칙을 가지는 복합문 구조를 포함한다. 절차형 프로그래밍 언어가 대세가 된 이유가 한 가지만 있지는 않겠지만, 그중 하나는 기계어 프로그래밍의 패러다임과 크게 다르지 않다는 점을 들 수 있겠다. 저장형 프로그래밍 방식의 전자식 컴퓨터는 메모리에 저장된 데이터를 조작한 후에 다시 메모리에 저장하는데 이는 변수 사용과 대응된다. 그리고 어떤 논리적 동작을 한 후에는 그 결과값에 따라 분기jump 여부를 결정할 때가 많은데 이는 조건문에 대응된다. 따라서 기계어 프로그래밍에 익숙했던 이들이 절차형 프로그래밍 언어로 갈아타기는 쉬웠을 것이다.

하지만 1950년대 말에 전혀 다른 관점에서 접근하여 개발된 프로그래밍 언어들도 있었다. 그 대표적인 사례가 리스프Lisp와 APL이다. 리스프는 리스트list 방식의 자료구조에 초점을 맞춰 출발한 언어이다. 그리고 APL은 알고리듬의 수학적 표기에 초점을 맞춰 출발한 언어이다. 오늘날 대세인 자바Java나 파이선Python 등의 언어에 익숙한 사람들에게는 리스프와 APL의 사용 방식이 매우 낯설게 느껴질지도 모르겠다. 마치 한국어에 익숙한 사람에게 아랍어가 이상하게 느껴지는 것처럼 말이다. 하지만 사용 방식이 상이할 뿐이지 결국 문제 해결을 위한 알고리듬을 컴퓨터가 실행할 수 있도록 만들어주는 도구라는 점에서는 차이가 없다.

APL 맛보기

1부터 10까지의 정수에 대한 평균값을 구하는 프로그램을 작성해야 한다고 가정해보자. 너무 쉬운 문제여서 그냥 암산으로 풀 수도 있지만 여기서는 컴퓨터 프로그램으로 해결해야 한다. 문제가 주어지면 일단 컴퓨터의 전원을 켜고 바로 코딩을 시작하는 이는 드물다. 그리고 바람직하지도 않다. 차분히 차 한 잔을 마시면서 우리는 답을 구하기 위한 절차를 생각해본다. 주어진 조건에서 출발해서 답을 구해나가는 절차가 바로 알고리듬이다. 앞에 언급된 문제에 대한 알고리듬은 아래와 같이 정리할 수 있다.

1) 1부터 10까지의 정수를 모두 더할 것.
2) 앞에서 구한 값을 10으로 나눌 것.

간단한 문제이므로 알고리듬도 간단하다. 그런데 위의 알고리듬을 절차적 프로그래밍 언어로 구현하게 되면 다음과 같이 변환할 수 있다.

sum := 0;
for (i:=1; i<=10; i++)
{
    sum := sum + i;
}
result := sum / 10;

절차적 프로그래밍 언어를 많이 다루어 본 사람이라면 위의 알고리듬에서 이 프로그래밍 코드로 변환하는 일이 그리 어렵지 않을 것이다. 어찌 보면 너무 당연하다고 느낄 수 있다.

하지만 냉정히 생각해보면 위의 알고리듬과 프로그래밍 코드는 직관적으로 연결시키기 어렵다. 특히 알고리듬의 1)번 문장이 프로그래밍 코드로 바뀌는 과정은 상당히 비직관적이다. 우리가 이를 당연하게 여기는 것은 이런 패턴을 이미 학습했기 때문이다.

그렇다면 APL로 작성하게 되면 어떻게 될까? 그 결과는 아래와 같다.

(+/⍳10)÷10

이것이 프로그래밍 코드가 아니라 수학 수식처럼 보인다면 그것은 이상한 것이 아니라 자연스러운 것이다. 케네스 아이버슨이 APL을 처음 시작할 때 마음에 두고 있던 것이 수학적 표기법이었기 때문이다. 그는 전통적인 수학적 표기법으로는 알고리듬을 잘 기술하기 어려움을 깨달았다. 그래서 알고리듬을 기술하기 위한 자신만의 수학적 표기법을 만들어낸 것이다.

APL로 작성된 프로그램 코드는 생소하기 때문에 처음에는 이해가 잘 되지 않지만, 몇 가지 규칙을 알고 나면 오히려 이해하기가 쉽다.

APL 이해하기

배열

APL은 본질적으로 배열array을 다루는 언어이다. APL의 시작이 하버드 대학교의 대학원 강의에서 시작되었음은 앞에서 설명한 바가 있다. 당시에 컴퓨터는 선형대수를 계산하는 데 사용되었고 선형대수에서는 행렬 계산이 중요했다. 행렬은 컴퓨터에서 배열로 표현되므로 아이버슨은 알고리듬에서 다루는 데이터의 기본형을 배열로 생각했다.

그래서 APL에서는 특별히 배열을 다르게 표현하지 않는다. 숫자가 나열되어 있으면 그것은 배열이며 한 덩어리로 생각해야 한다.

함수

함수function는 인자로 주어지는 값을 약속된 규칙에 따라 어떤 다른 값으로 사상mapping시키는 일을 한다. APL에서 함수의 인자는 함수 기호의 오른쪽(인자가 1개일 때)에 오거나 아니면 좌우 양쪽(인자가 2개일 때)에 온다. APL에서 사용되는 기본 함수들 중 일부는 다음과 같다.

+, -, ×, ÷ // 산술 연산. 배열이 인자로 오면 배열의 원소들 모두에게 적용됨.
~, ∧, ∨    // 논리 연산(NOT, AND, OR)
⌈, ⌊       // 배열 중 최대값, 최소값
*          // 지수 계산
⍳          // 1부터 시작하는 배열 생성
⍴          // 배열 원소의 개수

APL에서 함수의 인자는 기본적으로 벡터값을 가정한다. 함수를 사용했을 때의 예는 아래와 같다. (이탤릭체가 결과값이다.)

      2 + 2
4
      2 + 1 2
3 4
      1 2 + 1 2
2 4 
      ⍴ 4
1
      ⍴ 1 2 4
3
      ⍳ 10
1 2 3 4 5 6 7 8 9 10

연산자

연산자operator는 인자로 주어지는 값 혹은 함수를 약속된 규칙에 따라 어떤 다른 함수로 사상mapping시키는 일을 한다. 연산자의 결과물은 어떤 값이 아니라 새로운 함수라는 점에 유의하자. APL에서 연산자의 인자는 연산자 기호의 왼쪽(인자가 1개일 때)에 오거나 아니면 좌우 양쪽(인자가 2개일 때)에 온다. APL에서 사용되는 기본 연산자들 중 일부는 다음과 같다.

/          // Reduce operator
\          // Scan operator
.          // Product operator

연산자는 단독으로 사용되는 것이 아니라 다른 함수와 결합되어 새로운 의미의 함수를 만들어 낸다. 예를 들면 아래와 같다.

      +/ 1 2 3    // + 함수를 배열의 원소 1, 2, 3에 한꺼번에 적용. 즉, 1+2+3
6      
      +\ 1 2 3    // + 함수를 배열의 처음부터 적용하고 그 결과를 다음 계산에 사용
1 3 6    
      1 2 3 +.x 4 5 6   // 배열의 inner product
32

오른쪽에서 왼쪽으로 해석하라

일반적으로 프로그래밍 언어의 문장은 왼쪽에서 오른쪽으로 읽어가면서 해석한다. 이는 일반적인 수학적 표기법도 그러하다. 하지만 APL은 오른쪽에서부터 왼쪽으로 해석해나가야 한다. 단, 괄호로 묶여 있는 것은 한 덩어리로 취급한다. 앞에서 사용했던 예를 따져보자.

(+/⍳10)÷10

위와 같은 APL 코드 문장이 주어지면 오른쪽부터 따져 나간다. 가장 오른쪽에 있는 함수는 나눗셈이다. 오른쪽에 100이라는 값이 정해져 있으므로 이제 왼쪽 인자값을 알아야 한다. 그런데 왼쪽 인자값이 괄호로 묶여 있다. 따라서 괄호 안의 것을 통째로 먼저 계산해야 한다.

+/⍳10

가장 오른쪽에 있는 함수는 ⍳이다. ⍳10의 결과는 1부터 10까지의 배열이 된다. 따라서 위의 수식은 아래와 같이 바꿀 수 있다.

+/ 1 2 3 4 5 6 7 8 9 10

+/ 를 적용하면 1부터 10까지 더한 합계가 결과로 나온다. 따라서 이것을 10으로 나누면 평균인 5.5를 얻게 된다.

변수와 사용자 함수

APL에서도 변수를 사용할 수 있으며 이 변수는 사용자가 대화형interactive 환경을 종료할 때까지 유지된다. APL은 인터프리터로 작동하며 각 사용자의 대화형 환경마다 별도의 주소공간을 할당하여 사용한다. 이 주소공간은 워크스페이스workspace라고 부르며 여기에 변수들이 저장된다. 따라서, 서로 다른 사용자끼리는 데이터를 공유할 수 없다. 하지만 후에 공유변수shared variable 기능이 추가되면서 별개의 워크스페이스끼리도 데이터를 공유할 방법이 생겼다.

변수를 사용한 예가 다음과 같다.

      (+/A)÷⍴A ← 1 2 3 4 5 6 7 8 9 10
5.5

위의 코드를 해석하려면 역시 오른쪽에서부터 출발한다. 가장 오른쪽에 있는 함수가 ← 이다. 이것은 오른쪽에 있는 값을 왼쪽 변수에 대입하는 일을 한다. 따라서 이 함수가 수행되면 변수 A에는 1부터 10까지의 배열이 저장된다. 이제 왼쪽으로 전진하면 ⍴ 함수가 있다. 이것은 배열의 개수를 구하는 함수이다. 그 결과는 10이다. 이제 다시 왼쪽으로 이동하면 나눗셈 함수가 나오고 그 왼쪽에 있는 인자값을 알아내야 한다. 괄호 안에 있는 A라는 변수는 1부터 10까지의 배열이므로 거기에 +/를 적용하면 55가 나오고 결국 55÷10 = 5.5가 구해진다.

사용자 정의 함수도 변수와 유사한 형태이지만 차이가 있다면 중괄호로 묶어줘야 한다는 것이다. 위에서 사용한 평균계산 수식을 Average라는 이름의 함수로 만들어 보자.

      Average ← { (+/X)÷⍴X }
      Average 10
5.5

대화형 인터프리터

인터프리터interpreset는 컴퓨터 프로그램 소스 코드를 기계어로 변환하는 것이 아니라, 그 코드를 한 줄씩 시뮬레이션하듯이 처리하는 별도의 프로그램이다. 절차형 프로그래밍 언어는 기계어에서 출발했기 때문에 기계어로 변환하기가 용이하지만 리스프나 APL 같은 언어는 상당히 어렵다. 따라서 인터프리터 방식이 일찍부터 선호되었다.

APL 인터프리터는 1965년에 로렌스 브리드Lawrence Breed에 의해 구현되었는데 포트란 언어로 작성되었다. 이 첫 인터프리터는 일괄처리batch 방식이었다. 즉, APL로 작성한 프로그램 소스 코드 전체를 한 번에 실행하고 종료한다. 그런데 1966년에 IBM 내부에서 시분할 시스템이 실험적으로 개발되었고 APL 팀은 이를 적극 받아들였다. 그래서 대화형 방식의 인터프리터가 등장했다. 대화형 방식에서는 프로그램 코드를 한 줄 입력하면 그 결과를 바로 보여주는 식으로 동작한다.​7​

APL의 대화형 인터프리터는 상당히 효율적으로 만들어져서 컴퓨팅 속도가 느린 컴퓨터에서도 잘 동작했다. 그래서 교육용으로도 많이 사용되었다.

1 2 3 4