하스켈, 왜 이렇게 어려운거야?

하스켈, 왜 이렇게 어려운거야?

솔직하게 인정합시다. 하스켈은 어렵습니다. 어떤 사람들은 배우면서 포기하고, 어떤 사람들은 걸음마를 떼고 나서도 몇 걸음 걸어보기도 전에 그만둡니다. 하스켈에 대해 들어보기라도 한 사람들은 아주 많습니다. 그러나 하스켈을 배우기 시작해서 뭔가 쓸모 있는 프로그램을 만들어 보는데까지 도달하는 사람들은 아주 적습니다. 무엇이 하스켈을 그렇게 어렵게 만드는지 궁금해서 직접 배워봤습니다. 저는 아직 막 걸음마를 떼고 몇 걸음 걸어보는 단계에 있습니다. 기성 주류 언어는 물론 Lisp, Ruby, Go 같은 상대적으로 생소한 언어들도 배워본 적이 있지만, 하스켈은 그 어떤 언어보다도 배우기에 어려웠습니다. 익숙해지기까진 더더욱 오래 걸리겠지요. 여기에 그 기록을 남깁니다. 하스켈을 새로 배우거나, 혹은 재도전하고자 하는 분들께 도움이 되기를 바랍니다.

하스켈이 익숙하지 않은 사람을 위해 가능한 한 코드를 배제하고자 했지만, 코드를 보여주는 것이 설명하기 더 용이한 경우에는 가능한 한 작은 코드 예시를 제공하려고 노력했습니다.

  • 배우면서 겪는 어려움들
    • 생소한 개념들
    • 수많은 문법
    • 하스켈의 외모
  • 배우고 나서 겪는 어려움들
    • 모나드는 여전히 어렵다
    • 이 언어가 나에게 쓸모가 있을까?
  • 결론
    • 하스켈의 쓸모

배우면서 겪는 어려움들

많은 사람들이 하스켈을 배우면서 중도 포기하게 됩니다. 모나드는 물론 악명 높은 진입 장벽입니다. 하지만 하스켈을 공부하려고 시도해본 많은 사람들의 의견을 들어보면 모나드가 전부는 아니라는 걸 알 수 있습니다. 튜토리얼에조차도 많은 복병들이 숨어있기 때문입니다. 당신만 어렵다고 느끼는 게 아닙니다. 모두들 어려워하고, 도중에 그만둡니다. 어떤 어려움이 있는지 알아두면 하스켈 공부를 시도할 때 많은 도움이 될 것입니다.

1. 생소한 개념들

하스켈은 C, Java, Python 등 유명한 주류 언어들 중 그 어떤것과도 닮아있지 않습니다. 즉, 아주 새로운 세계 속에 있습니다. 그것은 바로 함수형 패러다임입니다.

1.1 함수형 패러다임

MIT-GNU Scheme의 로고. GNU Free Document Lisence 하에 배포됩니다.

함수형 패러다임은 '순수 함수'를 중심으로 한 흐름 제어를 기본으로 삼습니다. 실제로 하스켈의 함수에는 디버깅을 위해 (print같은)출력함수를 중간에 끼워넣는것조차 불가능합니다. 순수 함수라는건 부작용(side effect)를 일으켜서는 안되기 때문입니다. 하스켈에서는 컴퓨터가 소리를 내거나 출력을 하는 것은 물론, 변수의 값을 변경하는 것조차 부작용으로 봅니다. 변수를 바꿀 수 없기 때문에 그 흔한 반복문조차 없습니다. 대신 반복을 표현하기 위해 재귀 함수를 사용합니다. 하스켈은 그것을 더 '함수적'이라고 여깁니다. 입출력처럼 하스켈이 보기에 '순수하지 않은' 작업들은, 하스켈만의 방식으로 완전히 분리시켜버립니다. 심지어 순수하지 않은 연산끼리도 같은 유형이 아니라면 한 곳에 놓을 수 없습니다. 그야말로 순수성에 미쳐버린 언어입니다. 하스켈을 처음 배우는 사람들은 그런 식으로 순수하지 않은 코드를 찢어놓는데 익숙하지 않아 어려워합니다. 하지만 하스켈 프로그래머들은 그 덕분에 하스켈 코드에는 버그가 덜 생긴다고 주장합니다.

1.2 비결정성에 대하여

순수와 비 순수를 어떻게 찢어놓을 수 있을까요? 좀 더 간편한 논의를 위해, '결정적(deterministic)'이라는 단어부터 생각해봅시다. 결정적인 알고리즘은 반드시 예측한대로 동작하는 알고리즘을 말합니다. 반대로, 비결정적인 알고리즘은 때에 따라 예측하지 못한 방향으로 동작할 수도 있거나, 심지어는 완전히 예측 불가능한 알고리즘을 말합니다.

예를 들어, 출력 함수가 결정적이라고 말할 수 있을까요? 때때로 출력이 실패할지도 모릅니다. 예를 들어 인터럽트가 걸려 출력이 멈췄다거나, 디스크 오류로 인해 출력이 중지된 경우를 생각해봅시다. 이처럼 예측 불가능한 상황이 발생할 수 있으니, 출력 함수는 비결정적이라고 이야기 합니다. (이해를 돕기 위해 결정성을 예측 가능성으로 바꾸어 말했지만, 실제로는 예측 가능성 뿐만이 아니라 같은 입력에 대해 정확히 같은 결과를 만드는 것을 포함합니다.)

