1. 더 높은 프로그래밍의 세계 Haskell로 가자

 
김재우 동명 정보대학교 정보기술원 교육 시스템 연구개발 팀장 kizoo@bluette.com
블루엣 인터내셔널의 기술 이사로 재직중이며 개발 환경과 온라인 교육 시스템을 결합한 소프트웨어를 설계하고 있다. 소프트웨어 공학 기술이나 관련 이론을 실천하도록 만드는 것이 개발자로서의목표. 현재 정보기술원과 함께 분야별 표준 교육 과정, 전문 개발자 양성 및 인증을 위한 교육 시스템‘Theory Into Practice’를 설계하고 있으며, 인도 Vinayaka 대학 IT Parks 소프트웨어 개발팀과 함께 기업형 솔루션 교육과정 및 소프트웨어 개발 기술을 연구하고 있다.
 
이번 연재에서 소개할 프로그래밍 언어 Haskell은 한마디로 우아한 언어다. 작고 가벼우면서도 표현력을 끝없이 확장할 수 있어 프로그래밍이 진정 무엇인지를 제대로 보여주는 언어라 할 수 있다. 그저 기능이 적은 단순한 언어와 의미의 바탕이 충실해 알맹 이가 작은 언어의 차이를 느끼고 싶다면 Haskell을 꼭 다뤄봐야 한다. 특히 처음 프로그래밍을 공부하는 사람에게 의심없이 배우 라고 권할 수 있는 언어다. 다른 종류의 프로그래밍 언어를 오랫동안 사용한 사람이라면 상업적인 언어와 많이 다르기 때문에 조 금 혼란스럽고 황당할 수도 있을 것이다. 그러나 그런 변화는 아주 바람직한 것이므로 아무쪼록 이 글을 통해 Haskell 같은 아름다 운언어를즐길수있게되기바란다.

 
지난호 특집‘프로그래밍의 숨겨진 진실과 거짓’을 쓴 뒤, 다른 어떤 글을 썼을 때보다 많은 메일을 받았다. 주장이 강한 글이다 보니 자바 기술을 폄하하는 것으로 받아들이는 사람도 있었고, C#이나 C++를 은근 슬쩍 칭찬하는 것이 아니냐고 오해하는 이도 있었다. 또, 자바에 포괄 프로그래밍(Generic Programming) 기법을 도입하면 그나마 순수하고 단순했던 언어를 C++처럼 복잡하고 어렵게 만드는 것이 아니냐고 걱정하는 사람도 있었다.
먼저 필자의 의도를 오해한 독자에게 답한다면(물론 그 글 앞머리에서 이미 밝혔지만) 자바를 ‘빌려’ 객체지향성에 대한 맹신과 프로그래밍 언어에 대한 지나친 집착을 우려했을 뿐이다. 또한 우리 프로그래밍 교육이 실무를 익힌다는 미명 아래 프로그래밍의 기본을 무시하고, 지나치다 싶을 정도로 유행과 상품성에 휩쓸리는 점을 꼬집고 싶었다. 그리고 현재 그런 분위기를 몰고 가는 기술이 꼭 자바가 아니더라도 필자는 똑같은 방법으로 강하게 비판했을 것이다. 예로 든 기술이 자바였을 뿐이다.
나머지, 기술이나 철학에 관한 질문에는 이번 연재로 답이 되리라 여겨진다. 이번 연재 역시 방법이 다를 뿐 그 목적은 같다. 처음에는 단순히 Haskell이란 언어를 입문 수준에서 소개하는 정도로 그치고자 했지만, 글을 읽는 독자 중에는 기술 자체 보다 그 기술이 나타난 이유를 알고 싶어하는 사람들이 더 많을 것이다. 아무래도 기술이나 기법에 대한 자료는 쉽게 구할 수 있는 반면, 바탕이 되는 원리를 중심으로 프로그래밍이 무엇인지 다시금 생각하게 만드는 글은 드물다. 그래서 이번 호에서는 프로그래밍의 세계에서 묵직하게 흘러가는 기술의 흐름을 얘기하고 난 뒤, Haskell을 간단히 소개하고자 한다.
 

프로그래밍 패러다임에 대해

