Haskell의 함수형 프로그래밍, 정적 타이핑, 지연 평가, 모나드에 대한 탐색.
섹스, 소프트웨어, 정치, 그리고 총기. 삶의 소박한 즐거움들…
한동안 컴퓨터 언어 Haskell을 배우는 일을 할 일 목록에 넣어 두었다. 다른 활동이 잠시 뜸해진 데다 혹독한 감기에 걸려 고된 일을 할 마음이 나지 않게 된 덕분에, 이제 실제로 배우기 시작했다. 남은 생애 동안 Haskell을 아연 글루콘산염의 맛과 연관 짓게 될지도 모르겠다. 둘 다 떫고 약 같은 성질이 있으며, 효능에 관해 열렬한 주장이 나오지만 나는 적당히 낙관적일 뿐 아직 개인적으로 확인하지는 못했다.
Haskell은 수리논리학자들이 수리논리학자들을 위해 만든 언어임이 아주 분명하다. LISP 뒤에 람다 계산법이 도사리고 있듯, 그 뒤에는 범주론이 도사리고 있다. 다음은 Haskell이 세계를 구분하는 방식을 이해할 수 있게 하려는 시도이며, 부분적으로는 내 생각을 명료하게 하기 위해 썼다.
Haskell은 몇 가지 강력한 원시 개념과 하나의 pons asinorum 위에 세워져 있다. 그중 가장 널리 스며든 것, 즉 부수 효과나 변수가 없는 순수 함수형 프로그래밍은 C, C++, Python 같은 전통적 명령형 또는 객체지향 언어에서 온 대부분의 프로그래머에게 가장 어려운 장벽이라고 Haskell 입문서들은 대체로 설명한다. 그렇다. 프로그래밍에 대한 당신의 정신 모형이 모두 for 루프와 정적 변수에 관한 것이라면 — 아니면 객체에 관한 것이라 해도! — Haskell식 사고를 배우는 일은 꽤 큰 충격일 것이다.
그 노력에는 이유가 있다. Haskell 사용자들(Haskell 신봉자들? Haskell 학파? Haskell 조작자들? Haskell 골격들?)은 명령형 프로그래밍이 부수 효과에 크게 의존하며, 이것이 프로그램 정확성 증명을 실용적으로 어려운 것과 불가능한 것 사이 어딘가로 밀어 넣는다고 주장한다. 또한 부수 효과가 여러 프로세서에 걸친 프로그램의 자동 병렬화를 어렵게 만든다는 점도 지적한다. 멀티코어가 예외가 아니라 규칙이 되어 가는 상황에서 이는 점점 더 중요한 고려 사항이다.
둘 다 탄탄한 논거다. 나는 오래전부터 람다 계산법의 기사였고 — LISP가 순수 함수형 언어는 아니지만, 이를 진지하게 프로그래밍하는 사람은 누구나 함수형 스타일에 대한 감각을 기르게 된다 — 그래서 명령형 사고를 포기하는 일이 대부분의 사람보다 덜 힘들다. 따라서 #haskell IRC 채널에 머물던 중 나보다 더 초보인 누군가에게 먼저 LISP를 조금 배우고 Haskell로 돌아오는 방법을 고려해 보라고 권했다. 채널의 강경한 Haskell 사용자 누구도 반대하지 않았고, 일반적으로 좋은 조언일 가능성이 크다고 생각한다.
이제 앞선 문단들에는 거짓말이 두 가지 있었다고 인정하겠다. 첫째, Haskell에도 변수가 있다. 어느 정도는. 그러나 그런 ‘변수’는 한 번만 대입할 수 있고, 변수의 값은 변수가 평가될 때마다 대입문 오른쪽의 식을 재평가하겠다는 약속이다. 현대 명령형 언어에 익숙한 사람들이 변수라고 부르는 것보다는 안전한 매크로나 Algol-60의 이름 호출에 더 가깝게 동작한다. 둘째, 모나드라는 구조를 사용하면 부수 효과가 있는 연산과 실제 변수처럼 동작하는 것을 정의할 수 있다.
모나드로 다시 돌아가겠지만, 그전에 Haskell의 다른 두 기초, 정적 타이핑과 지연 평가를 소개해야 한다. C나 Java 같은 컴파일 언어로 작성해 본 사람에게 정적 타이핑은 낯선 개념이 아니겠지만, Haskell은 이 개념을 일종의 논리적 극한까지 밀어붙인다. 변수에 명시적 타입이 필요하지는 않지만(변수가 약칭하는 식에 의해 암묵적으로 타입이 정해진다), 어떤 함수에도 타입 서명을 붙일 수 있는 문법이 있고 컴파일러는 그것으로부터 타입 추론을 한다. 이는 두 가지 결과를 낳는다. 언어의 효율적인 컴파일을 가능하게 한다는 것(이 정도 추상화 수준의 언어에서는 드문 일), 그리고 Haskell의 관점에서 더 중요한 것으로 타입 주석이 프로그램 정확성을 증명하는 데 활용할 수 있는 불변 조건을 단언한다는 것이다.
사용자 정의 타입은 대체로 타입 값을 갖는 식의 값이다(실제로는 더 복잡하지만, 이렇게 생각하는 것은 유용한 출발점이다). 지금까지 마주친 Haskell의 가장 유쾌하게 기묘한 세부 사항은 이것이다. 타입 값을 갖는 변수를 만들 수 있고, 재귀적인 타입 값 식을 작성할 수 있다! 트리 같은 일부 타입에서는 이것이 자연스러운 방식이다.
지연 평가는 이해하기 쉽다. 식의 값은 호출자가 실제로 필요로 할 때까지 계산되지 않는다는 뜻일 뿐이다. 평가는 안쪽에서 바깥쪽이 아니라 바깥쪽에서 안쪽으로 진행된다. Scheme의 클로저 개념에 익숙하다면(혹은 Scheme 클로저를 차용한 Ruby 같은 다른 언어에 익숙하다면) Haskell 식이 클로저를 반환한다고 생각하면 도움이 된다. 프로그램이 실행되면 가장 바깥의 주 클로저가 강제되어 평가된다. 그러면 그 안에 직접 있는 다른 클로저들(식과 함수 호출)의 강제가 촉발될 수 있고, 이 과정은 클로저이기도 한 하위 식들을 통해 계속된다. 아는 척할 수 있는 지역색 하나: Haskell 세계에서 클로저는 ‘thunk’라고 부른다.
여러 최적화가 지연 평가를 효율적으로 만든다. 특히 식은 보통 순수하므로 클로저는 흔히 그 값에 대한 포인터로 대체될 수 있고 한 번보다 많이 평가될 필요가 없다. 지연 평가의 장점은 매 순환마다 값을 하나씩 반환하며 끝없이 돌아가는 Icon 또는 Python 생성기 같은 코드를 작성할 수 있다는 것이다. 호출자 자신이 생성기일지라도, 바깥 프로그램이 실제로 요구하는 정확한 횟수만 호출된다. 이것은 명령형 언어의 for 루프를 대체하는 능력 중 하나다.
Haskell 코드 대부분이 순수하더라도(상태도 없고 부수 효과도 없음), 상태를 지닌 세계와 연결해야 한다. 특히 입출력은 순수하지 않다. 입력원에서 한 줄을 읽으면 프로그램 바깥 세계의 상태에 따라 달라지는 결과를 얻는다. 일반적인 Haskell 관행은 가능한 한 많은 순수 코드와, 부수 효과가 있는 함수 호출(표준 입력에서 한 줄을 읽거나 표준 출력에 한 줄을 쓰는 것 등)을 최소한으로 사용해 프로그램을 작성하는 것이다. 다만 평가가 바깥쪽에서 안쪽으로 일어나므로, 입출력 함수 호출이 실제로 하는 일은 강제될 때 입출력 부수 효과를 낼 클로저를 만드는 것이다.
그러므로 질문은 이것이다. 출력 부수 효과를 가진 클로저를 생성하는 함수 호출이 여러 개 있다고 하자. 클로저들이 원하는 순서로 강제되도록 코드를 어떻게 작성할까? 여러 방법이 가능하다. 가장 명백한 방법은 ‘모든 것은 함수 호출’이라는 원칙의 예외가 되는 어떤 특별한 구문을 요구할 것이다. Haskell이 이런 순차화를 위해 사용하는 것은 모나드다.
아, 모나드. 이것이 Haskell의 _pons asinorum_이다. Haskell 사용자들은 이것이 범주론의 ‘강한 모나드’라는 난해한 개념처럼 실제로 동작한다는 사실을 사랑한다. 언어를 설명하는 많은 글은 이 지점에서 그 사실을 휘두르기 시작할 것이다. 나 자신도 수학자였고 범주론을 어느 정도 이해하고 있지만, 여기서 그것을 끌어들이는 일은 혼란스럽고 불필요하다고 말하겠다.
모나드를 생각하는 더 단순한 방법은 함수 합성을 함수 호출의 순차화를 강제하는 방법으로 바꾸는 해킹, 또는 함수형 프로그래밍에서 셸 파이프라인에 해당하는 것으로 보는 것이다. 그리고 그런 도발적인 말을 해 놓고, 실제로 그것이 일어나게 하는 지저분한 기술적 세부 사항으로는 들어가지 않겠다.
나는 보통 언어의 문법이 아무리 이상하더라도 타입 존재론이나 평가 방식처럼 더 중요한 것에 비하면 단순한 세부 사항이라고 본다. 그러나 Haskell 문법의 한 가지 핵심적인 점은 주목할 만하다. 바로 생산 규칙을 사용하는 방식이다. Haskell 함수는 일련의 패턴-동작 쌍으로 이루어진 다소 AWK 같은 스타일로 작성할 수 있다. 순서대로 시도하며, 입력과 처음으로 일치하는 패턴이 실행된다. 목록 끝까지 내려가면 실행 시간 오류가 나지만, 와일드카드를 쓴 ‘그 밖의 모든 것’ 규칙은 쉽게 작성할 수 있다.
요약하자면, Haskell을 무엇에 쓸지, 아니 실제로 언젠가 쓰게 될지조차 모르겠다. 하지만 이것과 씨름하며 보낸 시간은 헛되지 않았다. 내 선입견에 도전했고, 전에 보지 못했던 가능성을 보여 주었으며, 이전에는 이론일 뿐이었던 지연 평가 같은 개념을 실용적으로 이해하도록 강요했고, 전반적으로 내 사고를 흔들어 놓았다. 그것만으로도 가치가 있다. 해커가 되는 법에서 나는 이렇게 썼다. ‘LISP는 배울 가치가 있다. […] 마침내 그것을 이해했을 때 겪게 될 심오한 깨달음의 경험 때문이다. 그 경험은 실제로 LISP 자체를 많이 사용하지 않더라도 남은 생애 동안 당신을 더 나은 프로그래머로 만들어 줄 것이다.’ Haskell에도, 매우 비슷한 이유로, 같은 말을 할 수 있다고 생각한다.
esr가 2010-03-10에 게시함
소프트웨어로 분류됨
댓글과 보관 목록은 원문 페이지의 나머지 부분에 포함되어 있습니다.