하스켈은 타입 시스템을 통해 비결정성을 관리합니다. 하스켈의 타입 시스템은 오류 가능성이 있거나, 난수, 변수와 관련된 작업들의 결과를 효과적으로 포장할 수 있습니다. 심지어는 (리스트처럼)결정적인 값도 의도적으로 비결정적인 관점으로 바꾸어 볼 수 있게 해줍니다. 무슨 소리인지 모르겠다구요? 하스켈이 원래 좀 그런 언어입니다.

How can non-determinism be modeled with a List monad?
Can anyone explain (better with an example in plain English) what a list monad can do to model non-deterministic calculations? Namely what the problem is and what solution a list monad can offer.

원한다면, 이 스택 오버플로 문답을 읽어보셔도 좋습니다. 지금 당장은 이해하지 못하더라도 괜찮습니다. 어쩌면 영원히 이해하지 못하더라도 상관 없을지 모릅니다... 하스켈은 원래 그런 언어니까요. 어찌되었건, 하스켈의 함수형 패러다임은 순수 함수와 함께 비결정성 관리라는 두 개의 큰 줄기로부터 뻗어나옵니다.

1.3 비결정성과 타입 시스템; 모나드

함수형으로 표현한 티라미수 레시피 © Hayoi

함수형 패러다임에서는 프로그램을 생각할때, 프로그램의 입력 값이 함수들 사이로 흘러가 변환되는 과정이라고 봅니다. 그런 관점에서 프로그램은 함수들의 결합체일 뿐입니다. 그래서 함수형 언어에서는 함수를 결합하는 패턴이 자주 쓰입니다. 함수형 언어에 반복문이 필요 없는 것도 그런 이유입니다. 예를 들어, 배열이나 리스트같은 반복적인 자료구조를 탐색할 땐 두개의 함수를 결합하면 됩니다. 하나는 리스트 멤버를 순서대로 탐색하는 함수이고, 다른 하나는 멤버마다 어떤 작업을 해주는 함수입니다.

-- toUpper: 알파벳 문자를 대문자로 만드는 함수
toUpper 'a' -- 결과: 'A'
-- map: 문자열의 원소를 하나씩 순회하는 함수
-- map + toUpper: 문자열의 모든 원소를 대문자로 만들어 줌
map toUpper "the quick brown fox jumps over the lazy dog"
    -- 결과: "THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG"

그러다 보니 자연스레 이 자료구조에 함수를 적용할 수 있는지 없는지가 중요한 관심사가 되었고, 하스켈은 그것을 위한 타입 체계를 만들었습니다. 이것이 바로 그 악명높은 펑터,어플리커티브,모나드 타입들입니다. 이 타입들을 여기서 설명하기에는 주제를 벗어나므로 설명하지 않습니다. 지금은 이 타입들이 비결정적 작업을 표현하는데 쓰이기도 한다는 것만 알아두시면 됩니다.

이 타입 체계들의 존재 자체만으로 하스켈 입문자들이 괴로워 하는데는 이유가 있습니다. 하스켈을 배우다보면 온갖 곳에 타입이 붙어있다는 것을 알게됩니다. 처음에는 명시적으로 타입을 안써도 잘 돌아가기 때문에 별 중요성을 못느끼지만, 점점 복잡한 예제를 접하며 타입 에러들을 마주치다 보면 정말 모든 곳에 타입이 붙어있다는 것을 알게 됩니다.

비결정적인 작업들에도 또한 예외는 없습니다. 앞서 말했듯이 비결정적인 작업들도 타입시스템으로 처리되기 때문에, 같은 유형의 비결정적 작업들만이 (모나드 연산을 통해) 한 곳에 모일 수 있습니다. 그 사이에 순수한 작업이 끼어있으려면 (lifting같은) 특별한 결합 연산을 해주어야 합니다.

그 때문에, 모나드 실체를 배우기 전에는 화면 입출력조차 마음대로 하기가 어렵습니다. 그러나 어떤 사람은 이런 생각을 '모나드 괴담'이라고 일축하기도 합니다.

아래 슬라이드는 모나드 괴담을 설명한 슬라이드입니다. 하스켈을 모르더라도 재밌게 읽을만한 슬라이드이므로 보고 오시는걸 추천합니다. 이 슬라이드에서는 '굳이 모나드를 알려하지마라. IO타입 많이 써보고, 경험으로부터 배우라'라고 조언합니다. 저는 이 슬라이드를 하스켈 공부하기 전에 봤기 때문에 이를 실천해보려고 했으나, 장막 뒤에서 어떤 일이 벌어지는지도 모른 채 원인모를 모나드 타입 에러로 고통받으며 IO 타입을 많이 써본다는 것은 솔직히 어려웠습니다.