프로그래밍의 세계에서 패러다임이란 말이 유행처럼 번지게 된 것은 아무래도 객체지향 프로그래밍 때문이다. 그리고 객체지향을 닫힌 세계에서 끌어내어 세상의 도마 위에 올린 것은 누가 뭐래도 C++의 공헌이다. C++가 C의 인기를 업고 프로그래밍 세계를 한바탕 뒤집어 놓으면서‘패러다임의 이동’의 시대를 이끌었을 때, 그 혼란의 강도는 지금의 자바에 비할 바가 아니었다. 떠들지는 않았지만 묵직했기 때문에 프로그래머는 물론이고 관련 기술자 대부분이 어쩔 수 없이 ‘생각하는 관점’을 바꿔야 한다는 생각에 휩싸여 있었다. 그 영향으로 지금까지 정말 많은 종류의 객체지향 언어와 객체지향 소프트웨어가 쏟아져 나왔다. 객체지향이란 꼬리표가 붙어 있지 않으면 좋은 제품과 기술이 아니었던 적도 있었다.
지난 시절이 어떠했든, 그리고 얼마나 많은 프로그래머들이 고생을 했던 간에 이런 굵직한 변화는 박수를 받아 마 땅한 일이다. 새로운 개념이 유행하지 않으면 산업은 좀처럼 관행을 바꾸려하지 않는다. 그러나 객체지향이 당당한 소프트웨어 개발 기법으로 자리잡고 과학과 산업 전반에 커다란 영향을 주고 있는 이 마당에, 한 번쯤은 뭔가 빠뜨리고 지나가지 않았나 되짚어봐야 할 것이다. 필자는 많은 프로그래머와 입문자들이 낡은 관점을 그대로 답습하고 있어서 걱정이다. 객체지향에 대해 이론과 실용 양쪽의 생각을 냉정히 정리하면 따로 책 한권을 쓸 수 있을 만큼 긴 얘기가 나오겠지만, 여기서는 객체지향 대란의 시기를 거치면서 한 가지 잘못된 점만 짚고 넘어가기로 하겠다.
필자가 보기에 가장 심각한 병은 패러다임과 그 변화를 받아들이는 사람들의 자세다. 객체지향이 뜨는 바람에 이전보다 이런 개념의 유입과 변화에 대해 보다 부드러워진 것도 사실이지만, 현실에서 기술로‘먹고사는’사람들은 이런 가벼운 변화를 즐겁게 받아들일 수만은 없다. 그러므로 새로운 개념을 소개할 때는 득실을 분명히 따져보고, 장점과 한계를 명확히 밝혀 이론이나 실용 양쪽의 균형을 맞춰 받아들이도록 배려해야 한다. 어떤 개념이 잘못 전달되는 경우 일부는 분명 받아들이는 자세에 문제가 있어 그렇겠지만, 그 개념을 가르치고 전파하는 방법에도 어느 정도 책임이 따른다. 어떤 개념을 너무 강조하다 보면 배우는 이들은 이를 나머지 세계와 동떨어진 어떤 것으로 받아들이게 되 고, 심하게는 그 세계에 갇혀서 다른 개념을 완강하게 거부하게 될 뿐만 아니라 자기 것을 제외한 다른 방식은 가치 없고 복잡하며 어려운 것으로 간주한다. 특히 입문자에게서 이런‘패러다임 중독성’을 쉽게 찾아볼 수 있다.
객체지향성의 경우를 예로 들어 설명해 보자. 객체지향성을 설명하는 사람들이 클래스니 메시지니 메쏘드니 하는 이질적인 용어를 써서 이미 있던 프로그래밍 개념과 완전히 다른 어떤 것으로 오해를 유도한 덕분에(물론 고의로 그런 것은 아니겠지만), 다른 언어를 공부하다가 새로 이 개념을 익히는 이들은 패러다임 충격으로 한 동안 고생하게 된다. 은유(METAPHORE)를 써서 한 패러다임을 체계 있게 설명하는 시도야 나쁠 것이 없지만, 그 은유를 지나치게 강조하면 문제가 발생한다. 은유는 단지 무언가를 설명하는 수단일 뿐인데, 너무 무게를 실어 설명하다보면 주객이 전도 돼 준수해야 할 규칙과 원칙으로 둔갑한다. 한 패러다임에 입문한 사람은 대개 그 맛을 느끼면 무슨 철학의 한 유파처럼 행동하는 경향이 있어 실제 구현 기법을 제대로 이해하고 연습하는 데 오히려 방해되는 경우가 더 많다. 은 유는 처음 배우는 이들에게 부드럽게 개념을 이해하도록 도와주는 역할을 하는 것이고, 그 단계가 지나면 그 은유를 깨뜨리고 본질을 바라볼 수 있도록 도와주는 것에서 끝나야 한다고 생각한다. 예를 들어 객체지향 개념에 대한 이해가 깊어지면 클래스∙메쏘드∙메시지는 그저 설명하기 좋은 용어일 뿐이고, 그 껍질을 벗겨내면 모듈∙함수∙형(Type) 등, 다른 언어에서 잘 써왔던 용어와 개념상으로 별반 차이가 없다는 걸 알게 해줘야 한다. 소프트웨어를 설계할 때는 또 다시 그 은유의 힘을 잘 써먹어야 하지만, 본질을 알고 있다면 은유와 실제의 차이에서 개념을 정립하지 못하고 방황하거나 잘못된 길로 들어서지는 않을 것이다. 객체지향의 클래스란 용어 하나가 잘못되어서 생긴 피해만 따져 보더라도 결코 무시할 일이 아닌 것이다.
어쨌든 패러다임은 그렇게 뜬구름 잡는 어떤 것이 아니다. 어떻게 문제를 잘라서 추상화하고, 전체 프로그램의 구조를 결정하는지 판단 기준을 잡아주는 개념일 뿐이다. 그런데 많은 책이나 글에서 패러다임을 설명하는 걸 보면, 마치 딱딱한 철학 입문서를 보는 것 같다. 객체지향성을 멋지게 쓰는 게 어렵기는 하지만 신념을 갖고 순수성을 지켜내야 할 가치관이나 철학 같은 것이 아니기 때문에 어디에 중심을 두고 이해해야할 지를 잘 판단해야 한다. 단어의 본 뜻이 어떠하든 프로그래밍의 세계에서 패러다임은 그리 무겁게 받아들일 대상도 아니고, 닫힌 마음으로 지켜내야 할 원칙도 아니다. 다시 말하지만 패러다임은 그저 한 가지 표현 기법일 뿐이다. 그저 어떤 유형의 문제를 더 잘 푸는 데 도움되는 표현 방법이다. 그래서 한 가지 패러다임만으로 작업한다는 얘기는 되려 표현력에 결점이 있다는 것을 스스로 인정하는 것과 다를 바 없다. 따라서 여러 가지를 섞어 사용하는 것은 아주 자연스런 일이다. 패러다임 역시 즐겁게 갖고 놀면서 이리 저리 굴리고 붙여야 할 도구에 지나지 않다.
소프트웨어는 한 가지 기법만 써서는 잘 만들기 어렵다. 각 패러다임이 그 표현 기법과 특성에 따라 서로 맞아떨어지는 응용분야가 다르다는 것은 잘 알려진 사실이다. 그런 이유로 오래 전부터 많은 학자나 기술자들이‘패러다임 묶어 쓰기’를 시도하고 실천해 왔으며 1990년대에 이르러 그런 연구 결과가 눈에 띄게 드러나기 시작했다. 서로 다른 응용 분야가 하나의 소프트웨어로 합쳐져 마치 종합예술 같은 성향을 보이는 요즘, 한 가지 언어나 기법만으로 좋은 소프트웨어를 만들겠다는 고집은 고루한 욕심일 수 있다. 그리고 이런 추세를 부정하고 싶어도 이런 현상을 증명할 자료는 도처에 얼마든지 있기 때문에 닥쳐서 걱정하느니 일찌감치 이런 변화를 받아들일 준비를 해두는 게 현명하다. 지금부터는 잘 알려진 사례를 들어 그런 추세를 읽어보기로 하겠다.
먼저 80~90년대에 패러다임 대란의 주인공인 C++부터 살펴보자. 1990년대 초부터 시작해서 1998년 표준이 제정되기까지 C++ 프로그래밍 세계는 포괄 프로그래밍을 도입하느라 열병을 앓았다. A. Stepanov, M. Lee의 연구와 B. Stroustrup의 지원으로 STL(Standard Template Library)이란 작품이 나오지 않았다면, C++ 표준은 1990년대 초에 끝났을 것이다. 이 STL을 표준 라이브러리로 받아들이려면 C++ 언어를 크게 확장하지 않을 수 없었고, C++는 1998년에 이르러서야 겨우 표준 작업을 마무리하게 된다. 그런데 도대체 STL이 뭐길래 표준 제정을 수년이나 뒤로 미뤄가면서까지 도입하려고 노력했을까? 표준 위원회란 보통 보수 성향을 보일 수밖에 없기 때문에, 아무 리 좋은 연구 결과물이라고 해도 커다란 진보를 단번에 그리 쉽게 수용하지 않는 것이 정상이다. 더구나 C++는 이미 산업에 도입돼 한참 많이 쓰고 있는 언어인데, 개혁에 가까운 변화를 받아들였다는 것 자체가 보기 드문 사례라 할 수 있다.
당시 가장 주목받는 언어 중 하나였던 C++에 이 독특한 라이브러리가 도입됐다는 사실은 단순히 언어 기능의 확장을 넘어선 의미가 있다. 알다시피 STL이 제안되기 전에도 C++에는 템플릿(template)이 있었고 실험 수준의 포괄 프로그래밍은 어느 정도 가능했다고 할 수 있다. 그러나 C++다운 포괄 프로그래밍 구현 기법을 찾아볼 수 없을 뿐만 아니라, 당시의 템플릿 기능은 그런 고급 기법을 실현해낼 만큼 완성도가 높지 않았기 때문에, 그야말로 템플릿은 ‘계륵’같은 기능이었다. 그러나 C 언어의 문제점을 고스란히 내려 받은 데다가, 그 전체 기능이 정교하다 못해 어지러울 만치 복잡한 언어라 해도 STL은 또 하나의 굵직한 기법(포괄 프로그래밍이 멋지게 실현될 수 있음을 보여준 역작)이라 할 수 있다. 이것으로 C++는 객체지향성에 이어 포괄 프로그래밍까지 세상에 끌어다 주면서 또 한번 커다란 공헌을 하게 된 셈이다. 또한 템플릿 기능이 제자리를 잡은 덕분에 ‘패러다임을 섞어 쓰는 프로그래밍 언어’의 하나로 자리 매김하게 되었다.
자바의 JGL(http://www.objectspace.com/products/voyager/libraries.asp) 역시 STL의 영향을 받아 자바에서 비슷한 수준의 라이브러리를 쓰고 싶어하는 사람들의 요구를 반영한 것이라 할 수 있다. 그러나 JGL이 STL의 자바판이라 해도 너무 다른 언어의 것을 흉내냈기 때문에 STL이 잡아낸 두 마리의 토끼(성능과 유연성)를 따라잡을 수는 없었다. 사실, STL은 C에서부터 확장된 C++의 특성을 놀라우리만치 현명하게 이용해서 만든 것이다 보니, 그 철학은 빌리더라도 STL 만의 독특한 설계나 구현 기법을 다른 언어에서 흉내내기가 쉽지 않다. 하지만 자바에 처음부터 포괄 프로그래밍을 지원하는 기능이 들어갔거나, 최소한 JDK에 컬렉션 프레임워크가 들어가기 전에, GJ(Generic Java) 제안이 채택됐다면 상황은 크게 달라졌을 것이다. 최소한 STL과 같은 명작은 아니더라도 지금과 같이 빈약한 컬렉션 라이브러리는 나오지도 않았을 것이며, JGL도 지금보다 더 세련되고 쓸모 있게 설계됐을 것이다.

튼튼한 이론과 경험에서 나오는 소프트웨어의 힘

자바 프로그래머 중에는 GJ(http://www.cis.unisa.edu.au/~pizza/gj/) 제안을 받아들이는 경우 자바의 단순함이나 간결함을 해칠 것이라 걱정하는 이들이 있는데, 이는 틀린 생각이다. 어떤 언어의 표현력이 늘었다고 해서 그 언어의 의미 체계를 고쳐써야 한다면, 그 책임은 그런 기법에 있는 게 아니라 언어 자체에 있다. 정말 좋은 언어라면 표현력을 더하기 위해 언어를 확장해도 기본이 되는 언어의 알맹이(Kernel)가 뒤틀리거나 흐트러지지 않으며 불어나지도 않는다(Haskell을 배워가며 자연스럽게 알게된다). 같은 이치로, 만일 자바가 잘 설계한 언어라면 GJ 제안은 부담이 아니라 오히려 불편하고 모자라는 부분을 채워주는 치료제가 될 것이 틀림없고, 더구나 그것이 GJ 제안이라면 추가된 특징을 쓰지 않은 바이너리와 완전한 호환성을 보장하는 기술이라 큰 문제가 없다. 그리고 많은 이들이 GJ 제안을, C++의 템플릿을 자바에 옮겨놓은 것으로 잘못 알고 있는데 목적은 같더라도 접근 방식에 많은 차이가 있기 때문에 관심있다면 관련 문서나 논문을 꼼꼼하게 읽어보고 잘못된 생각을 바로 잡는 게 옳을 것이다. GJ 제안의 실용성은 채택된 다음에 실제 써봐 가면서 경험으로 따져야 할 주제지만, 최소한 그 기반이 되는 이론은 그리 대충 만들어진 것이 아니다.
GJ 제안을 초기부터 따라온 사람이라면 그 모태가 되는 언어 Pizza(http://pizzacompiler.sourceforge.net/)를 자연스럽게 알게되고, Phlip Wadler란 학자를 알게 되면서 관심은 Haskell로 이어지게 된다. 물론 Haskell과 Pizza는 상당히 다른 언어지만, 두 언어에서 추구하는 목표는 같다. 표현력에 있어서는 분명‘패러다임의 결합 ’을 시도하는 것이고 , 이를 바탕으로 좀 더 나은 프로그래밍을 할 수 있도록 언어를 더 튼튼하게 만들어 내는 데 있다. Pizza는 함수형 프로그래밍(Functional Programming) 언어 분야의 연구 결과를 실용화하는 시도라 할 수 있고, Martin Odersky(http://diwww.epfl.ch/~odersky/)와 Philip Wadler (http://www.research.avayalabs.com/user/wadler/)라는 이 분야의 석학이 이끌어낸 또 하나의 성과다. 이분들은 또한, 이 글에서 소개하는 Haskell에 지대한 공헌을 한 학자이기도 하다. 필자는 이런 연구 결과가 소프트웨어 산업이 바르게 진보하는 데 커다란 도움이 된다는 사실을 믿어 의심하지 않으며, 그게 좀 더 빠르게 실현되기를 진심으로 바란다. 경험이 풍부한 기술자라면 단순히 기술을 답습하는 게 아니라 이런 연구 결과를 높이 사고, 산업으로 이식하는 데 노력을 아끼지 말아야 할 것이다.
단 두 가지 예로‘패러다임 섞어 쓰기’의 타당성을 인식하기는 힘들 것이다. 그러나 이 글에서 배우게 되는 Haskell에서도 이런 사례를 찾아볼 수 있기 때문에 이 논의는 이쯤에서 접기로 하겠다. 관심 있는 독자들은 관련 자료를 찾아보고 왜 그런 변화가 자연스러운 것이며, 왜 저명한 학자들이 도처에서 그런 연구를 하고 있는지 인식하기 바란다. 이제 자바나 C#도 그런 추세를 피할 수 없는 단계에 이르렀다.
이런 추세 속에서 우리는 간단한 이치만 깨닫고 있으면 된다. 좋은 언어와 좋은 기법을 써서 제대로 프로그래밍하는 법을 가르치고배우자. 그리고그 목표를 이룰 수 있다면 이런 저런 근거 없는 선입관은 던져 버리자. 프로그래밍 능력과 소프트웨어의 질은 튼튼한 이론의 힘과 오랜 기간의 경험에서 온다는 사실을 의심하지 말자. 그러므로 언어나 환경에 집착하지 말고, 무엇을 어떻게 제대로 만들 것인가에 집중하자.
다음의 글은 Andrew Hunt와 David Thomas이 쓴, ‘The Pragmatic Programmer : From Journeyman to Master’란 책의 머릿글을 일부 옮긴 것이다. 이 글을 읽고 어떻게 좋은 프로그래머가 되는 것인가를 다시 한 번 상기하면서 Haskell의 세계로 들어가 보자.
“툴 공급사는 자사 제품이 여러 작업을 기적같이 처리해낼 거라고 큰 소리 친다. 기법의 대가들은 자신이 만든 기술을 쓰면 좋은 결과를 보장할 수 있다고 약속한다. 또 사람들은 모두 자기가 쓰는 프로그래밍 언어가 최고라고 주장하 고, 이런 저런 운영체제를 쓰면 앞으로 어떤 문제가 발생해도 해법을 얻을 수 있다고 설명한다. 물론, 모두 사실이 아니다. 세상에 쉬운 답이라는 것은 없다. 그리고 그게 도구이든, 언어든, 운영체제이든 최고의 해법 같은 것은 없다. 다만 어떤 특별한 환경에서 가장 적절한 체계(System)가 가능할 뿐이다. 여기서부터 실용주의 사고가 생겨난다. 어떤 특정 기술에 치우치지 않고, 풍부한 바탕과 경험을 가져야 어떤 상황에서라도 좋은 해결책을 선택할 수 있다. 이런 바탕은 컴퓨터 과학의 기본 이론과 원칙을 이해하고 여러 가지 광범위한 분야의 실무 프로젝트를 해본 경험에서 생겨나는 것이다. 그러므로 이론과 경험을 결합해야 스스로를 더 튼튼하게 가꿀 수 있다.”

순수하고 진보한 Haskell

Haskell(http://www.haskell.org)은 함수형 패러다임을 기본으 로 하는 언어다. 많은 차이점이 있지만, Lisp ∙ ML ∙ Scheme 등의 언어와 맥을 같이 한다. 함수형 언어가 어떤 것인지 전혀 모른다고 해도 다른 언어에 비해 문법과 기법이 간단해서 배우기 쉽기 때문에 미리 고민할 필요는 없다. 단지 언어가 너무 간단하다 보니 다른 언어를 배워가면서 풀어내는 수준보다 훨씬 더 빠르게 어려운 문제를 접하게 되고, 이 때문에 곤혹스럽다. 그러나 오히려 이런 점이 이런 류의 언어를 배우면서 얻을 수 있는 장점이다. 즉, 문제를 풀어내는데 집중할 수 있다. 그리고 언어 그 자체의 애매 모호한 기능을 이해하고 외운다고 시간을 낭비하지 않아도 되기 때문에, 많은 분야의 문제를 풀어볼 시간을 가질 수 있다.
산업계에서 유행하는 언어나 기술은 그 다음에 필요에 따라 익혀도 늦지 않다. 오히려 배우는 속도가 배로 빨라질 것이다. 유행이란 자주 바뀌기 마련이어서 배울 때의 기술과 써야 할 때의 기술 사이에는 많은 차이가 있기 마련이다. 그러므로 기술을 이해하는 바탕을 마련했다면 기술의 상세는 가능하면 뒤에 배우는 게 오히려 이익일 때가 많다.
Haskell이 크게 인정받는 이유는 그 의미의 순수성(순수성이란 이런 언어에다 붙여야 제격이다)을 지켜가면서도 진보한 개념을 받아들이는 데 게으르지 않고, 잘 정립된 이론을 체계있게 녹여 언어의 알맹이를 가볍고도 작게 유지하는 데 있다. 그러므로 새로운 표현 기법이 수용되더라도 언어의 바탕은 변화가 없다.
물론 Haskell은 이론상으로만 인정받는 언어가 아니다. 필자가 이 언어를 처음 접한 것이 1993년이었는데(그 때는 물론 Mark P. Jones의 Gofer로 시작했으니 엄격히 말하면 지금의 Haskell은 아니다), 이렇게나 응용의 폭이 넓어지리라고는 예상하지 못했다. 1998년부터는 영국 캠브리지의 마이크로소프트 연구소에서 이 회사의 분산객체 솔루션을 Haskell에서 지원하는 연구(http://research.microsoft.com/ppt/)도 진행되고 있다. 이 연구팀에는 Luca Cadeli, Simon P. Jones 등의 저명한 학자가 주축을 이루고 있으며, 같은 팀의 Don Syme의 홈페이지에서 이와 관련된 자료를 충분히 얻을 수 있을 것이다(http://research.microsoft.com/users/simonpj/, http://research.micr osoft.com/~dsyme/).
마이크로소프트가 실제 이런 연구를 지원하기 전에, 이와 비슷한 소문이 돌았는데 얼마 있지 않아 정말 그렇게 돼서 많은 사람들이 놀랐다. 사실 당시만 해도 상상하기 힘든 사건이었기 때문이다. 어떤 소문이 돌았는지 알고 싶다면 http://www.haskell./humor/press.html를 읽어보기 바란다.

Haskell로 도전하는 함수형 프로그래밍

함수형 프로그래밍 언어의 기본 개념은 정말 단순하다. 즉 기본 표현 수단이 함수고, 여러 함수를 결합해서 새로운 함수를 만들어내는 간단한 방법으로 표현력을 끌어올린다.
squarex = x * x --x를 제곱하는 함수square quad x = square (square x) -- square를 이용해 quad를 표현
앞에서 quad는 함수형 언어답지 못한 표현이다. 객체지향 언어에서 객체가 마음대로 조작할 수 있는 기본 기능이듯이 함수형 언어에서 함수는 특수한 기능이 아니라 정수와 같은 기본 데이터형(primitive data type)이다. 즉 함수는 값이다. 그러므로 두 함수를 합쳐서 하나의 함수로 만들 때는 함수 합성 연산자(composition operator)를 쓰면 된다.
quad x = (square . square) x
여기서 양편의 x를 없애버리면, 좀 더 함수형 언어다운 표현을 쓸 수 있다.
quad = square . square
 
여기서도 알 수 있듯이 함수형 언어에서 함수란 주고받고 붙일 수 있는 보통 데이터와 다를 바가 없다. 그래서 함수를 다른 함수의 인자로 보내고 값으로 되돌려 받는 것이 전혀 이상하지 않다. 예를 들어 우리가 모든 홀수에 4제곱을 해야 한다고 하자. 먼저 모든 홀수 값을 열거하는 리스트를 Haskell로 표현하면 다음과 같다.
odds = [1,3..] -- 무한한 리스트를 표현한 것이지만 값이 한 번에 쏟아져 나오는 것을 걱정할 필요가 없다. -- 필요할 때 값을 계산하는 Lazy Evauation에 대해서는 차차 알게 된다.
다음에 각 원소에 quad를 적용해 보자
quadOfOdds = [quad e | e <- odds]
마치 수학 시간에 배운 집합의 표현 방법과 비슷하다. 읽어보면 리스트 odds의 모든 원소를 열거해서 각각의 원소에 quad 함수를 적용한 다음, 다시 리스트로 묶는다는 뜻이다. 이 때 4제곱 이외에 다른 함수를 쓰고 싶다면 다음과 같이 함수를 인자로 받을 수 있다.
 
oddsWith f = [f e | e <- odds ]-- C 언어 등에서 함수의 포인터를 받는 것과는 차원이 다르다.
그리고 모든 원소를 제곱하고 싶다면 다음과 같이 square 함수를 건네줘 처리한다.
OddsWith square
한번 쓰고 말 함수라면 이름 없는 함수 식을 만들어 쓸 수도 있다(이를 lambda 식이라고 한다. 아마 자바의 anonymous object와 비슷한 느낌을 받을 것이다).
oddsWith (\x -> x * x) -- \는 lambda를 말한다. x를 받아 x * x 식이 되는 함수 값을 표현한다.
다시 모든 원소에 1씩 더한 다음에 제곱하고 싶다면 다음과 같이 여러 표현을 쓸 수 있겠다.
 
plus1 x = x + 1 oddWith (square . plus1) -- 합성된 함수를 인자로 보낸다. oddWith (square . (\x -> x + 1)) -- lambda 식으로 plus1을 바로 만들어 쓴 것. oddWith ((\x -> x * x) . (\x -> x + 1) -- 모두 lambda 식으로 표현
 
사실 C++의 STL에서 Functor라 불리는 기법은 바로 함수형 언어의 함수 표현을 C++에서 객체로 표현한 것이다. 물론 재미있는 기법이고 잘 만들기는 했지만 함수형 언어만큼 자연스런 표현은 나오지 않고 번거롭기 때문에 그 용도도 크게 제한된다. 자바나 C#에서도 비슷한 기법을 쓸 수 있다.
간단한 예제지만 아마 생소한 느낌이 들었을 것이다. 그리고 전에 비슷한 언어를 접해보지 않은 사람은‘참 저렇게 생겨먹은 프로그래밍 언어도 있구나’싶을 것이다. 간단한 맛보기 예제를 보고 이래 저래 성급한 판단을 내릴 필요는 없다. 우선은 그저 새로운 걸 익힌다는 재미로 부담없이 받아들이자. 천천히 하나씩 만지다 보면 점점 더 재미를 느낄 것이고 좀더 진지하고 복잡한 문제를 풀 수 있게 될 것이다.
 

천국과 지옥을 오가며, Haskell과 C++

후배나 동료에게 프로그래밍 능력을 키우는 데 어떤 언어를 선택 해 어떻게 공부하면 좋으냐는 질문을 자주 받는다. 나는 다른 특별한 이유가 없다면 진지하게 공부할 만한 언어로 언제나 두 언어를 추천한다. 그 중 하나는 Haskell이고 다른 하나는 C++다. 두 언어가 너무 다른 세계에 살면서도 공통점을 갖고 있고, 그 학습 효과는 서로의 장단점을 잘 보충해 주기 때문이다. 그리고 두 프로그래밍 언어를 자유롭게 쓸 정도가 되면 이 땅에 나온 대부분의 프로그래밍 언어를 쉽게 받아들일 수 있다. 그 만큼 학습 기간이 짧아지면서 이해의 정확성도 높아진다는 것이다. 물론 이 글을 읽는 독자 중에 이미 다른 언어를 진지하게 공부하고 있는 사람이 있을 것이다. 그렇다면 굳이 배우고 있는 언어를 포기할 필요는 없다. 단지 성격이 아주 다른 언어 하나를 택해서 프로그래밍의 재미를 느껴보면 된다. 그게 꼭 Haskell과 C++가 아니어도 상관없다.
Haskell로 직접 소프트웨어를 구현하지 않더라도, ‘Test First programming(Prototyping)’에 쓰면 딱 좋은 언어다. 문제를 풀어내는 방법에만 집중 할 수 있으며, 형(Type)의 의미를 바로 세워서 프로그램 틀을 제대로 잡는 데 큰 도움이 된다. 학습 도구로서의 좋은 점은 굳이 말할 필요조차 없다. 필자는 처음으로 프로그래밍을 배우는 사람에게 언제나 이런 류의 언어를 추천한다. 이런 저런 세부기술이나 도구를 익히기 전에 프로그래밍하는 방법을 몸에 배이게 하고 제대로 프로그래밍하는 재미를 알게 하는 것이 먼저이기 때문이다.
C++를 권하는 이유는 사뭇 다르다. C++는 이런 저런 언어 기능을 다 알고 총 동원해서 쓰자면 최소한 수년 이상의 경험이 필요하다. 물론 특정 개발 도구에서 정해진 응용 분야의 프로그램을 반복해서 만드는 게 일이라면 다른 언어와 비교해도 그리 어렵지 않다. 그러나 이왕 배우는 언어라면 누구나 유창해 지고 싶을 것이고, 그렇다면 C++는 만만한 언어가 아니다. C++가 다른 면에서 프로그래밍 능력을 키우는 데 도움이 되는 이유는 나름대로 독특한 특징이 있기 때문이다. 일단 C 언어나 어셈블리 언어를 쓰는 것처럼 기계 수준의 여러 자원 관리 기법이나, 각각의 언어 기능이 어떻게 돌아가는 지 정확하게 꿰고 있어야 안전한 코드를 쓸 수 있다. 따라서 저 수준의 자원관리나 성능에 관한 여러 주제를 접하면서 시스템 프로그래밍으로 얻을 수 있는 학습 효과를 공유한다. 역으로 C++에는 여러 가지 수준 높은 추상화 수단이 있기 때문에 저 수준의 자원 관리 기능을 적절히 가려가며, 자바나 C# 수준의 간편한 표현을 얻어낼 수 있다. Haskell에 비할 수는 없지만, C++는 산업에서 인기를 얻고 있는 언어 중에서 메타언어 기능이 강한 편에 속한다. 즉 언어의 문법이나 기능을 마치 기본 기능인 것처럼 꿰맨 자국 없이 끼워 넣을 수 있어 코드를 쓰는 재미가 있다.
정리하면, C++는 현실 세계에서 일어나는 여러 가지 프로그래밍의 쓴맛을 느끼면서, 실제 일어날 수 있는 여러 분야의 프로그래밍 소양(시스템 프로그래밍에서 비즈니스 응용 프로그램까지)을 고루 갖추는 데 도움이 되는 학습 도구다. 또한 Haskell은 C++ 류의 언어에서 잃어버리기 쉬운 다른 여러 프로그래밍 기법이나 표현력을 보충해 주는 효과가 있고 진보하는프로그래밍의 개념을 놓치지 않고 따라 잡는데 큰 도움이 된다. 또한, C++류의 언어를 쓸 때 좀 더 나은 기법을 시도하는 밑거름이 되는 경우가 많다(STL과 같은 라이브러리가 그 좋은 예다).
어차피 프로그래머가 되려고 한다면 천국과 지옥 양쪽을 오가야 한다. 그래야 접해보지 않은 분야라도 놀라서 도망치지 않을 수 있다. 사실 모두가 C++나 Haskell을 쓰지 않아도 일정 수준에 오르면 같은 생각을 갖게 되지만 지금까지의 교육 경험이나 학습 경험을 빗대어 예를 들었으니 특정 언어를 강요하는 것으로 오해하지 않기를 바란다. 어차피 서너개 정도의 언어는 자유롭게 쓸 수 있어야 하니 그 중에 C++나 Haskell이 있다고 나쁠 이유가 없다.
 

Haskell을 첫 번째 언어로

Haskell의 전체 기능을 익힌다면 고급 기능을 쓸 수 있는 수준까지 상당히 오랜 시간이 걸린다. 다른 언어와 달리 Haskell이 제공하는 모든 언어 기능은 시간을 투자해서 배우고 익힐 가치가 충분한 것 뿐이지만, 처음으로 접근하는 이들에게는 분명 부담스럽다. 그러나 그럼에도 불구하고 이 언어를 프로그래밍에 입문하는 이들에게 적극 권한다. 특히 시간을 두고 제대로 프로그래밍을 익힐 여유가 있는 사람이라면 놓치지 말아야 할 언어다. 왜 그런지를 이제부터 서서히 알게될 것이다.
대부분의 교육 기관에서 프로그래밍 언어를 처음 가르칠 때 선택하는 언어는 자바, C#, C/C++ 등이다. 우리나라 소프트웨어 산업에서 아주 인기있는 언어를 고르는 경우가 많다. 그러나 첫 번째 언어는 보다 신중하게 생각할 필요가 있다. 프로그래머에게 있어 첫 번째 언어란 모국어와 같다. 언어는 분명 생각하는 방법과 표현 능력에 커다란 영향을 준다. 그런데 소위 ‘뜨는 언어’의 대부분은 처음부터 복잡한 문법과 여러 가지 언어 기능을 먼저 이해하도록 강요하기 때문에, 쓸만한 문제를 풀어내는 재미를 느끼게 하기까지 상당한 연습이 필요하다. 대학 수업을 예로 들자면 한 학기 내내 그런 언어를 가르쳐 봐야 풀어내는 문제의 수준과 배우게 되는 프로그래밍 기법의 범위는 극히 제한된다. 그 정도라면 스스로 관심이 있어, 좋은 책으로 혼자 익히는 것보다 못하다. 그러므로 그 기간 안에 제대로 프로그래밍하는 재미를 느끼게 만드는 것은 그런 언어의 특성상 거의 불가능하다고 봐야한다. 많이 쓰는 언어를 가르치면 실무 능력이 뛰어난 프로그래머가 될 거라고 무턱대고 배우고 가르치는데, 얼마나 위험하고 얕은 생각인지를 깨달아야 한다.
한 프로그래머의 프로그래밍 수준이 프로그래머의 추상화 능력과 응용 분야별 프로그래밍 경험의 다양함에 달려있다는 점에는 아무도 이의를 제기하지 않을 것이다. 그렇다면 프로그래밍 능력을 갖추는 데 우선 필요한 것이‘문제를 제대로 풀어내어 표현하는 방법’, 다시 말해 프로그래밍 언어를 사용해 문제의 해결책을 체계적으로 끌어내는 훈련이라는 점에도 동의해야 한다. 프로그래머의 모국어가 되는 첫 번째 언어는 앞으로 익히게될 수많은 이론과 기술의 바탕이 되는 프로그래밍 사고의 틀을 이룬다. 이 틀을 바탕으로 프로그래머는 새로운 개념을 접하게 됐을 때의 적응력과 창의성을 얻게 되는 것이다. Haskell은 수학이라는 튼튼한 바탕 위에 쌓아올린 프로그래밍 언어다. 물론 이런 언어가 Haskell만은 아니겠지만, 분명 가장 진보하는 프로그래밍 언어 중에 하나임은 틀림없다. 그리고 다른 언어로 프로그래밍하는 노력을 이 언어를 익히는 데 조금씩만 투자하면, 스스로의 프로그래밍 능력을 키우는 데 훨씬 높은 효과를 얻어낼 수 있다.

Haskell 프로그래밍 환경

Haskell 공식 홈페이지에 가면 금방 찾아낼 수 있겠지만, 가장 많이 쓰는 도구를 두 개만 간단히 소개하겠다. 먼저, 인터프리터 방식이라 입문자가 쉽게 쓸 수 환경으로 HUGS(Haskell User’s Gofer System)가 있다. HUGS는 Mark P. Jones라는 Gofer로 유명해진 학자의 작품이다. 필자도 Gofer라 불리던 환경에서 먼저 Haskell과 비슷한 언어를 써보기 시작했는데, 프랑스 inria 연구소의 CAML 다음으로 만지게 된 언어였다. 그 때의 신선한 충격은 아직도 잊을 수가 없다. SASL에서나 볼 수 있는 무한 데이터 스트럭처(infinite date structure)를 간편한 인터프리터 환경에서 즐길 수 있다는 것만 해도 무척 가슴 떨리는 일이었기 때문이다. 이름에서 쉽게 짐작할 수 있듯이 지금의 HUGS는 Gofer로부터 시작됐고, Gofer 인터프리터 환경에서 Haskell를 지원하면서부터 HUGS라는 이름으로 불리게 된다.
다른 하나는 GHC(Glasgow Haskell Compiler)라 약칭하는 Haskell 컴파일러다. HUGS가 간편하고 단순한 개발 환경이라면, GHC는 전문 기능을 고루 갖추고 있어서 제대로 응용 프로그램을 만들려면 꼭 필요한 도구다. 보통 Haskell 사용자들은 둘을 섞어 쓰는 경우가 많다. 물론 입문하는 사람은 HUGS로 시작하는게좋다. 아마 어느 정도 수준이 될 때까지는 HUGS만으로도 충분할 것이다. 이어지는 Haskell에 대한 설명은 모두 HUGS를 기본 환경으로 한다.

HUGS와 함께 하는 Haskell 프로그래밍

HUGS는 쓰기 쉽기 때문에 상세한 기능 소개는 생략하고 처음 접하는 이를 위해 간단한 설명만 하고 넘어가기로 하겠다 (HUGS를 내려 받는 사이트에서 매뉴얼도 구할 수 있다. 꼭 받아서 한 번 읽어 보길 권한다. 유닉스 계열 운영체제를 사용한다면 간단히 man hugs로 기본 사용법을 알 수 있다). 먼저 HUGS 를 설치한 다음 그대로 실행한 상태라면 다음과 같은 프롬프트를 볼 수 있다.
 
🗣
Prelude> _
HUGS의 표준 라이브러리를 Standard Prelude라고 부르기 때문에 현재 Prelude 모듈이 올라와 있다는 걸 뜻한다. 간단히 인터프리터를 돌려보기 위해 수식 계산을 해보자.
34.7897 * 0.43215
🗣
15.0344
 
이번에는 간단한 변수를 선언해 보자.
x = 2
🗣
ERROR - Syntax error in input (unexpected `=’)
 
이상하게 에러가 난다. 인터프리터에서 간단한 변수 하나 선언하려 했는데 무슨 에러까지 내나 싶을 것이다. 처음 HUGS를 쓰는 사람들은 당연히 이상하겠지만, Haskell을 알고 나면 차차 이해가 갈 것이다. 일단은 인터프리터에서 전역 환경(Global Environment)에 새로 이름을 더할 수 없다는 것 정도로만 알아두자. 그래서 함수나 변수를 정의하고 싶다면 따로 파일을 불러들여 Haskell 스크립트를 만들어야 된다. 이때 인터프리터 명령어 :edit를 쓰면 원하는 미리 지정한 편집기를 불러 Haskell 스크립트를 편집할 수 있다.
 
:edit Fact.hs
 
모든 인터프리터 명령어는‘:’을 붙여서 일반 Haskell 식(Expression)과 구분한다. 만약 다음과 같은 에러 메시지가 나오면 편집기로 쓸 에디터를 정해주지 않은 것이다.
 
🗣
ERROR - Hugs is not configured to use an editor
 
환경변수 EDITOR를 에디터 프로그램의 이름으로 설정하면 제대로 동작한다. 특히 vi 호환 에디터라면 다음과 같이 설정해서 다른 편리한 기능을 쓸 수 있다.
 
EDITOR=“vi +%d %s” export EDITOR
 
여기서 %d는 스크립트 파일의 특정 줄을 찾아갈 수 있도록 만든 것이고 %s는 특정 글월을 검색해 그 글월이 있는 줄로 가는 기능이다. 모두 vi 류 에디터의 표준 기능이기 때문에 윈도우 환경에서 노트패트 같은 에디터를 쓴다면 이 기능과 연계된 hugs의 인터프리터 명령어를 이용할 수 없다(EMACS를 쓰면 hugs-mode에서 이보다 더 편리한 기능을 제공한다).
이제 다음과 같이 Fact.hs를 작성하고 저장한 다음 인터프리터로 돌아가자(에디터로 vi를 쓰면 ex-mode에서 wq로 파일을 저장하고 에디터를 빠져 나올 수 있고, 다시 인터프리터 환경으로 돌아온다).
fact n | n < 0 = error“negative integer” | n = 0 = 1 --여기서 에러가 난다. | n > 0 = n * fact(n-1)
 
| boolean_expression = expression의 꼴은 Guarded expression이라 부르는 것인데 나중에 따로 설명하기로 하겠다. 그러나 코드가 쉬워서 대충 그 쓰임새나 의미를 알아차리기 어렵지 않다. 이제 인터프리터의 전역 환경에 방금 작성한 스크립트 내의 함수 정의를 추가할 수 있도록 스크립트 파일을 읽어 들인다.
 
:load Fact.hs
🗣
Reading file“Fact.hs”: Parsing ERROR“Fact.hs”:3 - Syntax error in input (unexpected `=’)
 
예상대로 세 번째 라인 코드에서 에러가 발생한다. 이때 EDITOR 변수에서 정한 라인을 찾아가도록 설정해 두었다면 (+%d), 다시 edit 명령을 쳤을 때 에러를 만든 라인을 찾아서 보여준다. 인터프리터는 바로 전에 에러를 만든 스크립트 파일을 기억하고 있기 때문에, 파일 이름없이 edit명령을 치면 된다.
 
:edit
 
함수 fact의 Guarded Expression의 조건식, n = 0은 변수 n0 값을 비교해서 참거짓을 결정하는 식이기 때문에 =대신에 ==을 써야 옳다. 맞게 고친 다음 저장하고 에디터를 빠져 나오면 인터프리터는 다시 스크립트를 읽어 들인다. 다음과 같은 결과가나오면 제대로 고쳐 쓴 것이다. 이때, 스크립트 내의 코드 수행 결과가 인터프리터의 전역 환경을 확장한다.
 
🗣
Reading file“Fact.hs”: Hugs session for: /usr/local/share/hugs/lib/Prelude.hs Fact.hs Main>
 
따로 정하지 않아도(당연한 것이지만) Standard Prelude 스크립트는 기본으로 따라다니는 걸 알 수 있다. 그리고 Fact.hs 스크립트가 잘 처리됐다는 것을 확인할 수 있다. PROMPT가 Main으로 나오는 것은 스크립트 모듈의 이름을 따로 지정하지 않았기 때문이다. 그래서 스크립트에 모듈을 더하면 다음과 같다.
 
module Fact ( fact ) where -- 모듈 이름을 Fact로 정의한 이름 중에 -- fact를 밖에서 불러 쓸 수 있도록허락 한다. -- 여기서 where는 ()안의 이름을 정의한다.
 
인터프리터로 돌아온 다음 정상 처리된 스크립트의 모듈(Fact)의 이름으로 PROMPT 문자가 바뀐 걸 볼 수 있다.
 
Fact>
 
이제 fact를 써보자.
 
fact 40 -- 이만하면 계산기로 사용해도 훌륭하다.
🗣
815915283247897734345611269596115894272000000000
fact (-1)
🗣
Program error: negative integer
 
이제 다음과 같이‘장난’을 걸어보자.
 
fact‘a’ -- 일부러 문자 값을 인자로 보냈다.
 
다음과 같은 에러 메시지를 보게 될 것이다.
🗣
ERROR - Illegal Haskell 98 class constraint in inferred type *** Expression : fact ‘a’ *** Type: Num Char ⇒ Char
 
이 메시지는 에러의 원인을 정확하게 설명하고 있다. 식 fact ‘a’의 타입(형)을 유추해보니 Num Char => Char여야 하는데 class constraint(클래스 제약조건)를 위반했다는 말이다. 어떻게 이런 결과가 나오게 된 것일까? 그리고 Num Char => Char은 무엇을 뜻하는 것일까? 하나 더 실험해보자.
 
fact 4.0
🗣
24.0
이번에는 에러 없이 값이 나왔지만 역시 원하던 결과가 아니다. 정의에 따라 fact 함수는 오로지 양의 정수만을 받을 수 있어야 하므로 그 범위를 넘어서는 값을 보내면 형 불일치(type mismatch) 에러가 나와야 정상이다. 결론부터 말하면 여기서 예제로 든 fact는 미완성이다. 그 이유와 해결책은 다음으로 미루 , 일단 :type 명령으로, 식(expression)의 형을 유추해 볼 수 있다는 것만 알아두자. 이제 앞의 fact 함수를 써서 여러 식의 형을 알아보면 다음과 같은 결과를 볼 수 있다.
 
:type fact 4.0
🗣
fact 4.0 :: (Ord a, Fractional a) => a
:type fact 50
🗣
fact 50::(Ord a, Num a)=>a
 
앞의 두 식에서 같은 함수를 썼지만 인자의 형에 따라 식의 형도 바뀐다는걸 알 수 있다.그리고 fact함수의 형은 다음과 같이 나온다.
 
:type fact
🗣
fact :: (Num a, Ord a) => a -> a
 
Haskel의 형 체계는 다음 호에서 살펴보기로 하자. 마지막으로, 현재 인터프리터 환경에 올라와 있는 이름이 어디서 어떻게 정의한 것인지 알고 싶을 때, 찾아가는 기능을 써보자.
 
:find fact
 
설정에 문제가 없다면 지정한 에디터로 Fact.hs 파일을 열어 fact를 정의하는 코드를 화면에 보여준다.
 

형을 유추하는 언어 Haskell, Type inference

 
 
대부분 우리가 알고 있는 프로그래밍 언어는 C 언어처럼 형을 직접 적어주는 것이 보통이다.
int fact(int n)
 
 
그러나 앞의 예제에서도 보았듯이 Fact 스크립트에는 그런 게 전혀 보이지 않는다. 그런데도 스크립트를 처리하는 시점에 어떻게 함수의 형을 알아내고 검사하는 것일까? Haskell이나 ML 같은 언어는 파이썬과 같이 형을 검사하지 않는 언어가 아니라, 다른 언어보다 더 철저하고 그 규칙이 아주 까다로운 언어다. 다만 형 표현과 코드를 분리해 놓았기 때문에 언뜻 보면 형 검사를 하지 않는 언어처럼 보인다. 선언과 정의를 분리하는 기법은 C 정도의 언어에서도 찾아볼 수 있다.
 
int fact(int); // 미리 선언하고 ... int fact(int n) {...} // 뒤에 정의한다.
 
잘 알다시피 C/C++에서 프로토타입 선언이라고 부르는 기능인데, 미리 함수의 형을 선언하고, 나중에 구현할 때 형을 잘못 선언하는 실수를 막을 수 있기 때문에 꼭 필요한 기능이다. C/C++에서는 여러 함수나 자료 구조의 type signature를 따로 모아 헤더파일에 넣어 놓고, 여러 파일에서 같이 불러 쓸 수 있다. Haskell에서도 비슷한 기능을 쓸 수 있다. 앞에서 보았던 fact 함수의 정의는 type signature를 더해서 완성한다.
 
fact :: Integer -> Integer -- type expression으로 fact 의 type signature를 정의한다. fact ...
 
그러나 Haskell 류의 언어와 C나 파스칼과 같은 언어의 형 검사 기능 간에는 큰 차이점이 있다. 다른 언어에서는 정확한 형 검사를 위해 프로그래머가 모든 이름에 형 이름을 직접 붙이도록 강요하지만 Haskell에서는 그렇지 않다. 똑똑하게도 식을 읽어서 그 형을 추적해 낸다. Haskell에는 형을 추적하는 기능을 type inference 엔진이라고 한다. Haskell의 모든 식은 정해진 inference rule에 따라 가능한 일반적인 형을 추론해 낸다. 그러므로 코드에서 어떤 이름을 쓸 때, 코드 내부에 있는 이런저런 이름에다 형을 일일이 붙여줄 걱정을 하지 않아도 된다. 다만 fact와 같은 함수에서는 너무 일반적인 형을 추론해냈기 때문에, 보다 범위가 적은 Integer 형을 쓰도록 제약을 더해야 올바른 함수 정의가 된다. 이런 경우에도 함수 그 자체의 형만 type signature로 선언할 뿐 내부 코드에 있는 변수에 형을 적어줄 일은 거의 없다. 완성된 Fact.hs 모듈은 다음과 같다.
 
module Fact ( fact ) where fact :: Integer -> Integer fact n | n < 0 = error“negative integer” | n == 0 = 1 | n > 0 = n * fact(n-1)
 

배정문이 없는 언어 Haskell

Haskell에는 배정문(Assignment)이 없다. 그래서‘순수’함수형(purely-functional)이란 수식어가 항상 따라다닌다. 사실 함수형 언어라면 수학 논리에 따라 프로그래밍하는 언어이기 때문에 배정문 같은 이상한 기능이 당연히 빠져 있어야 한다. 그러나 배정문이 없으면 정말 불편한 응용 분야가 있다. 알다시피 배정문은 상태를 바꾸는 명령어다. 즉, 값을 나타내는 식이 아니다. 그래서 이전의 상태를 바탕으로 계산을 해나가야 할 필요가 있는, 다시 말해 시간에 따라 변하는 상태를 계산하는 방식을 꼭 써야하는 경우 배정문이 있으면 편리하다. 상태를 바탕으로 하는 계산 기법에서, 꼭 배정문이 유일한 해법은 아니지만, 다른 기법을 쓰면 많이 불편하다(다른 기법에 관심이 있는 사람은 Stream이나 Continuation을 공부해 보는 것도 좋다). 그래서 같은 함수형 언어라고 하더라도 ML ∙ Scheme ∙ LISP 등 대부분의 언어는 어쩔 수 없이 배정문을 포함했다. 그러나 Haskell은 다르다.
고집스럽게도 Haskell에서는 배정문을 아예 쓸 수 없다. 그 대신에 상태 모나드(State Monad)라고 불리는 더 우아한 기법을 쓴다, 모나드는 1990년대 컴퓨터 과학 분야에서 가장 멋진 연구 결과물의 하나일 것이다. 이 이론이 채택된 다음, Haskell은 한층 더 좋은 언어로 발전을 거듭하게 됐다.
왜 함수형 언어에서는 배정문을 쓰지 않으려고 그렇게 애를 쓰는 것일까? 우리가 쓰는 대부분의 프로그래밍 언어에서, 배정문은 필수 불가결한 기능이기 때문에 그런 표현이 없다면 어떻게 프로그램을 할 수 있을까 의심스럽기만 하다. 그런데도 scheme과 같은 언어를 보면(어쩔 수 없이 배정문을 쓰기는 하지만) set!과 같이 뒤 에 ! 기 호 를 붙 여 ‘ 아주 위험한 기능 ’이라는 경고 표시를 하는 걸 관례화 해 쓸 정도로 배정문을 가능하면 쓰지 말아야 할 표현으로 보고 있다. 그렇게나 피해야 할 이유가 있을까?
배정문은 프로그래밍 언어의 자연스런 표현이라기보다, 하드웨어의 저장 장치를 그대로 모방하는 것으로, 배정문을 쓰는 언어에서는 변수란 것도 메모리 내의 어떤 주소에 붙여둔 이름에 불과하다. 간단히 말해 변수는 메모리 주소를 추상화한 것이다. 사실, 안전한 코드를 쓰지 못하게 하고, 하드웨어 수준의 디버깅을 피하지 못하게 만드는 이유가 바로 배정문이다. 심하게 말하자면 프로그래밍의 거의 모든 문제점이 배정문과 연관있다. 특히, 쓰레드 프로그래밍이나 포인터를 살펴보면 이런 얘기가 더 가슴에 와 닿을 것이다.
병행처리 환경에서 프로그래밍이 어려워지는 원인이 실제 어디에 있는지 생각해 보자. 임계 구역을 정하고 쓰레드가 동기를 맞추도록 특정 코드 영역에 배타성을 부여하는 걸 당연하게 생각했다면, 스스로 다음과 같이 되물어 볼 필요가 있다. 병행처리는 동시성을 쓰려고 하는 것인데, 다시 특정 영역에 순차성을 보장하려고 세마포어니 모티터니 랑데뷰니 하는 기법을 쓰고 있다. 이 모든 기법은 병행처리 환경에서 임계 구역이라 불리는 어떤 영역의 코드를 보호하는 기법이다. 무엇 때문에 이런 보호 기법이 필요할까? 모두 그렇지는 않지만 임계 영역으로 정한 코드 안에는 상태를 바꾸는 코드가 있고, 여러 쓰레드가 동시에 상태 변화를 이용해 데이터를 처리하려고 하기 때문에, 시간의 연속성을 보장하지 않으면 코드가 제대로 돌아가지 않는 경우가 생긴다. 이와 같이 여러 쓰레드가 공유하는 상태 변화를 따라 가다보면 정확한 코드를 쓰는 게 얼마나 힘든 일인지 잘 알게된다. 프로그래머가 가능한 상태 변화를 모두 그려내지 못하면 그 코드는 에러 발생 가능성이 높은데, 코드가 복잡해져서 간단한 병행성 패턴이나 안전성이 검증된 기법을 써서 표현할 수 있는 한계를 벗어나면, 그 많은 경우를 다 잡아내는 게 사실상 불가능하다.
또 한 가지 예로 포인터가 있다. 자바가 C++에 비해 쉬운 이유 중의 하나는 바로 이 포인터를 없애버렸기 때문이다. 사실 포인터만 없앤다고 문제가 해결되는 것은 아니다. 물론 포인터를 남용해도 코드가 이상해지지만, 정작 골치 아픈 상황은 배정문과 포인터를 함께 썼을 경우다. 한 객체를 가리키는 여러 개의 포인터 중에, 한 포인터가 가리키는 객체의 값을 바꿔버리게 되면 그 영향이 전체 포인터 변수에 미치게 된다. 효과야 간단하지만 이런 변화가 전체 코드에 걸쳐 있으면, 어디서 어떻게 무엇을 바꾸었는지 쉽게 드러나지 않을 수 있기 때문에 여러 가지 문제를 일으키고 코드를 분석하기 어렵게 만드는 원인이 된다. 게다가 포인터와 변수를 잘못 써서 에러가 발생했을 때, 디버거로 메모리에 적재된 값을 일일이 뒤져보지 않으면, 그 원인조차 쉽게 발견할 수 없는 경우도 많다. 객체지향이나 표현 수준이 높아진 프로그래밍 언어에서 오류를 분석하는 데 쓰는 디버거가 하드웨어 메모리 검사기나 다를 바 없다는 게 전혀 이상하지 않은가. 고급 표현 수단을 쓰다가도 이런 오류가 발생하면 기계 수준으로 돌아가야 한다는 것을 이상하게 생각해야 정상이다. 포인터가 없는 자바와 같은 언어에서도 여전히 메모리에 변수 값이 무엇인지 찾아봐야 에러를 고칠 수 있는데, 이것도 바로 배정문 때문이다. 이 배정문과 병행처리, 포인터가 이리 저리 얽혀 있는 상황에서 에러가 났을 때,전체 코드를 이 잡듯 뒤져가며 에러 수정 작업으로 고생을 한번이라도 해본 사람은 그 일이 얼마나 시간을 잡아먹으면서도 효과적이지 않은 일인지 잘 알고 있을 것이다.
상태를 바탕으로 하는 계산이 반드시 배정문과 같은 기계어 수준의 명령어로만 처리돼야 한다는 생각은 잘못된 것이다. 우리가 앞으로 배우게 될 Haskell이 그런 사실을 증명한다(Haskell을 공부하면서 그런 좁은 생각에서 벗어날 수 있을 것이다).
 

정말 작은 언어, Haskell

자바나 C#을 좋아하는 사람들은 이 언어를 작고 단순한 언어라고 한다.그러나 결코 작고 단순한 언어가 아니다. 두 언어 모두 그런 평가가 어울리지 않는다. 정말 작고 단순하기로 치자면 기계어 만치 작고 단순한 언어가 어디에 있겠는가(0과 1만 있으면 된다). 그러므로 그저 기능이나 표현력이 모자란 언어를 작고 단순하다고 평가해서는 안된다. 표현력은 풍부할수록 좋다. 표현력이 문제를 풀어내는 데 얼마나 영향을 주는지 공감하기 힘들다면, 다음과 같은 식을 다른 언어로 표현해 보라.
 
[ (x, y) | x <- [1,3 ..20], y <- [2,4..20]] -- 1에서 20까지 사이의 모든 홀수와 짝수의 순서쌍
🗣
[(1,2),(1,4),(1,6),(1,8),(1,10),(1,12),(1,14),(1,16),(1,18),( 1,20),(3,2),(3,4),(3,6),(3,8),(3,10),(3,12),(3,14),(3,16),(3, 18),(3,20),(5,2),(5,4),(5,6),(5,8),(5,10),(5,12),(5,14),(5,16 ),(5,18),(5,20),(7,2),(7,4),(7,6),(7,8),(7,10),(7,12),(7,14), (7,16),(7,18),(7,20),(9,2),(9,4),(9,6),(9,8),(9,10),(9,12),(9 ,14),(9,16),(9,18),(9,20),(11,2),(11,4),(11,6),(11,8),(11,10) ,(11,12),(11,14),(11,16),(11,18),(11,20),(13,2),(13,4),(13,6) ,(13,8),(13,10),(13,12),(13,14),(13,16),(13,18),(13,20),(15,2 ),(15,4),(15,6),(15,8),(15,10),(15,12),(15,14),(15,16),(15,18 ),(15,20),(17,2),(17,4),(17,6),(17,8),(17,10),(17,12),(17,14) ,(17,16),(17,18),(17,20),(19,2),(19,4),(19,6),(19,8),(19,10), (19,12),(19,14),(19,16),(19,18),(19,20)]
 
어셈블리 언어를 쓴다고 해도, 마음만 먹는다면 못할 리야 없겠지만 그리 즐거운 일이 아닐 것이다. 간단히 말해서 어셈블리나 C는 저런 문제를 풀어내는 데 편리한 언어가 아니다.
Haskell은 정말 풍부한 표현력과 기능을 제공한다. 그리고 끝 없는 확장성을 제공한다. 그러면서도 의미 바탕이 얼마나 단순한지 알면 놀랄 것이다. 이런 사실을 이해하기 위해 간단하지만 엉뚱한 질문을 던져 보겠다. 만일 다른 언어에 if 같은 기본 제어문이 없다면 어떻게 될까? 아마 대부분의 언어는 치명적인 결함으로 코드를 쓸 수조차 없을 것이다. 그러나 Haskell이라면 if 문이 없다 해도 문제 될 게 없다. 그저 함수로 그런 기능을 정의해서 쓰면 된다.
 
newIf True thenExpr elseExpr = thenExpr newIf False thenExpr elseExpr = elseExpr
 
그리고 마치 진짜 if문처럼 쓰더라도 전혀 문제가 없다.
 
factIf n = newIf(n < 0) (error “negative integer”) (newIf (n == 0) 1 (n * factIf (n - 1)))
 
달리 말하면 Haskell에서는 if와 같은 기능조차 반드시 필요한 표현이 아니다. 제어 기능도 기본 표현으로 만들어 쓸 수 있다. 같은 함수형 언어라고 하더라도 ML(물론 Lazy ML같은 변형도 있지만)이나 Scheme 같은 언어에서는 불가능하다. 자바나 C# 같은 언어에서도 사정은 같다. 이런 언어에서는 ifswitch니 하는 제어문을 특수 구문으로 추가하지 않으면 달리 해결할 방법이 없다.
프로그래밍 언어에 있어 Haskell과 같은 유연성은 아주 큰 의미가 있다. 다른 언어에서 상상할 수 없는 기능을 만들어 나갈 수 있다는 것은 기본이고, 점차 표현을 늘리고 기능을 더해도 언어의 기본 구조나 의미의 틀이 바뀌지 않는 다는 걸 뜻하기 때문이다. 그러나 C++나 C# 류의 언어에서는 언어 특징이 늘어나면 언어의 알맹이가 늘어나고 기존의 다른 특징과도 그리 잘 결합되지 않기 때문에, 이런 저런 복잡한 규칙이 새로 추가되는 걸 피할 수 없다. 게다가 컴파일러는 한층 만들기 어려워지고 점차 불어나는 덩치를 감당하기도 힘들다.
Haskell과 같은 언어가 저런 표현의 유연성을 얻게 되는 데는 그만한 이유가 있다. 그동안 모든 프로그래밍 언어의 필수 기능이던 배정문을 과감하게 없애버리고, 미뤄뒀다가 계산하는 기법(Lazy Evaluation)을 기본으로 채택하고 있기 때문이다. 이 기법은 다음 호에서 상세하게 설명하기로 하고 일단 다음과 같은 사실만 기억하자.
프로그래밍 언어가 작고 간결하다는 말은 단순히 문법이 덜 복잡하고 기능이 단순하다는 것을 뜻하는 게 아니다. 물론 그런 요소도 고려해야겠지만, 그보다는 그 언어의 의미가 얼마나 간결하고도 정립된 이론에 바탕을 두고 있느냐에 달려있다. 비슷한 이유로 GJ 제안이 채택돼 자바의 덩치가 커지더라도 자바 언어 자체가 복잡해지는 게 아니다. GJ에 비하면 Inner Class 같은 기능이 언어를 더 복잡하게 만든다. 그러므로 더 안전하고 좋은 표현을 쓸 수 있다는 것은 프로그래머로서 환영해야 할 일이지 걱정할 일이 아니다. 만일 어떤 언어가 기능이 늘어남에 따라 정말 의미가 복잡해지고, 그 언어의 건전성을 파괴한다면 그 이유는 다음 둘 중 하나다. 예를 들어 포인터나 배정문과 같이 그 언 어의 안전성을 해치는 기능이 더해졌거나, 아니면 언어 차제가 애초부터 잘못 만들어진 것이다. 자바나 닷넷에 포괄 프로그래밍의 지원을 더했을 때, 그것이 앞의 두 가지 경우 중 어디에 해당하는 변화인지를 독자 스스로가 냉정하게 판단해 보기 바란다.

불평하기보다 극복하는 방법을···

우리는 STL 같은 업적으로부터 한 가지 교훈을 얻어야 한다. 우리 대부분은 그 당시에 C++가 어렵다고 불평만 했다. 그리고 지금은 자바와 닷넷이 어렵다고 불평한다. 그러나 STL을 만든 사람들의 자세는 달랐다. 불평하기보다는 극복하는 방법을 썼다. 필자는 학자든 프로그래머든 이래야 한다고 생각한다. 우리는 Haskell과 같이 잘 설계한 언어로부터 제대로 프로그래밍하는 방법을 배우기도 해야겠지만, C/C++ 같은 언어를 쓰지 않을 수도 없다. 덜 깨끗하고 복잡한 언어가 있는 이유는 덜 깨끗하고 복잡한 환경을 위해 프로그램을 만들어야 하기 때문이며, 기술자라면 그 환경을 제대로 길들일 줄 알아야 한다.
어떻게 길들일 것인가를 위한 여러 가지 아이디어와 동기는 Haskell과 같은 언어를 쓰면서 얼마든지 얻어 낼 수 있다. STL이 나오게 된 이유도 비슷하다. 만일 C++에서 포괄 라이브러리(Generic Library)를 설계하기 전에, scheme이나 Ada같은 언어로 실험하지 않았다면(함수형 패러다임과 parametric polymorhism으로 얼마나 멋진 코드를 쓸 수 있는 지 알지 못했다면, 그리고 이런 저런 불평을 해대며 C++와 같이 까다로운 언어에서 시도조차 하지 않았다면) STL과 같은 역작이 나왔을 리가 없다. Haskell과 같은 언어를 배우는 데는 이런 여러 가지 깊은 뜻 이 있으며, 그 교육의 효과는 무궁무진하다.
Haskell과 같은 언어를 소개하면 언제나 듣는 불평이 있다. 이 언어를 배우려면 기초부터 이론을 다시 정리하고(변수에 대한 생각부터 고쳐나가야 한다), 당장 실무에 필요 없어 보이는 이론까지 알아야 하는데 그럴 이유가 있냐는 것이다. 다시 말해 ‘이론 따위는 필요없다. 당장 프로그램을 만들고 쓸 수 있는 언어만 배우기에도 벅차다’는 불평이다. 나는 보통 이런 반론에 다음과 같이 답한다. 바로 기술을 익히는 것이 벅찬 이유가 지금까지 그렇게 학습해 왔기 때문이라고...
이번 호에서는 프로그래밍 언어 Haskell을 소개했다. Haskell 프로그래밍 입문은 다음 호부터 시작하니까, 우선 이런 언어가 있다는 것만 알아뒀으면 한다. HUGS 정도를 설치하고, 나온 예제를 따라가는 정도만 공부하는 것으로 충분하다. 이번 기사는 Haskell을 배워야 하는 이유를 설명하고, 동기를 부여하는 게 목적이었으므로 여유가 있으면 관련 자료를 읽어보며 스스로 학습 의지를 다져보는 것도 좋을 것이다. 그런 뜻에서 MIT의 설립자 William Barton Rogers의 말을 인용하며 이번 글을 마친다. 다음 호에서는 Haskell과 즐거운 시간을 보낼 수 있을 것이다.
“세상은 과학자와 공학자를 억지로 구분하려 하지만 정말 쓸데없는 짓이다. 지금까지의 모든 경험에 비춰볼 때 그것이 얼마나 헛된 생각인지 잘 알 수 있다(The world-enforced distinction between the practical and the scientific worker is utterly futile, and the whole experience of modern times has demonstrated its utter worthless)”
 
정리 : 강경수 elegy@sbmedia.co.kr
 

참고 자료

 
  1. Herold Abelson, Gerald Jay Sussman with Juile Sussman,“Structure and Intepretation of Computer Programs,”2nd Ed. MIT Press. 1996
  1. Richard Bird,“Intoduction to Functional Programming,”2nd Ed. Prentice Hall 1998
  1. Andrew Hunt, David Thomas,“The Pragmatic Programmer : From Jorneyman to Master,”Addison-Wesley 1999.
  1. Paul Hudak John Peterson,Joseph Fasel,“The Gentle Introduction to Haskell,” http://www.haskell.org/tutorial/, 2000
  1. Why Functional Programming Matters by John Hughes, The Computer Journal, Vol. 32, No. 2, 1989, pp. 98 - 107. Also in: David A. Turner (ed.): Research Topics in Functional Programming, Addison-Wesley, 1990, pp. 17 - 42.