함수형 프로그래밍에 대한 오해.
1. Functional Programming이라고 해서 미적분을 알 필요는 없다.
많은 사람들이 '함수형' 프로그래밍이라고 하면 미적분등을 왠지 머리에 떠올리는 순간 묘한 고통과 더불어 길고 긴 풀이식을 떠올리기 마련인데, 사실 함수형 언어는 그런것만은 아니다.
물론 수학적으로 고도로 추상화된 문제를 풀기에 적합하도록 설계된 언어들인것은 사실이지만, 그것이 수학이라는 분야에 특화되어 있다는 뜻은 아니다.
여기서 주목해야 할 부분은 '고도의 추상화'인데, 즉 프로그래머는 복잡한 기계적 지식이 아니라 알고리즘에 집중하게 하는 역할을 프로그래밍 언어쪽에 위임한다는 뜻이다.
일반적인 프로그래밍 언어에서 'Function'이라는 것은 값을 입력받아서 다른 한 값을 리턴하는 역할을 한다. 다만 이 과정에서 '변수'를 사용해서 함수를 계산한 다음 리턴값을 돌려주는 기존의 언어와 비교할 때 '상수'를 사용해서 연산을 하고, 그 값을 돌려줄 뿐이다. 이것을 보통 '참조투명성'이라고 일컽는다. 즉, 변수가 아닌 상수를 사용하게 되면 같은 값을 입력했을 때 같은값을 리턴하게 된다는, 어떻게 보면 지극히 당연한 원리를 실현하고 있을 뿐이다.
C언어에서 가장 흔히 쓰이는 void형은 없는게 정상이다. 값을 되돌리지 않는다면 수학적 의미에서의 함수라고 할 수가 없기 때문이다. C언어와 비슷한 파스칼에서는 이러한 void형을 procedure라고 해서 다른 것으로 취급한다. 다만 입출력에 대해서는 예외적인데, Haskell에서는 모나드라는 개념으로 이것을 극복했고, 다른 함수형 언어들에서는 입출력에 대해서만 예외를 두는방식을 선택한다.
실제로 인자를 여러개 리턴할 필요가 있는 함수에 대해서는 함수포인터같은 위험한 방법이 아니라 보통 함수형 언어들은 리스트, 혹은 튜플 등을 리턴값으로 돌리는 방법을 선택한다.
이처럼 '함수는 여러개의 인자를 받을 수 있으며, 리턴값은 반드시 하나여야 한다.' 수학적 개념에 충실한 프로그래밍 언어군을 일컫어 함수형 프로그래밍 언어라고 한다.
2. 함수형은 list중심 언어만 있는 건 아니다.
실제로 대부분의 함수형 언어는 리스트를 그 기본적인 자료구조로 하여 연산을 한다. 이것은 최초의 프로그래밍 언어였던 lisp가 원래는 '리스트 처리용'으로 시작되었기 때문이다. 그 뒤를 이어서 lisp을 간결하게 람다식으로 정리한 Scheme, 나름대로 다른 방향으로 발전되었고 종류도 가장 다양한 ML, 전위표기식이 아니라 중위표기식을 선택한 Haskell까지 모조리 리스트처리가 함수형 언어의 기본이라는 전통을 이어받고 있다.
Python이나 Ruby, 혹은 넓게 봤을때 Logo에 이르기까지, '괄호없는 Lisp'이라는 별칭을 가지고 함수형 프로그래밍을 문법에서 지원하는 언어들은, 실은 단순히 '리스트'와 '람다식' 그리고 재귀호출을 지원하는 것을 일컫어서 그렇게 부르는 것이다. 이처럼 대부분의 '함수형 언어'라고 하면 리스트 처리를 떠올린다.
하지만 이것이 전부는 아니다. 기묘한 글자를 쓰는것으로만 알려져 있지만, 실은 '모든것은 벡터다'라는 일관된 모토를 가진 APL. 그 후손격인 J Language등은, 분명 Array(배열-단 일반적으로 생각하는 배열과는 조금 다른 형식의 배열)중심적이기는 하지만, Lisp계열의 리스트로 모든 걸 처리할 수 있다! 라는 생각의 틀은 깬 언어들이다. 다만 이 언어들이 그다지 성공적이지는 못했기 때문에(유명할 지언정 사용자는 많지 않았기 때문에) 이런 방식의 언어들이 널리 쓰이지 않을 뿐이다.
3. lazy binding은 사실이지만, 실행순서가 없을수는 없다.
순수한 함수형 언어라고 하는 Haskell에서는, 모든 값은 lazy binding된다. 즉, 선언할 때 메모리 공간에 그 값을 올려두거나 하는 일 없이, 함수가 연산될 때 불려와서 메모리에 올라가게된다. 이것은 값을 바꾸면서 프로그래밍 하는 기존 언어와는 달리, 값이 고정되어 있기 때문에 가능하다. 이러한 방식은 필요한데까지만 계산한다. 즉, 필요없는 부분은 계산하지 않는다는 효율성상에서의 장점이 있다. 또한 이것은 2세대 디버깅 방식이라고 불리는 '타입추론'의 바탕이 된다.
하지만 이것은 어디까지나 값을 정의할 때의 이야기이다. 실제 프로그래밍 언어는 자료구조와 알고리즘으로 이루어져 있고, 알고리즘은 실행 순서가 있을 수 밖에 없다. 아주 기본적인 계산구조에서, 실행순서가 없다면 연산을 할 수 없게 된다.
다음의 haskell 코드를 보자
plus x y = x + y
inc x = plus 1
plus는 값 두개를 받아서 그 더한 값을 리턴하는 함수이다. inc는 값을 받아서 1을 더하는 함수이다. 여기서 inc는 plus에 의존하고 있다. 즉, plus라는 함수가 미리 정의되어 먼저 실행되지 않으면, inc라는 함수는 실행될 수가 없는 것이다. 이처럼 함수형 언어라도, 함수의 실행순서는 있다. 실제로 Haskell에서는 연산자의 우선순위를 바꿀 수 있는 명령어 또한 제공한다.
다만 위의 코드에서 정의문인 plus와 inc의 위치가 바뀌는 것은 상관없다. 즉, 실행순서가 없다는 말은 그냥 순서대로 프로그램이 진행되는 것이 아니라, 테이블표를 참조해가면서 패턴에 맞으면 적용시켜가는 과정일 뿐, 과정 자체의 순서가 없다는 것이 아니라는 뜻이다.
4. 실은 기존 언어로도 함수형 프로그래밍은 얼마든지 가능하다.
요즘들어 Haskell의 약진과, Lisp의 재도약. MS사의 F#까지 객체지향형 프로그래밍에서 서서히 프로그래밍 패러다임은 함수형으로 변화해 나아가고 있다. 하지만 이것이 정말 새로운 것일까?
Python은 C언어로 만들어졌다. 하지만 (일반적인)함수형 언어의 특징인 List와 재귀호출을 지원하는데, 이것은 다시 생각해보면 기존 언어로도 함수형 프로그래밍의 패러다임을 흉내낼 수 있다는 말이 된다. 즉 자료구조론에서 빠지지 않고 나오는 List를 자료구조로 만들고, 그것을 재귀호출을 이용해서 프로그래밍을 한다. 배정문은 const를 붙여서 상수로 만든다. 실은 이것이 함수형 프로그래밍의 전부다. (Ruby에서 lisp을 따라했다고 말하는 클로져같은 기능은 '함수형 프로그래밍'과는 별 관계가 없어보이는 lisp의 특징이기에 논외로 한다. - 물론 따지고들면 끝도 없겠지만, 함수가 값처럼 사용된다. 혹은 코드 실행 기능이 있다.. 라는것은 방법이 다를 뿐 다른언어에서도 대부분 다 지원한다, 그렇지 않다면 소위 말하는 '구조화 프로그래밍' 자체가 불가능
하다.)
우리는 절차지향형 언어나 객체지향형 언어를 사용시 반복문을 대부분 For loop문으로 만든다.
이것은 기계중심적인 패러다임으로, 확실히 C언어식 컴파일러에서는 재귀호출을 사용하는것보다 빠르고 메모리를 적게 사용한다. 하지만 모두 For Loop방식만을 가르치다 보니, 종종 사람들은 C언어에 재귀호출 기능이 있는지조차 잊는다. C언어나 Pascal등 대부분의 범용언어에는 재귀호출을 지원하고, 언어에 따라서는 list를 제공하는 경우도 많다. 이것만으로도, 함수형 프로그래밍의 패러다임을 흉내내기에는 부족함이 없다. 람다식을 사용하지 못해서 조금 복잡하게 함수를 선언할 지언정, 불가능하다고는 할 수 없는 것이다. 여담이지만 실제로 Lisp은 그당시의 범용언어였던 Fortran이 재귀호출을 지원하지 못했기 때문에 만들어졌다고 한다.
5. 실제로 대부분의 함수형 언어는 '변수'가 있다.
Haskell을 제외하고는, 실제로 대부분의 함수형 언어는 변수가 있다. Scheme을 예로 들면, Set!은 변수를 배정하는 명령이고, Define은 함수형 언어의 값에 이름을 붙이는 명령이다. 실행효율을 위해서는 변수를 사용해야 할 때도 있는 것이다.
예컨데 게임을 만든다고 생각해보자. 플레이어와 적의 Hp를 표현한다고 할때, 함수형 언어에서는 이것을 리스트로 표현하고, 그 변화를 기록해야 한다. 함수형 언어에서는 프로그램 전체가 커다란 하나의 함수가 되기 때문에 각종 재귀가 난무하는 가운데 조건문을 써서 각각의 리스트의 구조를 옮겨야 한다. 이렇게 하는것보다 그냥 Hp라는 변수를 선언해서 변화할때마다 hp=hp-x를 사용하는 편이 더 편리하다.
즉, 실제로 어떠한 '상태'를 기록해야 할 때는 상수보다 변수가 더 사용하기 편리하고 인간이 읽고 사용하기에 쉬운 경우도 있다는 것이다. 이러한 '변화하는 상태'에 촛점을 맞추고 있는것이 객체지향언어이며, 변하지 않는 값에 더 중점을 두는 것이 함수형 언어이다. 물론 객체지향에서도 값이 변하지 않는 객체-즉 함수의 역할만 하는(멤버변수가 없는)-도 있고, 위에서 봤듯 Lisp, Scheme, ML처럼 변하지 않는 값에 중점을 두고 변하는 값은 상태를 저장하는 때에만 사용되는 경우도 있다. 이것은 각자의 패러다임은 서로에게 영향을 주며 어느쪽이든 절대적인 선을 가를수는 없다는 것이다.
6. 은탄환은 없다.
과연 재귀호출과 리스트 처리가 인간중심적인 걸까? 프로그래밍 소스를 처음보는 사람에게 재귀호출의 코드를 보여주고 설명해주면 단번에 이해할 수 있게 되는걸까? 나는 동의하지 않는다.
분명 for문을 위시한 반복문보다 조금 더 인간이 생각하는 사고방식에 가깝기는 하지만, 결국 결과를 예측하고 나서 코드를 짜야한다는 커다란 틀은 변하지 않는다. 나는 지금도 재귀호출보다 일반인들이 더 쉽게 이해할 수 있는 반복문의 형태가 있을 것이며, 단지 널리 사용되지않을 뿐이라고 생각한다.
이번엔 리스트에 대해서 생각해보자. 리스트라는 것이 수학에서 그렇게까지 많이 사용되는 것인가? 아니면 수학이 프로그래밍에 더 많이 사용되게 되면서 채용된 편의적 방법에 불과한가? 혹은 전자가 맞다고 해도, 일반적인 함수형 언어가 쓰는 리스트를 '쪼개서 계산하고 합친다'라는 복잡하고 개념적인 방식 외에 다른 방법은 없는것인가?
함수형 프로그래밍 언어의 진정한 강점은, 고도의 추상화가 가능하다는 것이다. 객체지향의 효시인 Smalltalk가 처음 나왔을때 가장 강력한 무기는 '모든것은 객체다'라는 단순함과, 거의 Syntax가 없다는 점이었다. 즉, 영어를 읽을 수 있다면 누구나 사용할 수 있는 언어를 앨런 케이는 꿈꿨고, 훌륭한 방법론을 제시함으로써 oop자체의 발전과는 관계없이 프로그래밍 언어가 어디까지 추상화될 수 있는가에 대해서 좋은 본보기가 되어주었던 것이다. 함수형 프로그래밍 언어는 기본적으로 '수학적인 이론과 깔끔함을 본받자'라는 자신의 가장 기본적인 틀 아래 고도의 추상화를 발전시켜오고 있고, 앞으로도 발전할 것이다. 내부적 구현이 함수형이든 객체지향이든, 누구나 쉽게 읽고 사용할 수 있는 방향으로 프로그래밍 언어는 나아가야 한다.
다만 유의해야 할 것은 어떠한 놀라운 패러다임이 개발되더라도, 그것이 결코 은탄환이 되어주지는 못한다는 점이다. 즉, 프로그래밍 패러다임이 조금 더 인간의 실수를 덜어주거나 편리한 사용을 도와줄 수는 있을 지언정, 그것이 모든것에 대한 완전한 해결책이라는 결과를 가져다 주지는 않는다는 것이다. 프로그래밍은 본질적으로 어떤 목적을 향해 나아가는 창작활동이고, 패러다임은 그 도구일 뿐 본질이 아니라는 것을 명심했으면 한다.
06.11.09. by RL.T
없는 관계로 부득이하게 영어 위키로 대신합니다.