모나드 괴담(https://xtendo.org/ko/monad)

지금 이 슬라이드가 틀렸다고 주장하려는 것은 아닙니다. 슬라이드의 내용에 반쯤은 공감하고 있습니다. 제가 생각하기에도 겨우 화면에 몇글자 쓰려고 모나드를 완전 격파해야 한다는 믿음은 조금 과한 느낌이 듭니다. 그러나 "모나드를 이해하려 하지마라!"는 조언은 납득할 수 없습니다. 어쩌면 이분이 말씀하시는 '이해'와 제가 생각하는 '이해'의 수준이 다른 걸수도 있겠습니다마는, 결론적으로 모나드를 아예 모르면 안 됩니다.

모나드 배웠다고 하스켈이 끝나냐? © 주호민

모나드를 배운다고해서 모나드의 악몽이 끝나는 것은 아닙니다. 물론 모나드 타입 에러로부터는 전보다 자유로워지겠지만, mtl, transform, lens 등... 모나드를 조금이나마 더 편리하게 사용하기 위한 몸부림이 남아있습니다. 주로 모나드가 어렵다고 고통스럽게 외치는 것은 이 부분이긴 합니다(개인적으로 LYAH은 mtl을 다른 책에 비해 쉽게 설명하고 있는 것 같습니다). 이런 것들을 모두 배웠다고 해도 하스켈이 손에 익는 것은 또 다른 문제이며... 정말 배울게 끝이 없는 것 같습니다.

1.4 타입 시스템

꼭 모나드가 아니더라도 하스켈의 타입시스템은 독특한면이 있습니다.  하스켈 입문자가 만나는 뜻밖의 타입 에러 중 하나는 숫자 타입 사이의 호환성 문제입니다.

length [1, 2, 3, 4] / 4
-- <interactive>:36:1: error:
--   ? No instance for (Fractional Int) arising from a use of ‘/’
--   ...

하스켈은 강타입 언어입니다. 이 말은 자동 형변환이 없다는 뜻입니다. 예를 들어, 반환형이 정수형인 함수의 결과를 실수형을 받는 함수에 집어넣을 수 없습니다. 물론 명시적으로 형 변환을 해주면 되는 간단한 문제이긴 하지만, 익숙하지 않으면 고생을 하게 됩니다. 특히, Int, Word 등의 타입을 Num a 타입으로 업캐스팅 해주는 fromIntegral 함수와, Double, Float 등의 타입을 Integer 타입으로 잘라내주는 truncate 함수 등은 알아두는 것이 좋습니다.

fromIntegral (length [1, 2, 3, 4]) / 4
-- 결과: 1.0

또 다른 어려움은 대수적 데이터 타입(algebraic data types), 타입 클래스(type classes) 등의 생소한 타입 시스템입니다. 클래스를 만들어 내용을 숨기고, 클래스를 상속해서 타입을 확장하는 객체지향 언어들에 익숙했다면, 하스켈의 타입 시스템이 아주 새롭게 느껴질수도 있습니다.

특히 대수적 데이터 타입은 다른 언어의 개념과 일대일 대응이 되지 않기 때문에 더더욱 그렇게 느끼게 됩니다. 대수적 타입은 어떤 면에서는 다른 언어의 값 객체(Value Object)나 구조체처럼 쓰이기도 하고, 어떤 면에서는 C언어의 열거형같이 보이기도 하고, 또 어떤 경우에는 디자인 패턴에서 말하는 컴포지트 패턴처럼 쓰이기도 합니다.

data BillingInfo = CreditCard CardNumber CardHolder Address
                 | CashOnDelivery
                 | Invoice CustomerID
from "Real World Haskell", by Bryan O'Sullivan, et al. §3.3 'Algebraic data types'

이 예시는 대수적 타입이 이렇게까지 쓰일 수 있다는 것을 단적으로 보여줍니다. BillingInfo라는 대수적 타입을 만드는 선언 구문인데, CreditCardCashOnDelivery, Invoice 세개의 생성자를 이용해 BillingInfo 타입을 만들 수 있도록 했습니다. 객체지향 언어에서 이와 비슷한 타입 설계를 하려면, BillingInfo 타입을 상속하는 세개의 클래스를 만들어야 합니다. 그런데 하스켈에서는 아주 다른 설계 패턴이 나타납니다. 더 간단한 예시는 이런 것도 있습니다.

data Ordering = LT | EQ | GT
표준 모듈(Prelude)에 포함된 실제 정의

이 타입은 두 데이터의 비교 결과를 나타내는데 쓰입니다. 마치 C언어의 열거형처럼 생겼지만 대수적타입으로 나타낼 수 있습니다(그래서 하스켈에는 enum 타입이 없습니다).

하스켈의 대수적 타입은 너무나 투명해서, 멤버 함수는 커녕 접근제한자조차 없습니다. 이런 차이는 기존의 자료형 설계 관념을 송두리째 흔들어놓을것입니다.

1.5 게으른 언어

하스켈의 게으른 평가(lazy evaluation)는 정말 중요한 특징 중의 하나이면서, 정말 혼란스럽기 짝이없는 특징이기도 합니다.

당신이 백수 생활을 하고 있다고 가정해봅시다. '부지런한' 당신은, 부모님이 청소하라고 말 하기 전에 미리미리 집안 청소를 합니다. 반면 '게으른' 당신은, 부모님이 청소 좀 하라고 고래고래 소리를 지르기 전에는 절대로 청소를 하지 않습니다. 부지런하면 칭찬을 듣기는 하지만, 게으르면 결과적으로 청소를 조금 덜 해도 됩니다. 아마 게으른 쪽이 몸에는 더 편할겁니다.

하스켈의 게으른 평가도 마찬가지입니다. 하스켈은 그 값이 정말로 필요해질 때까지 연산을 계속해서 미루어둡니다. 예를 봅시다. 하스켈은 무한히 긴 리스트를 생성하는 문법을 갖고 있습니다.

ghci> [1..]
[1,2,3,4,5,6,7,8,9,10,... -- Ctrl-C로 인터럽트를 줄때까지 멈추지 않음

리스트의 앞 쪽 몇개만을 가져오는 take 함수를 이용해, 여기서 원하는 만큼만 가지고 올 수 있습니다. 하스켈의 게으름 덕분에 무한히 긴 리스트를 영원히 계산하지 않습니다. 필요한 만큼만 계산해서 줍니다.

ghci> take 15 [1..]
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]

게으른 평가를 통해 달라지는 또 다른 사실은 and와 or 논리 연산이 꼭 연산자일 필요가 없다는 점입니다. 다른 언어에서는 short circuit evaluation을 구현하려면 반드시 함수가 아닌 특별한 형태(ex. 연산자)일 필요가 있습니다. 그러나, 하스켈의 &&||은 연산자가 아닌 함수입니다. 이것은 하스켈의 게으른 특성으로 말미암은 것입니다.

그러나 게으른 연산이 좋은점만 있는 것은 아닙니다. 하스켈에서는 게으른 연산을 구현하기 위해 함수가 실행될 때마다 값을 저장하는 대신 'Thunk'라는 형식으로 연산 과정만을 저장해둡니다.

두 개의 도해를 보면, 연산을 늦추는 대신 공간을 소모하게 되리란 점을 예상할 수 있습니다. 이 탓에 하스켈 프로그램의 공간 복잡도는 거의 예측이 불가능합니다. 심각한 경우, 매우 복잡한 연산에 의해 비대해진 Thunk는 메모리와 성능을 크게 잡아먹는 Space Leak 현상을 일으키기도 합니다.

하스켈 입문자는 게으른 연산이 자신에게 득이될지 실이될지 가늠하기가 어렵습니다. 예를 들어 무한히 피보나치 수열을 생성하는 다음 코드는, 물론 코드 자체도 이해하기 어렵겠지만, 게으른 연산의 도움으로 선형 시간 복잡도로 아주 빠르게 계산된다는 사실은 더더욱 이해하기 어려울 것입니다.

fibs = 0 : 1 : zipWith (+) fibs (drop 1 fibs)

이 코드에 대해 더 자세히 알고 싶다면 이곳을 참고하세요. 이것 역시, 지금 당장은 몰라도 별 상관이 없습니다. 많은 하스켈 교재에서 게으른 연산으로 인한 문제는 대부분의 경우 신경 쓸 필요가 없다고 조언합니다.

Lazy Dynamic Programming
Dynamic programming is a method for efficiently solving complex problems with overlapping subproblems, covered in any introductory algorithms course. It is usually presented in a staunchly imperative manner, explicitly reading from and modifying a mutable array—a method that doesn’t neatly translate to a functional language like Haskell.

게으른 연산은 하스켈에 어느정도 경험이 있는 사람도 혼란스러워하는 부분입니다. 물론 게으름 때문에 문제가 발생할 경우 확실히 해결할 수 있는 방법이 있기는 합니다. 그러나 여전히 예측하기 어렵다는 점은 똑같습니다.

하스켈의 함수형 패러다임, 타입 시스템, 지연 평가는 입문자를 당혹케 만드는 대표적인 개념들입니다. 다른 언어에서 흔히 볼 수 없어 이해하기가 순탄치 않습니다. 아마 하스켈을 배우다가 그만두게 만드는 가장 큰 이유가 이런 생소한 개념들이 아닌가 싶습니다.  사실 프로그래밍 언어를 몇 개 정도 익혀 놓으면, 누구라도 이런 자신감이 생기기 마련입니다.

'나는 일주일이면 어떤 프로그래밍 언어든 배울 수 있겠다'

새로운 언어라고 해봤자 비슷비슷한 제어 구문에, 비슷한 데이터 선언, 익숙한 라이브러리 함수들에 뭔가 신기해보이는 개념이 양념처럼 추가된 것이지요. 그러나 그런 생각으로 하스켈을 배우기 시작하면, 자신의 생각과는 다르게 흘러간다는 것을 느낍니다.

'함수형?? 대수적 타입? 타입 클래스? 지연 평가?? 모나드???? '

하스켈에게선 기성 주류 언어와 비슷한 면모라고는 코빼기도 보이지 않습니다. 그래서 배우는 동안 자주 어려움을 느낄 것입니다. 여기에, 어려운 개념이 다가 아닙니다. 속속 나타나는 새로운 문법들도 입문자들을 괴롭히곤 합니다.

2. 수 많은 문법

하스켈에는 문법이 꽤나 많습니다. 주류 언어들은 사실 문법이 몇개 안됩니다. 문법이라고 생각되는 것들 중에 연산자와 세미콜론같은 사소한것들을 빼고 나면 언어의 토대를 이루는 문법은 얼마 되지 않을 것입니다. 그러나 하스켈은, 정말로 많습니다. 어찌나 많은지, 하스켈 컨닝 페이퍼가 만들어 질 정도입니다.

하스켈 컨닝 페이퍼 © Rudy Matela

이 중에서는 정말 필요한 문법도 있지만, 그냥 편의를 위해 제공하는 문법 설탕(syntactic sugar)도 있습니다. 하스켈은 문법 설탕이 너무 많아서 단내가 진동하기로 유명합니다. 원한다면, 아래에 소개된 글에서 더 많은 문법 설탕을 맛볼 수 있습니다(이 글도 사실 완전한 리스트를 반영하고 있지는 않습니다. let/in과 As-Pattern 등 빠진 항목들이 있는 걸로 보입니다):

Haskell/Syntactic sugar
Syntactic sugar refers to any redundant type of syntax in a programming language that is redundant to the main syntax but which (hopefully) makes the code easier to understand or write.

2.1 달다, 달아!

하스켈이 이렇게 많은 문법 설탕을 가지고 있는 이유는, 솔직히 말해서 없는 것보다는 편하기 때문입니다. 문법 설탕으로 인해 편의성이 올라가는 극단적인 예시가 바로 모나드 패턴입니다.

[ x | (x,y) <- foos, x < 2 ]

[ (x, bar) | (x,y) <- foos,
              x < 2,
              bar <- bars,
              bar < y ]
리스트 표현식
foos >>= 
  \(x, y) -> guard (x < 2) >>
     return x

foos >>= 
  \(x, y) -> guard (x < 2) >> bars >>= 
    \bar -> guard (bar < y) >>
       return (x, bar)
모나드 연산 패턴

앞선 코드(리스트 표현식)는 사실 뒤이은 코드(모나드 연산)와 의미가 같습니다. 아무리 보아도 리스트 표현식이 훨씬 정갈하고 읽기도 편합니다.

문법 설탕 덕분에 읽거나 쓰기에 편해지는 것은 좋으나, 그래도 너무 많은게 탈입니다. 문법이 많을수록 체계를 잡기는 더욱 어려워지고, 배우기도 힘들어집니다. 상황을 더 악화시키는 것은, 기성 하스켈 유저들이 이마저도 부족하다고 더 많은 확장을 요구하고 있다는 것입니다. 다음 링크는 그런 요구사항 중 하나입니다. 여기에 쓰인 LambdaCase 문법은 GHC 8 버전에 실험적으로 포함된것으로 알고 있습니다.

lambdas vs pattern matching
The current lambda abstraction syntax allows us to conveniently bind parts of the arguments by using patterns, but does not provide a way to branch quickly (without naming the argument). Usually we just cringe a bit and write

그런데 생각해보면, 모던 C++도 비슷한 길을 걷고 있는 것 같습니다.

2.2 레이아웃

하스켈 코드에는 세미콜론을 쓰지 않습니다. 그렇다고 자바스크립트처럼 현재 줄과 다음 줄을 자동으로 붙이거나 떼주는 그런 기능이 있는 것도 아닙니다. 그럼 하스켈은 어떻게 여러줄에 걸친 표현식들을 구분할까요?

하스켈에는 여러 줄에 걸친 표현식이 모호해지는 것을 피하기 위한 들여쓰기 규칙이 있습니다. 입문자들은 종종 이것을 간과해 (혹은 몰라서) 짜증나는 에러를 맞닥뜨리게 됩니다. 예시를 들어봅시다. 다음 코드는 컴파일이 될까요?

let i = 1
  j = 2
  in ...

컴파일하면 다음과 같은 에러가 발생합니다.

error: parse error on input ‘j’
  |
2 |          j = 2
  |          ^

에러 메시지는 무엇이 잘못되었는지 구체적으로 알려주지 않습니다. 그래서 입문자를 더욱 혼란스럽게 만듭니다. 올바르게 고치는 방법은 j의 위치를 i가 있는 곳에 맞춰 주는 것입니다.

let i = 1
    j = 2
  in ...

레이아웃에 대한 원칙은 '보기 좋은 코드가 컴파일도 잘 된다'는 것입니다. 하스켈은 삐뚤빼뚤한 것을 좋아하지 않는다고 기억해두세요. 만약 문제가 발생한다면, 레이아웃에 대한 가이드를 참고하길 바랍니다.

Haskell/Indentation
Haskell relies on indentation to reduce the verbosity of your code. Despite some complexity in practice, there are really only a couple fundamental layout rules.

2.3 이해할 수 없는 이름 규칙

하스켈에는 이상하다 싶을만한 문법적 제약들이 있습니다. 그 중의 하나는 앞서 얘기한 레이아웃입니다. 그리고 또 다른 하나는 바로 이름 규칙입니다.

언어마다 이름을 지을 때 반드시 따라야하는 룰이 있습니다. 대개의 언어에서, 그런 규칙은 그다지 대단한게 아닙니다. 대부분의 경우 그 규칙은 이렇습니다: "영어 알파벳과 숫자, 밑줄 문자만 쓸 수 있고, 첫 문자는 숫자이면 안된다"(자바는 파일명과 클래스명이 일치해야한다는 룰이 더 있기는 합니다). 거기에 언어마다 함수나 클래스 이름 짓는 법에 대한 컨벤션이 있긴 하지만, 어디까지나 컨벤션입니다.

그러나 이런 컨벤션조차 문법에 의해 강제되는 언어가 몇개 있는데, 하스켈이 그 중 하나입니다. 함수명은 반드시 소문자로, 타입 이름은 반드시 대문자로 써야합니다. 거기에 모듈 이름도 반드시 대문자로 시작해야 합니다. 안 그러면 컴파일 에러가 발생합니다.

조금 더 혼란스러운 규칙도 있습니다. 하스켈은 기본적으로 어떤 문자든 함수 이름으로 쓸 수 있으나, 실상 몇 몇 특수문자는 불가능합니다. 이미 함수 이름으로 예약되어있기 때문에 파싱할 수 없기 때문이죠. 특수문자와 관련된 이름 규칙은 좀 더 복잡합니다. 어지간해선 안쓴다고 보는 게 맞습니다.

2.4 괴물 함수들

개인적인 경험으로, 하스켈 입문해서 생소한 개념들을 제외하고 제일 적응하기 어려웠던 것 중의 하나는 함수 표기법이었습니다. 하스켈에서는 함수 호출이 다른 어떤 연산자보다도 우선순위가 높습니다. 그리고 괄호를 쓰지 않습니다. 뭐가 어디에 붙는지 몰라서 한동안 하스켈의 표현 방식에 익숙해지느라 고생했던 기억이 납니다.

-- 더하기(+)보다도 length와 foldl 함수가 더 먼저 실행됩니다.
length [1, 2, 3] + foldl (*) 0 [1, 2, 3]

게다가 하스켈 프로그래밍 스타일 중에는 커링을 이용한 'Pointfree'라고 하는 스타일이 있어, 입문자들이 코드를 읽는데 있어서 더 부담을 주기도 합니다. Pointfree 스타일이란 가능한 한 필요 인자를 생략하는 스타일을 말하는데, 주로 함수 합성을 이용합니다. Pointfree에 대해 설명한 하스켈 위키 문단을 보면 'Pointfree 스타일은 함수의 관점에서 프로그램의 흐름에 집중할 수 있게 해준다'라고 소개하는데, 글쎄요, 더 읽기 어렵기만 한 것 같습니다.

Pointfree
It is very common for functional programmers to write functions as a composition of other functions, never mentioning the actual arguments they will be applied to.

3. 하스켈의 외모

일본 NTV 드라마 "수수하지만 굉장해! 교열걸 코노 에츠코" 中

이제는 하스켈의 외면에 대해 이야기해볼까 합니다. 사실 일단 언어를 배우기 시작하면 외적인 면에 대해 신경쓸 일은 많지 않습니다. 이 라이브러리가 지원 되느냐, 이런 이런 것을 할 수 있느냐 하는 것은 어찌보면 언어를 배우는 과정보다는 동기 부여에 더 영향을 미칩니다. 그래서 언어의 외적인 면이라면 여러가지가 있지만, 여기서 말해볼 것은 교수법과 하위 호환성입니다.

3.1 가장 큰 문제

어찌보면 하스켈을 배우는 사람들이 포기하는 가장 큰 원인이라고도 볼 수 있겠습니다. 책이 너무 어렵다는 점입니다. Graham의 "Programming in Haskell"은 아예 처음부터 수학 식들이 등장하고, Bryan과 2명의 공동 저자가 쓴 "Real World Haskell"은 경이로운 난이도의 연습문제와 예제, 설명법으로 악명을 떨치는 책입니다. 국내에 하스켈 공식 입문서라고도 알려진 Hudak(및 2명의 공저자)의 "Gentle Intorduction to Haskell"은 예제가 부실하며 설명도 자세하지가 않습니다. 개인적으로 Lipovaca의 "Learn You a Haskell for Great Good!"이 가장 하스켈을 이해하기 쉬웠고, 예제도 잘 선별되었다는 느낌을 받았습니다.

제 생각으로는 널리 알려진 학습서가 그 언어의 첫인상에 많은 영향을 미치는 것 같습니다. SICP가 리스프를 어렵고 난해한 언어로 인식시키는데 많은 기여를 했듯이, 높은 난이도의 하스켈 학습서들도 하스켈이 어렵다는 인상을 더욱 강화 시킨 것으로 보입니다. 널리 알려진 하스켈 학습서들부터가 이렇게 어려워서야, 진도를 팍팍 나가지 못해서 학습 의욕이 떨어진다고 해도 어쩔 수가 없습니다.

3.2 하위 호환성

책의 난이도는, 도전적인 사람들에게는 의욕을 불태우는 동기가 될 수도 있습니다. 그리고 어떻게든 쉬운 책을 찾아서 공부하는 사람들도 있겠지요. 그런데 어렵사리 읽어온 책이 사실 시의에 맞지 않는 책이었다면? 책의 코드를 그대로 따라 쳤는데도 버전 차이로 인해 내 컴퓨터에서 동작을 하지 않는다면 허탈해하지 않을 사람이 얼마나 있을까요?

보통의 언어는 문법이나 표준 라이브러리가 그다지 자주 바뀌지 않습니다. 하위 호환성 때문입니다. 파이썬도 2.x 버전에서 3.x 버전으로 바뀌면서 많은 진통이 있었죠. 그래서 프로그래밍 언어를 설계하는 사람들은 언어의 스펙을 삭제하거나 바꾸기보다는 새로 추가하는 것을 더 선호합니다.

그러나 하스켈은 이런 면에서 좀 다릅니다. 하스켈은 하위 호환성을 깨는 것을 두려워하지 않는 언어입니다. 다시말해, 버전 차이가 극심하다는 말이지요. 더군다나 앞서 소개한 네권의 책 모두 지어진지 짧게는 10년 길게는 20년 된 책입니다. 그래서 낡은 코드들이 꽤 많습니다.

몇 몇 모듈은 하스켈 기본 모듈에서 빠졌고, 아예 삭제되거나 교체된 인터페이스도 많습니다. 몇 가지 일반적인 예시를 살펴봅시다.

  1. System.Random 모듈은 기본 모듈에서 빠졌습니다. 이제는 cabal이나 stack같은 패키지 매니저를 이용해 random패키지를 설치해야 이용할 수 있습니다.
  2. 더이상 Monad 클래스를 구현하는 것만으로는 모나드 인스턴스를 만들 수 없습니다. Functor-Applicative-Monad Proposal이 받아들여짐에 따라, 모나드 인스턴스를 만들려면 펑터와 어플리커티브 클래스를 모두 구현해야 합니다.
  3. Write, State 등 mtl 모나드들이 생성자를 직접 제공하지 않고, 대신 래퍼 함수를 제공합니다. Write 인스턴스의 값을 만들고 싶다면 Write 생성자를 호출하는 대신 write 함수를 호출해야 합니다. State모나드도 마찬가지 입니다.
  4. 에러 핸들링 방식이 과거와는 매우 많이 바뀌었습니다. 자세한 것은 매뉴얼을 참고하세요.

"Real World Haskell"로 공부하고 있다면 다음 스택오버플로우 문답이 도움이 될것입니다.

Which parts of Real World Haskell are now obsolete or considered bad practice?
In the chapter 19 of Real World Haskell a lot of the examples now fail due to the change of Control.Exception. That makes me think maybe some of the stuff in this book is actually obsolete and not...

이렇듯, 하스켈의 하위 호환성 파괴는 고질병입니다. 여기에는 물론 나름의 변명이 있기는 합니다.

모나드 괴담 슬라이드 中

그러나 변명은 변명일 뿐, 하위 호환성 파괴가 새로운 학습자들에게 걸림돌이 된다는 것은 부정할 수가 없습니다.

배우고 나서 겪는 어려움들

저는 지금 이 단계에 있습니다. 어떤 언어를 '써봤다'고 말하기 위해서는 많은 코드를 써보고 토이 프로젝트라도 하나 해보는 게 중요하지요. 그래서 어떤 사람들은 하스켈 문법을 막 익힌 사람들을 뉴비라고 부르기도 한답니다. 그러나 이런 저런 이유로 고난의 튜토리얼을 끝내고 나서 하스켈을 더이상 쳐다도 보지 않는 사람들이 있습니다.

생각보다 저와 비슷한 입장의 사람을 찾기가 쉽지 않았습니다. 하스켈을 들어본 사람부터가 생각보다 찾기 힘들었고, 기성 하스켈 유저들은 이런 뉴비의 고충을 이해하지 못하기에... 이 문단은 순전히 저의 경험을 바탕으로 한 것입니다.

1. 모나드는 여전히 어렵다

하스켈 학습 곡선 © Yuras Shumovich

하스켈의 모나드를 배우고 나면 마치 C언어에서 포인터를 막 배우고 난 것과 같은 느낌을 받습니다. 이게 중요하다고 하길래 배우긴 했는데, 어디에 쓰이는지도 모르겠고, 내가 제대로 잘 쓸 수 있을것같은 자신도 없습니다. 모나드를 제대로 이해하기 위해서 여러 글도 찾아서 읽어보고, mtl, transformer 등의 응용 패턴을 배우기 전까지는 계속해서 답답함을 느끼게 됩니다. 그리고 그것을 배웠다 하더라도, 기존의 명령형 프로그래밍 방식에서 벗어나 하스켈 식에 익숙해지려면 많은 연습이 필요합니다.

게으른 연산도 마찬가지입니다. 앞서 하스켈의 생소한 문법으로 게으른 연산을 언급했었는데, 솔직히 말해서 저 또한 게으른 연산은 아직도 익숙해지질 않습니다.

2. 이 언어가 나에게 쓸모가 있을까?

많은 사람들이 하스켈을 배우기 시작하지만, 챕터를 한 장 한 장 넘길 때마다 '이렇게 어려운 언어를 배워서 나에게 쓸모가 있을까?'라는 의문이 들며 의욕을 잃습니다. 금방 끝낼 수 있을것 같지도 않은데, '이 언어로 뭘 할 수 있을까'하는 의심만 계속해서 쌓여가기 때문입니다.

하스켈을 비롯해 많은 함수형 언어들이 산업 현장에서 외면당하고 있는 것을 우리 모두 알고 있습니다. 그렇기 때문에 '함수형이 좋다던데...'라는 이유만으로는 하스켈을 배우기에 충분한 이유가 되지 못합니다. 사실 함수형을 맛보는 정도는 다른 언어에서도 충분히 가능하기 때문이지요.

함수를 값처럼 주고받는 것을 고차 함수라고 부릅니다. 보통 주류 언어들이 함수형 프로그래밍을 지원한다고 말하는 것은 이 고차 함수를 말합니다. 자바에서는 함수 객체를 통해 고차 함수를 흉내낼 수 있습니다. 파이썬과 C++도 람다 표현식을 비롯한 장치들을 통해 고차 함수를 지원하고 있습니다. 그런데, 함수형 언어들은 보통 그게 전부가 아닙니다. 고차 함수는 하스켈을 배우는 과정의 아주 일부분에 불과합니다.

'하스켈에서 진짜 함수형이 뭔지 배워서 내 프로그래밍 언어에 적용해봐야지!'

라는 생각으로 하스켈을 배우다보면, 고차 함수를 다루는 단원에서 목적을 다 이룬것이나 다름이 없습니다. 타입 시스템이나 펑터, 모나드같은 난해하고 생소한 개념들을 배울 이유가 없는 것이지요. 그래서 많이들 그만두는것 같습니다.

물론 몇 몇 특이케이스들이 있긴 합니다. 어려운것만 보면 내것으로 만들지 못해 환x을 하는 사람들은...

환x이라니요... ©Reaction GIFs

그런 사람들에게 하스켈은 아주 좋은 장난감일겁니다. (정말로 그런 사람이 있더라니까요!)

결론

하스켈에 대해 분명히 좋은 말을 해주는 글들은 정말로 많습니다. 심플하다, 버그가 적다, 코드가 짧아진다, 등등.. 모두 맞는 말입니다. 하스켈을 배우는 것은 분명히 가치 있는 일이지만, 어려움이 있다는 것을 반드시 알아두셔야 합니다. 당장은 어디에 별 쓸모도 없는 언어기 때문에, 쉬운 마음을 먹고 시작했다가는 금방 포기할 확률이 높습니다.

이 글은 하스켈에 '데였다가' 다시 시도해보려는 사람들을 위해, 그리고 데일까봐 겁먹은 사람들에게 도움을 주기 위해서 썼습니다. 이 글에서 내내 하스켈의 어려운 부분을 강조했지만, 하스켈을 어렵다고 느끼는 것은 앞서 말했듯이 어려운 교재와 낯선 개념들의 영향이 큽니다. 정말 어려운 부분도 있기는 하지만, 급한 마음만 먹지 않으면 점점 더 많이 알게 될 겁니다.

두서없이 긴 글을 끝까지 읽어 주셔서 감사합니다. 이 글이 도움이 되셨다면 기쁘겠습니다. 하스켈을 통해서 함수형 프로그래밍을 정복할 수 있기를 바랍니다!

아래는 하스켈을 공부하는 데 있어 조그마한 동기부여가 될 수 있을까 해서 조사한 자료들입니다.

하스켈의 쓸모

하스켈도 물론 사람이 쓰라고 만든 언어기 때문에, 라이브러리같은 것들은 찾아보면 많습니다. 하스켈로 만들어진 쓸모있는 프로그램도 생각보다 많고, 적지 않은 기업에서 하스켈과 같은 함수형 언어들을 쓰고 있습니다. 그러나 하스켈의 쓸모는 이게 다가 아니지요.

Haskell in industry
Haskell has a diverse range of use commercially, from aerospace and defense, to finance, to web startups, hardware design firms and a lawnmower manufacturer. This page collects resources on the industrial use of Haskell.
Who’s using Scala?
Who's using Scala? With the blog post released recently by LinkedIn (LinkedIn is using the Play Framework), showing that they're using the Play Framework, I thought I'd take a quick look at who we know is using Scala these days.

하스켈을 배운 사람들은 현장에서 직접 하스켈을 쓰기보단 하스켈에서 배운 개념들을 다른 언어에서 응용하는 경우가 많다고 합니다. 함수형 언어를 쓰고 있는 회사가 적지 않다고 해도 여전히 많다고 보기는 어려우니까요. 하스켈(을 비롯한 함수형 언어)에서 나온 개념들이 어떻게 이용되고 있나 한 번 살펴봅시다.

리액티브 프로그래밍이 사실 모나딕 연산과 아주 닮아있다는 것을 알고 계셨나요? 실제로, 이들의 관계를 묻는 Quora 문답도 있습니다. 원한다면 여기(아래 링크)에서 보실 수 있습니다.

(현재 블로그 시스템의 문제로 인해 Quora로 향하는 링크에서 오류가 발생합니다. 다음 url을 직접 복사해 주소창에 붙여넣어 주세요: https://www.quora.com/What-is-the-relationship-between-Futures-Monads-Reactive-programming-and-Functors)
What is the relationship between Futures, Monads, Reactive programming and Functors?
Functors and monads are concepts from Category Theory, imported into functional programming. I'm not a category theorist, so I'll instead do my best to explain these in terms of what they mean for programming. As I'm also not well-versed in Scala, I'll use Haskell type signatures—but they'll hopefully be relatively straightforward to understand.

그리고 Spring 프레임워크의 리액티브 프로그래밍 기능도 함수형 프로그래밍 언어의 영향을 받았다고 공언한 바 있습니다. 여기(아래 링크)에서 보실 수 있습니다.

New in Spring 5: Functional Web Framework
As mentioned yesterday in Juergen’s blog post, the second milestone of Spring Framework 5.0 introduced a new functional web framework. In this post, I will give more information about the framework.

C++에 맞서는 신흥 강자로 떠오르고 있는 러스트의 타입 시스템도 하스켈의 영향을 받았다고 합니다.

Haskell and Rust.
FP Complete is known for our best-in-class DevOps automation tooling in addition to Haskell. We use industry standards like Kubernetes and Docker. We’re always looking for new developments in software that help us empower our clients.

이 글에서 여러번 인용한 모나드 괴담 슬라이드에 따르면, 자바 generics, C++ concept, 파이썬 list comprehension 등 다양한 언어에서 하스켈을 비롯한 함수형 언어들의 기능을 차용했다고 합니다. 특히 파이썬은 하스켈로부터 많은 영향을 받은 것으로 유명합니다.

Functional Programming HOWTO
In this document, we’ll take a tour of Python’s features suitable for implementing programs in a functional style. After an introduction to the concepts of functional programming, we’ll look at language features such as iterators and generators and relevant library modules such as itertools and functools.
Languages Arranged by Difficulty © xkcd

포스트 이미지의 저작권은 xkcd에 있음을 밝힙니다.