Haskell

main = putStrLn "Hello, world!"

1 개요

순수 함수형 프로그래밍 언어[1].
I/O와 같이 필요한 경우가 아니면 Side Effect가 없는 순수 함수로만 만들어졌다. 그러나 정작 프로그램들은 순수하지 않은 함수들만 잔뜩 써서 짜더라..

2 등장배경과 역사

하스켈은 1985년 등장한 느긋한 계산으로 작동하는 함수형 언어 미란다[2]의 뒤를 이어, 상용 프로그래밍 언어 따위가 순수 함수형 언어를 대표하는 것이 마음에 안드는 함수형 언어와 컴퓨터 아키텍처 연구 학술회에서 1987년부터 설계를 시작했다. 언어의 이름인 하스켈은 논리학자 하스켈 커리의 이름에서 가져왔다. 목표는 순수 함수형 프로그래밍 언어의 원형을 정의하는 것으로 학술조직이 주도한 언어답게 디자이너가 다 타려면 미니버스가 필요할 정도로 많고, 개발속도는 티스푼 공사만큼이나 느리다는 특징이 있었다(...). 하여간 최초 논의로부터 3년 뒤인 1990년 4월 1일[3], 1.0 버전[4]의 보고서(...)가 발표되었고, 1.1~1.4 버전을 거쳐, 1999년에 가장 널리 알려진 버전인 하스켈 98 보고서가 나왔다[5]. 2016년 시점에서 최신 정의는 하스켈 2010이다. 참고로, 하스켈 2010 역시 그리 바뀐것은 없다. (...) 2016년 4월 Haskell 2020 위원회가 출범하였다! 구체적으로 반영될 컴파일러 버전까지 언급하고있는데 이번에는 날짜를 지킬까....

3 특징

메인스트림 언어인 C/C++/Java/Python 등은 각 언어마다의 특징들을 갖고 있긴 하지만, 사실 언어적으로 과거 사용되던 언어들에 비해 큰 혁신을 가져와서 유명해진 언어들은 아니다. 컴퓨터 언어적 관점에서 혁신과 함께 어느정도 유명세를 탄 언어는 저들보다는 차라리 Fortran, Simula, Lisp, Prolog 정도를 꼽을 수 있고 Haskell 은 Purity, Lazy evaluation, Monad 등 당시 기준으로는물론 지금도 좀 극단적으로 볼 수 있는 언어 디자인으로 유명하였다.

3.1 Currying

수학/논리학자인 하스켈 커리(Haskell Curry)는 오늘날 함수형 언어에서 가장 중요한 컨셉중 하나로 볼 수 있는 Currying으로 유명한데, 이 Currying은 사실 하스켈만의 특징이라기보다는 거의 모든 함수형 언어가 공유하는 특징이기도 하다. Currying은 다인자 함수의 파라메터를 쪼개기로 생각하면 간단하다. 즉, 다변수 함수(여러 개의 값을 묶어서 한꺼번에 입력으로 받는 함수)를 일변수 함수(값을 하나씩 입력으로 받는 함수) 여러개로 쪼개는것이다. 예를들어, f(x,y)라는 함수를 g_x (y)로 바꾸는것이다. 여기서 g_x 함수는 이름처럼 고정된 게 아니라, x 값에 따라 정해진다. 따라서 g_x (y)에는 y를 받아 결과값을 돌려주는 g_x (y) 함수와, x를 받아 g_x 자체를 돌려주는 함수라는 2개의 일변수 함수 중첩되어 들어 있다.

보다 구체적인 예를 들어 보자. 3*2에서 *를 두개의 인자를 받는 함수라고 생객하고 보통 함수와 같은 방식으로 쓴다면 *(3,2)이라고 쓰면 될 것이다. 즉 * 함수가 3과 2의 쌍을 받아서 6을 돌려주게 되는 것이다. 그런데, 입력값을 받아서 그 입력값의 3배를 돌려주는 것도 함수이므로, 그 함수를 (×3)이라고 표시해 보자. 그러면 (×3)(2) 라는 표현도 가능하다. 같은 방법으로, 입력값을 받아서 그 입력값의 4배를 돌려주는 함수를 (×4)라고 표시한다면, (×4)(3) 같은 식도 가능해진다. 이런 식으로 일반화해서 입력값을 받아서 그 입력값의 m배를 돌려주는 함수를 (×m)이라고 써 보자.

그러면 *(m,n)=(×m)(n) 이라는 일반식이 성립한다. 이 식의 우변에 일변수 함수 1개가 있다고 생각하기 쉽지만, 그렇지 않다. 왜냐하면 ×는 m을 입력으로 받아서 (×m)이라는 함수를 출력으로 돌려주는 함수이기 때문이다. 따라서 좌변에는 이변수함수 *, 우변에는 일변수함수 ×와 (×m)이 있다고 보아야 한다.[6]

이런 식으로, 모든 이변수함수는 일변수함수 하나와 그 일변수함수를 결과값으로 내놓는 다른 일변수함수의 조합으로 분해가 가능하다. 이런 것을 currying이라고 하며, 타입을 이용하여 다음과 같이 생각해 볼 수도 있다.

  • (m,n)은 (int, int) -> int 라는 타입을 가지고, 이는 * 함수가 정수의 쌍을 받아서 정수를 돌려주는 함수라는 의미이다. 한편 (×m)(n)의 타입은 int -> (int -> int)가 되고, 이는 × 함수가 정수를 받아서 함수를 돌려주는 함수인데 ×가 돌려준 함수는 정수를 받아서 정수를 돌려주는 함수라는 것을 의미한다.[7]

이렇기 때문에, 하스켈에서는 그냥 * 3 2 라고 쓰고, *과 3 사이에서 끊어 읽으면 *(3,2) 가 되고 3과 2 사이에서 끊어 읽으면 (×3)(2) 가 된다는 식으로 처리한다. * 3 2 라는 표기 자체가 currying을 반영하고 있으며, 이런 currying 덕에 여러 가지 함수를 사용해서 코드를 짜면서도 코드가 간결해지고, 풍부한 의미를 담을 수도 있게 된다.[8] 덧붙여 위의 * 3 2 는 그냥 설명을 위한 예시이고, 실제 하스켈에서 *는 중위 연산자이기 때문에 3*2로 표기한다.

Currying과 연관된 집합론적 내용도 생각해 볼 수 있는데, 다음과 같다. A×B를 앞자리는 A의 원소, 뒷자리는 B의 원소인 순서쌍의 집합이라고 정의하고 D^C를 C에서 D로의 함수의 집합으로 정의하면, C^(A×B)와 (C^B)^A 사이에는 매우 적절한 일대일 대응이 존재한다. 이에서 집합의 기수 또는 농도(Cardinal number, cardinality) 의 지수법칙의 일부도 증명된다.

Currying 은 type inference 알고리듬에 의해 자동으로 수행되므로, 프로그래머가 골치썩일 필요 없이 알아서 적용된다. Currying 은 언뜻보기에 별 쓸모없어보이지만, 사실 이게 빠진 함수형 언어는 포인터 없는 C 언어라 봐도 무방할정도로 핵심적인 위치를 갖는다.그래서 언어 이름도 하스켈

3.2 순수 함수형 언어

보통 순수 함수형 언어의 가장 큰 특징으로는 부수효과(Side Effect)가 없는 것을 꼽는다. 하지만, 사실 이것은 함수형 언어보다는 순수 선언형 언어(Declarative language)의 특징에 가깝다.[9]

순수 함수형 언어에서 함수는 수학에서의 함수와 같아서 부수효과[10](Side Effect)가 없는 것을 의미한다. 즉, 순수 함수형 언어에서 함수의 결과값은 파라메터로 넘겨진 입력값에 의해서만 결정된다. 여기서 말하는 부수효과란, 같은 입력임에도 불구하고 출력이 달라지는 것을 말한다. 예를 들어 자동차를 클래스로 구현했을 때, 주어진 파라메터만큼 자동차의 속도를 증가시키고 현재 속도를 출력하는, 절차지향적으로 작성된 accelerate() 함수를 가정해 보자. 예를 들어, accelerate(10)는 자동차의 속도를 10 증가시킨다. 자동차가 정지해 있을 때 accelerate(10)이 호출되면 자동차의 속도는 10 증가하므로 현재 속도는 10이고, 따라서 현재 속도인 10이 출력된다. 이때 한 번 더 accelerate(10) 함수를 호출하면 출력값은 20이 된다. 이처럼 state를 가지는 상태머신의 경우, 출력값은 입력 뿐만 아니라 머신의 상태 역시 출력을 좌우할 수 있다. 즉, 같은 입력인데도 결과과 달라지는 것이 바로 부수효과다.

하지만 순수한 함수들로 구성된 프로그램은 부수효과가 없어 실행 순서의 영향에서 자유롭기 때문에 병행성을 가지기 좋다. 또한 루프문이 없고 제어문도 아주 적으며, 대부분의 경우 제어문을 사용할 필요가 없다. 대부분의 분기는 패턴 매칭과 가드에 의해 만들어진다.

하스켈은 UnsafePerformIO[11] 시스템 레벨로 뭐 할 때 빼고는 누구도 안 쓴다. 사실 일반적인 프로그램에서 쓰면 안되는 함수들이다.등을 제외하면 순수 함수형 언어로 규정할 수 있다.

3.3 느긋한 계산

느긋한 계산(Lazy Evaluation)은 계산이 필요한 순간까지 계산을 미루어 둔다는 의미다. 따라서 그때 그때 필요한 만큼만 계산하는 것도 가능하다. 이 느긋한 계산의 개념으로 인하여 하스켈은 프로그램에 무한의 개념을 쉽게 적용할 수 있다. 예를 들어 어떤 길이의 정수 제곱 리스트가 필요하다면 아래처럼 무한한 리스트를 만들게 해도 상관 없다.

-- 무한한 정수의 제곱 리스트를 만드는, 하스켈에서는 권장되는 방법
--(보면 알겠지만, 수학에서의 집합 { x² | x∈{1, ..} } 와 유사하다.)
square = [x^2 | x <- [1..]]

유사한 배열을 만드는 JavaScript 코드.

// 전형적인 무한루프, 하지마라
var square = []
var i = 1
while (true) {
square.push(i*i);
i += 1;
}

// ECMAScript 6의 제너레이터(Generator)를 이용한 버전
function* square_gen(){
var i = 1;
while(true){
yield i*i;
i++;
}
}
var square = square_gen();
console.log(square.next().value);
console.log(square.next().value);
console.log(square.next().value);

일반적인 프로그래밍 언어에서는 짤없이 무한 루프를 돌다 메모리가 터질 버그 코드지만[12]] 느긋한 계산 패러다임에서는 이런 경우 값이 필요해지기 전까지 계산을 미루고 있다가 값을 요청할 때 계산을 해서 보내주기 때문에 아무 문제가 없다. 오히려 이렇게 정의해두면 필요할 때 필요한 만큼 동적으로 리스트를 생성하므로 유연한 프로그래밍을 위해 필요한 경우에는 무한을 사용하는 쪽이 권장된다.

계산 말고 패턴매칭에서도 느긋한 패턴매칭이라는 개념이 있는데, 느긋한 패턴매칭이란 패턴이 안맞아도 일단 참으로 가정해서 통과시키고, 그 패턴이 실제 사용될때가 되어서야 해당 패턴의 변수에 값을 대입하는것이다. 문법적으로는 ~ 를 앞에 붙여 ~pattern 정도로 사용하지만, 몇몇 경우에는 ~ 를 따로 붙이지 않아도 암묵적으로 적용된다.

이론적으로 볼때, Simply typed lambda calculus 에서 느긋한 계산은 WHNF(Weak head normal form)이 존재할 경우, 언제나 WHNF 를 찾을 수 있는 장점이 있다. Strict evaluation 의 경우는 WHNF 가 존재하더라도 β-reduction 도중에 무한루프에 빠지는 경우가 생긴다.

3.4 강력한 타입 추정

하스켈은 함수에 입력, 출력되는 타입에 대해 엄격하다. 즉, 하스켈에서 사용되는 모든 함수는 유일한 principal type 을 가지며, 그것이 실제로 그러한지 컴파일 타임에 체크를 하고 그렇지 않을 경우, 에러를 뱉는다. 이는 컴파일 타임에 상당한 수의 버그를 잡아낼 수 있음을 의미하며, 런타임시 그것들을 체크할 필요가 없으므로 런타임 성능향상에도 이점이 있다.

그리고, 보통 이런 정적 타입 언어를 생각하면 코딩시 그런 타입을 하나하나 적어줘야 하는 짜증이 연상되게 마련이다. 하지만, 하스켈 코드를 직접 보면, 파이썬이나 Perl 같은 동적 타입 언어처럼 타입을 명시적으로 적어놓은 부분이 별로 없다. 실제로, 하스켈은 강력한 Hindley-Milner 타입 시스템을 사용하여 대부분의 경우, 타입을 명시적으로 지정하지 않아도 함수에서 사용된 데이터와 연산자 및 기타 정보를 통해 해당 함수의 타입을 자동으로 연역한다.[13] 물론, 예외상황이 있기때문에, 타입을 명시적으로 적어야만 하는 경우도 있다. 추상화 레벨을 낮추어 최적화하고 싶거나 관습적으로 논리적 버그를 막기 위해, 적을 필요가 없더라도 쓰는 경우도 있다.

3.5 타입 클래스

타입 클래스는 하스켈이 자랑하는(?) OOP의 대항마이다. (..) 이를 위해서는 우선, 다형성(Polymorphism)에 대해 알아볼 필요가 있다. 보통의 프로그래머는 다형성 하면 OOP를 떠올리고, 다형성은 OOP의 특징정도로만 알고있는 경우가 많다. 하지만, 컴퓨터 과학에서 다형성은 OOP 와 독립적인 개념이며, OOP 에서 사용되는 다형성은 Subtype polymorphism이라는, 다형성의 한 종류에 불과하다. 하스켈과 같은 함수형 언어에서는 Parametric polymorphism 이 사용되는 경우가 많은데, 이것은 Type variable이라는 개념을 넣어서, 타입 자체를 인풋으로 받아서 새로운 타입 자체를 아웃풋으로 내놓는 함수처럼 사용하는것이다.[14] 간단히 말하자면, 유저가 타입을 직접 정의할때 타입변수 a 같은것을 사용할 수 있고, 이것을 이용해서 하나의 타입선언으로 여러개의 타입을 용도에 맞게 사용가능하게끔 원소스 멀티유즈를 한다는 개념이다. 대표적인 예가 리스트이다. 실제로 하스켈에서 리스트의 타입은 타입변수 a 를 이용한 [a] 로 정의되어있으며, 저 a 에는 Int, Char, String 등을 비롯, 아무 타입이나 다 들어갈 수 있다.(즉, Universal quantifier ∀ 가 앞에 생략되어있다고 보면 된다.)

하지만, 그냥 저런식으로 쓰면 너무 광범위해서 타입체킹에 별 도움이 안되고...좀더 제한적인 룰을 가할 수 있게 만든것이 Type class 이다. 그냥 클래스가 객체들의 모임이듯이, Type class는 Type의 모임이라 보면 간단하다.[15] 예를 들어, 어떤 타입을 정의하고 싶은데, 이 타입은 적어도 타입의 원소들간 비교는 가능해야 한다면, 그런 비교가 가능한 타입들을 Ord a 로 묶어서, 이것은 a -> a 인 함수인데, 저 a 에는 비교가 가능한 타입들만 올 수 있다는식으로 제한을 가할 수 있다. 하스켈 문법으로 이것은 Ord a => a -> a 로 쓸 수 있고, '=>' 부분을 논리식에서의 implication 즉, if ~ then ~ 으로 읽는다면 'Ord a 이면 a -> a 이다' 정도로 볼 수 있다.

OOP 보다 유연한 시스템이기때문에 그 방법과 접근방식만 살짝 다를 뿐, OOP 를 네이티브 OOP 프로그래밍 언어에 가까운 수준으로 간단히 에뮬레이션 하여 사용하는것이 가능하다. 예를 들어,

class (Eq a) => Ord a where ...

는 Ord a 라는 타입 클래스를 정의하면서, 동시에 이것은 Eq a여야 함을 전제하고 있기때문에 Ord a가 Eq a의 부분집합[16]임을 말하는것이고, 이는 Ord a 클래스의 타입들은 Eq a 클래스의 모든 메소드를 사용가능하다는 것이다. 즉 OOP에서의 상속과 유사해진다.

C++17에 concept라고 거의 똑같은 개념이 추가된다. -> Coroutine, Module과 함께 취소되었다. 그렇게 Concepts 추가는 다시 한번 미뤄지게 되었다(...).

3.6 모나드

지금까지 좋은 점만 나열했는데, 그럼에도 하스켈 점유율이 바닥을 치는 이유가 있고, 그 이유는 물론 여러가지가 있을 수 있겠지만, 지금 이 항목에서 이야기하는 모나드가 커다란 부분을 차지하고 있다고 해도 과언이 아니다. 모나드는 수학의 범주론(Category theory)에서 사용되는 개념을 가져와 사용하는 것이다. 사실 하스켈을 배우던 사람 중 상당수가 모나드를 수학적으로 이해하려다 멘붕 후 접는다고 봐도 될 정도로 개념적 이해까지의 난이도가 높다. 하지만, 애시당초 수학의 모나드와는 미묘하게 다른데다 실제 사용례 위주로만 습득해서만 써도 별 문제 없으니 수학적인 개념이 이해가 안 간다고 그다지 난감해 할 것은 없다.

막연히 어렵다 하면 대중적인 관점에서 프로그래밍 언어의 최대 난적으로 불리우는 C 언어의 포인터같은 것인가 생각이 들텐데, 짧은 지문에서 모나드의 정확한 개념적 설명은 무리고, 여기서는 대충 '무엇이 어려운지' 감을 잡기 위해 범주론에서 모나드란 게 무엇인지 개념만 훑어보도록 한다.

범주(Category)는 몇가지 규칙을 만족시키는 대상(Object)와 그 대상 사이의 사상(Morphism)[17]으로 정의된다. 그런데 저 규칙이란 것이 결합법칙 적용[18], 항등사상[19]의 존재 2개밖에 없다. 이렇게 규칙이 아주 일반적이어서 사실 거의 모든 것들이 저 규칙에 해당된다. 프로그래밍 언어도 예외가 아니다. 순수 함수형 프로그래밍 언어에서 데이터 타입을 대상으로 놓고, 그 데이터 타입 사이의 함수(프로그램)들을 사상으로 놓으면 이것도 하나의 훌륭한 범주가 된다. 여기서, 다시 두개의 범주 사이의 구조를 생각해볼 수 있는데, 범주론에서는 몇가지 규칙을 만족시키는 저런 구조를 함자(Functor)라 한다[20]. 이 규칙 역시 아주 관대해서 함수형 언어들에서 스탠다드로 사용되는 map 같이 함수 자체를 인풋으로 받는 고차함수(higher-order function) 몇몇이 이에 해당된다고 볼 수 있다. 여기서, 다시 두개의 함자 사이의 변환을 생각해볼 수 있겠는데, 범주론에서는 몇가지 조건을 만족하는 저런 변환을 자연 변환(Natural transformation)이라 한다. 이 조건 역시 상당히 관대한데 unit 같은 함수가 이런 조건을 만족시킨다. 여기서 다시 저 자연 변환이 관련된 몇가지 조건을 만족하는 함자 쌍을 수반함자(Adjoint functor)라 하는데, 모나드(Monad)는 바로 이 수반함자쌍의 Composition 이다. 여기까지 오면 뭔소린지는 몰라도 뭐가 어려운건지 대충 이해는 갈 것이다. 그냥 함수의 함수의 함수의... 식으로 order 가 올라가고, order 가 올라갈 때마다 조건이 붙고...최종적으로 그래서 그게 대체 무엇인가 오리무중에 빠지는 난점이 있다.

다행히, 함수형 언어에서 사용되는 모나드 개념은 범주론을 적극적으로 사용하기보다는 아이디어만 빌려온 수준이며, 때문에 이런 쪽 전공이 아니라면 쓸데없이 시간만 잡아먹을 수학적 이해는 그냥 포기하고 사용례로 실용적인 부분만 습득하는 것도 적극적으로 권장된다.

실용적인 관점에서 모나드는 모나드의 타입 인스턴스에 따라 여러가지 용도로 쓰일 수 있는데[21], 하스켈에서 모나드의 가장 중요한 용도는 사이드 이펙트가 있는 Impure function을 다루는 용도이다. 쉽게 말해, 모나드는 순수 함수형 언어인 하스켈 내에서 명령형 언어를 에뮬레이션 하기 위한 목적으로 많이 쓰인다고 보면 된다.

즉, 하스켈은 순수 함수형 언어이면서 동시에 모나드를 이용해 정의되는 명령형 언어라는 서브언어를 내부에 갖고있는셈이다. 그렇다면 반대로, C++ 이나 파이썬같이 명령형 언어이면서 함수형 표현을 지원하는 언어와 다른 것은 무엇인가...생각이 들법도 한데, 모나드를 이용해 하스켈 내부에 정의되는 명령형 언어 시스템은 순수성(Purity)를 훼손하지 않아 참조 투명성(Referential transparency)가 유지되며 동시에 명령형 언어 부분과 함수형 언어 부분이 엄격하게 구분된다. 일반적으로, 명령형 언어 베이스에 함수형 표현을 추가한 언어들은 말그대로 저런 '표현'정도를 지원하는 정도라, 저런 구분이 존재하지 않고 섞이는 경향이 있다.

사실 모나드는 함수형 패러다임을 수용하는 언어라면 모두 이를 구현할 수 있다. 하스켈에서도 모나드는 언어 레벨에서 구현한 구조가 아니라, 수많은 타입 클래스 구현 중 평범하고(?) 조그마한(!) 하나이다.[22] 다만 하스켈에서는 이를 보다 적극적으로 언어의 특징으로 내세워서 적용하고 있을 뿐이다. 그 덕에 많은 사람들이 멘붕을 하고 있기는 하지만... 여담으로 모나드를 실용적으로 구현하여 사용하는 대표적인 분야가 바로 JavaScript로 작성된 여러 프레임웍이나 라이브러리 중에서 ajax를 처리하는 부분이다. 여기에 모나드를 적극 적용하는 이유는 다른 것이 아닌, 이것을 적용하면 노가다 양이 줄고, 사용하기 편하며, 보기도 좋기 때문. 그러나 대다수의 웹 프론트엔드 개발자는 이 사실을 모르고 있다.

4 수학과의 관련성

위의 내용을 읽어 보았다면 알겠지만 각종 수학 이론을 이용해서 언어를 만들어 보자는 의도로 만들어진 언어이므로 수학과의 관련성은 당연히 크다.

프로그래밍을 수학, 특히 수리논리에서 배운사람은 보통 λ-calculus 혹은 class of recursive functions 라는 함수체계를 통해 배운다. 당연히 이 시스템도 사이드이펙트는 전혀 없으며[23] 심지어 built-in 조건문이나 루프문도 없다. 하지만 수학답게(?) 여러가지 트릭으로 존재하는 알고리듬은 모두 표현이 가능하다. 하스켈에도 λ-calculus가 어느 정도 반영되어 있으며, 아예 λ식이라는 개념도 있다.[24] 함수형 언어 하면 재귀부터 떠올리게 마련이고, 재귀에만 어느정도 익숙해지면 문제가 없을것같은 상상을 하는 경우가 많은데, 재귀보다 마스터하기 어려운것이 λ-calculus 의 η-reduction 과 partial application 을 이용한 여러가지 트릭들이다. 보통 함수들을 정의할때 x y 등의 파라메터를 사용하는데 η-reduction 과 partial application 을 사용하면 많은 경우 저런 파라메터를 사용하지 않고(point-free) 함수기호만 갖고 함수를 정의하는게 가능해진다. 또한, (익숙한 사람들에 한해서)상당히 직관적이며 코드길이도 괴상할정도로 축약되는 경우가 많아 유용성도 꽤 되는편이지만, 익숙하지 않은 프로그래머의 눈에는 거의 암호문이 되어버리므로, 함수형 언어를 사용하려면 제대로 알아두는것이 좋다.

하스켈에서 사용하는 타입 시스템은 inductive structure[25] 를 갖으므로, 수학에서 사용되는 type theory 와 보다 유사하다. 하스켈의 끝판왕이라 불리는 monad 역시 수학의 category theory 에서 가져왔으며, Co- 로 시작되는 여러가지 개념들도 역시 수학에서 쓰이는것들이다.[26] 그리고, 가장 중요한 함수형 구조 자체가 위에 언급한 수학에서 알고리듬을 표현할때 자주 쓰인다. 덕분에 수학(수리논리)와 친근하다면 함수형 언어가 결코 낯설지 않을것이나, 모른다면 익숙해지기까지 진입장벽도 은근히 높은편이고 언어를 깊게 이해하기도 상당히 힘들어진다. 다만, 언어 자체에 대한 깊은 이해가 없더라도 프로그래밍을 하기에 큰 무리는 없으므로 부담을 갖지는 말자. 애초에 C++ 이나 Java 같은곳에 사용되는 컨셉들도 학적으로 깊게 들어가면 수학만큼은 아니더라도 충분히 복잡하다. 하지만, 프로그램을 만들려는 목적으론 그걸 알 필요도 없고, 그걸 안다고 더 뛰어난 결과물이 나오는것도 아니다.[27] 물론, 여기에는 각 언어 제작자들이 최대한 관련지식 없이 직관적으로 프로그래밍이 가능하도록 디자인을 하는것도 한몫 한다. 하스켈 역시 저런 이론이 아니라, 함수형 표현이나 재귀등에 익숙해지는것이 관건.

정리하자면, 컴퓨터의 탄생에는 20세기 초반의 수리논리학의 개념들이 영향을 주었는데, 하스켈은 더 나아가서 20세기 후반의 수학 및 수리논리학의 개념들까지 이용하고 있다고 보면 된다.

5 안써요

scaled_full_2d13e60a05d16f00e1d5.jpg
[28]

haskell.png

Haskell로 쓴 코드는 side effect(부작용)이 없어.

...아무도 안 쓸테니까?

인기는 두말할 것도 없이 바닥이다. 표현력이 좋고, 코드가 간결하며, 알고리즘의 구현이 직관적이고, 부수효과가 없고, 동시성이 보장되는건 좋은데 애초에 함수형 언어라는 놈이 일반적인 프로그래밍 언어와 체계가 너무나 달라서 쉽게 익숙해지기도 어렵고 현실적으로 사용하기 어려운 점이 많다. 그 결과로 시도하는 사람 자체가 적어서 잘 아는 사람이 많지 않으니 사용 예가 적고, 사용 예가 적으니 배우려는 사람이 적다, 의 무한 반복. 당장 게중에 쉬운편에 속하는 위의 무한 제곱 리스트 코드도 LISPErlang을 공부한 사람이 아닌 이상 별다른 학습 없이 이해하기 어렵다. 그나마 LISP가 함수형 언어 중에서 비중이 크다 보니 상당히 밀린다.

오래된(?) 프로그래밍 격언중에 You can write Fortran in any language라는 게 있다. 포트란은 최초의 (명령형)하이레벨 언어로 여겨지는 프로그래밍 언어인데, 오늘날 세대에게는 보통 C 언어가 저 위치에 들어간다. 말하자면, 엄청나게 많은 기능을 탑재해서 거의 새로운 언어라고 봐도 될정도인 C++ 에서도 그런거 깡무시하거나 아주 기본적인 구색만 맞춰놓고 실상은 C 식으로 코딩을 하는 사람이 많고, 역시 마찬가지로 큰 혁신을 가져왔던 자바에서도 그냥 C 식으로 코딩을 하는 프로그래머가 많다는 소리다. 바꿔말하면, 대부분의 프로그래머들은 하스켈이나 기타 다른 패러다임의, C 식으로 코딩이 불가능한 언어는 거들떠도 보지 않는다는 소리. 사실 여러가지 다 고려하면서 '제대로' 프로그래밍 하는것과 대충 돌아가게만 하는것과의 난이도 차이는 상상을 초월할정도로 크지만, 대부분 오너의 눈에는 들어가는 비용만 크게 보이고 실제 프로그램은 그냥 돌아가기만 하면 별반 차이가 없어보이니 당연한 결과라 하겠다.

또한, 언어 자체에 대한 비판도 있는데, 역시 함수형 언어인 SML의 저자인 Robert Harper 가 정면으로 나서 비판을 하였다. Laziness와 type class 시스템에 대한 비판인데, 흥미가 있는사람은 직접 읽어보자. laziness type class. 다만, 이사람은 CS 교수이면서 관심분야는 proof theory, category theory 등 수리논리쪽으로 쏠려있어, 수리논리 전공으로 꽤 지식을 갖고있어야 이해가 가능하다는게 함정.[29]
구미권에서도 인기는 바닥을 긴다. 게다가 함수형 언어에서 자주 사용되는 재귀문은 루프문에 비해 코드를 짧고 간결하게 만들어 가독성을 높여주는 장점이 있지만, 그 재귀코드를 짜는 것은 상당한 두뇌노동을 필요로 하는 단점도 있다.(수학에서 아름답고 간단한 공식일수록 탄생하는 과정(증명)은 괴로운 경우가 많은것과도 어느정도 상통한다.) 이도 물론, 재귀코드에 많이 익숙해지면 어느정도 해결될 문제기는 하지만 아무리 그래도 일반적인 Procedural language 처럼 아무생각없이 일단 닥치고 써내려가고 실행-디버깅으로 완성하는 방식은 불가능하다. 사실 서로 재귀(Mutual recursion)와 간접 재귀(Indirect recursion)를 자연스럽게 생각하며 쓸 수 있다면, 그냥 써내려가면서 그때그때 테스트 돌리며 프로그램을 짤 수 있다. 그게 가능하게 뇌개조를 시키는 것이 문제일 뿐. 혹자는 프로그램 구조를 완전히 머리속에 디자인한 후에야 코딩이 가능한 재귀코드가 오히려 프로그래밍 습관에 도움이 된다고 하지만, 이미 C/C++/자바등에 익숙한 사람들에게는 오히려 커다란 진입장벽이 될뿐이다. 그나마 코드에 수학적인 요소가 많고, 코어는 넘쳐나는 High Frequency Trading Firm 같은 금융계에서 소규모적으로 쓰는 경우가 있는 정도. 그 외에 프로페셔널한 목적보다는 프로그래밍 자체를 좋아하는 애호가 혹은 C/C++/Java 등을 메인언어로 사용하는 프로그래머들이 함수형 언어 혹은 함수형 패러다임에 대한 컨셉을 이해하기 위해 자주 선택하는 언어중 하나. 수학과 관련성이 높기때문에 논리학과 프로그래밍의 상관관계 및 타 언어에 비해 흥미로운 백그라운드 이론들이 많이 존재하며 재미있는 트릭도 많이 적용가능하고, 수학적인 관점에서 보다 아름다운(?) 코드를 만드는것이 가능하기때문이다. 무엇보다 현재 등장한 메이저 함수형 언어중에선 가장 엄격하게 함수형 패러다임을 따르는 언어이기때문에[30] 좀 색다른것을 추구하는 프로그래머들도 많이 찾는다.

6 하스켈로 만들어진 프로그램들

당연하겠지만, 윈도우보다는 프로그래밍 애호가들의 장인 유닉스쪽에서 많이 사용된다. 대표적인것은 darcs 라는 버전 컨트롤 시스템과 xmonad 라는 타일링 윈도우 매니저이다.

xmonad는 dwm 의 하스켈 버전이다. dwm 의 특징은 1000 줄 미만의 단일 C 프로그램으로, 세팅도 헤더파일을 변경하는식으로 이루어지는 하드코어 윈도우 매니저인데, xmonad 도 역시 하스켈 소스파일을 직접 변경하는식으로 세팅이 이루어지는데, 하스켈이라는 고생산성 언어를 사용한덕에 dwm 에 비해 지원하는 기능이 훨씬 많다. 다만, 사용하기 위해서는 200 메가가 넘는 ghc 라는 의존이 딸려오기때문에 꺼려하는 사람들도 종종 있는편. xmonad 의 코어는 매우 주의깊게 디자인되었고, Coq 라는 Theorem prover(수학적 증명 보조 프로그램, 4색정리도 이 프로그램의 도움으로 증명되었다.)까지 사용하여 검증하는것으로 유명하다.

darcs 는 하스켈 프로그램답게 구조와 디자인면에서는 많은 찬사를 받는 버전 컨트롤 시스템이다. 하지만, 퍼포먼스에서 C 로 짜여진 git 에 비해 많이 밀리기때문에 현재는 알다시피 git 에 거의 점령당한 상태. 하스켈 컴파일러인 GHC조차 매우 거대해진 몸집을 감당하지 못해 2011년 darcs에서 git으로 넘어갔다. 새로운 이론적 배경·디자인을 가진 Camp가 준비되고 있다. Camp는 아직 희망이 보이지 않고, 그에 반해 darcs 는 [1] 에서 볼 수 있는 것처럼 지속적으로 퍼포먼스가 개선되고 있다. git 에게 밀리는 것은 darcs는 github 만큼 훌륭한 접근성을 제공하는 open source server 가 없기 때문(...)이다.

문서 상호 변환 프로그램으로 프로그램/라이브러리로서 Pandoc이 있다. 매우 널리 쓰이는 프로그램이지만, 주로 backend로 사용되기 때문에 일반 유저는 그런게 있는지는도 모른다. 소스를 직접 compile하다보면 가끔 요구되는 것을 볼 수 있다.

Haskell로 만든 운영체제로 House가 있다. 함수형 언어로 시스템 프로그래밍을 하는 일종의 도전과제 비슷한 개념이지만 꽤 잘 작동한다. 관심있으신 분은 받아서 분석해보는 것도 괜찮을듯. 다만 현재의 컴퓨터 구조로는 아직 함수형 언어의 장점과 성능을 살리기 어렵기에 느려터진 것은 감수해야 한다.

2015년에 공개된 오픈소스 프로젝트로는 facebook에서 공개한 스팸 필터[31]인 Haxl가 있다. 개발 책임자의 말을 빌면, 기존의 C++베이스로 작업하던 FXL[32]이라는 물건으로는 물밀듯이 밀려오는 스팸을 도저히 감당할 수 없어서 포기하고 하스켈로 개발을 시작했다고. 결과적으로 실제 상황에서 20~30%정도의 성능 향상을 얻을 수 있었고, 특히 복잡한 문제일수록 더 빨라졌다고[33]. 다만 하스켈 개발자는 구하기 어려워서, 언젠가 Haxl을 C++로 포팅하는 수순을 밟을지도 모른다고. 실제로 이번 시스템 개발과정에만 하스켈 언어·컴파일러 톱 디자이너만 두명이나 참가했다. (...) 인터뷰 # # 하스켈로 뭘 했는지 간단히 살펴볼 수 있는 슬라이드

하스캘 전용 IDE인 leksah[34] 또한 하스캘로 개발됐다.주로 텍스트파일을 작성하고 ghc로 컴파일하는 번거로움이라곤 하지만 테스트할 땐 :l 하나면 끝이다을 일반 개발 환경처럼 편하게 해준다.

7 기타

2009년에 나온 번역서 초판 1쇄를 아직도 새책으로 살 수 있을 정도로 안습하다. 참고로 이 책의 역자 중 한명은 카이스트 출신이다. 그리고 아라의 유명인이다.

하스켈 스크립트는 GHC로 완전히 기계어 컴파일을 할 수 있고 GHCi의 가상기계 인터프리터로도 작동 가능하다. 또한 스크립트 중 .lhs라는 확장자는 일반 텍스트가 메인이고 코드를 별도로 처리해서 작성했다가 컴파일 하는 방식이다. 기본적으로 뭘 만들어도 코드 자체는 상대적으로 짧게 만들어지기 때문에 주석이 메인인 코드 형식이 생긴 것이다. 다만 haddock을 통한 문서화가 주류가 되면서 .lhs 는 예제코드를 만들기 위한 용도 정도로나 사용되고 있다.

하스켈을 배울 수 있는 웹사이트로는 [2][3] 등이 있다. 다만 이곳의 내용은 프로그래밍 경험이 있는 사람을 위하여 쓰여 있다.
  1. 정확히는 일반적으론 쓰면 안되는 함수들을 제외하면 순수하다.
  2. 상업적으로 어느정도 성공을 거두었다.
  3. 일부러 만우절을 골라서 발표했다. 그 이후로도 하스켈 98 보고서나 관련된 책들을 만우절에 발표하곤 했다
  4. 당시에는 IO 처리에 악명높은(?) 모나드 대신 Stream model이 사용되었다.
  5. 2003년, 하스켈 98 보고서의 개정판이 나왔지만 개념적으로 바뀐 사항은 없고, 일부 작은 오류에 대한 수정과 애매한 표현등을 다듬은 정도였다. 즉 언어의 개정판이 아니라 어디까지나 보고서의 개정판. 논문 5p를 참조
  6. 그래서 수학적으로 엄격하게는 *(m,n)=(×(m))(n) 이 맞다. 그렇지만 가독성 문제가 있어 생략.
  7. 이처럼, 함수의 출력이 함수가 되는 것을 얼마나 적절히 활용할 수 있느냐가 하스켈로 얼마나 괜찮은 코드를 짤 수 있는가를 말해주는 한 요소가 된다.
  8. 여러 개의 함수를 사용하면서 f(x), g(x,y)와 같은 방법으로 쓴다면 앞 주석의 (×(m))(n) 처럼 괄호가 한없이 불어나게 마련인데, currying 덕에 괄호를 최대한으로 생략하면서도 코딩할 수 있게 된다. 따라서 여러 개의 함수를 사용하더라도 덜 복잡하게 보이고, 따라서 여러 가지 함수 여러 개를 쓰고 싶은 만큼 마음껏 쓰는 것도 할 만해진다.
  9. 함수형 언어는 순수 선언형 언어의 하위 카테고리라 볼 수 있다.
  10. 또는 부작용
  11. 하스켈 위키에서는 아예 'Dark side of IO monad' 단락에 포함시켜 놨다(...).
  12. Python의 yield같은 함수는 예외
  13. 이 기능은 C++11 에도 auto 라는 키워드로 탑재되었다.
  14. 이 개념은 상당히 강력해서 템플릿이라든가 해서 C++을 포함한 다양한 프로그래밍 언어들에 전파되었다. 대표적인 용례가 일반적 프로그래밍의 꽃이라고 불리는 STL이다.
  15. 수학적으로 모임(class)은 집합의 상위개념이다. 수학에서 모든 집합의 집합같은 것은 비문이지만, 모든 집합의 클래스는 말이 된다.
  16. 물론 엄밀히는 부분 모임, 즉 서브클래스(subclass)가 된다.
  17. 함수를 추상화한 것이 사상이므로 그냥 함수라고 생각해도 된다. 정확히는 집합들을 대상으로 하는 사상이 함수이다.
  18. 즉 부작용(Side-effect)가 없으면 성립한다.
  19. 역시 항등함수(입력값을 그대로 돌려주는 함수)로 생각해도 좋다.
  20. 하스켈 언어에도 Functor 타입 클래스가 존재한다.
  21. Maybe 모나드는 오류를 처리한다든가, 리스트 모나드는 비결정성을 다룬다든가
  22. 실제로 모나드와 그 메소드를 정의하는 코드는 코멘트를 제외하면 8줄, 100글자도 안된다. 물론 Functor, Applicative등을 계승하고 있지만, 그 또한 모나드만을 위해서 만들어진 것도 아니니 과장은 아니다. 게다가 그것들의 정의 또한 매우 짧다.이 8줄 정도밖에 안되는 정의를 이해하지 못해서 많은 사람들이 죄절하고 있는 것이다. 읽어보면 당연한 내용만 정의되어있고, 거기에다가 이해를 돕기위해 주석이 코드의 4배 이상이다.
  23. 말그대로 수학의 함수이다.
  24. 사실 하스켈뿐 아니라, 모든 함수형 언어의 이론적 기반이 λ-calculus 라고 봐도 과언은 아니다.
  25. 수학적 귀납법을 떠올리면 간단하다. 베이스가 되는 객체들이 있고, 그 객체들을 이용하여 A, B 가 타입이면 A->B 도 타입이다같은 정의를 주면, A 가 타입이면 A->A 가 타입이고, A->A 가 타입이면 (A->A)->A 혹은 (A->A)->(A->A) 가 타입이고...하는식으로 죽죽 나간다. 이런 정의를 inductive definition 이라 칭하며, 이런 시스템은 inductive structure 라고 한다. 가장 대표적인 예시는 당연히 자연수이다.
  26. Co- 로 시작되는것들은 mathematical dual 이라 하며, '대비되는 개념'정도로 이해할 수 있다. 예를들어, 전체집합 X 가 있을때, 그 부분집합인 {x} 의 Coset 은 전체집합 X 에서 x 를 제외한 X\{x} 가 된다. 물론, 이것은 간단한 예시이고, data 의 dual 인 Codata, 재귀(Recursion)의 dual 인 Corecursion, 귀납의 dual 인 Coinduction 등 직관적으로 이해하기 힘든것들도 많다. dual 을 구하는 방법은 그에 대한 정확한 수학적 정의를 논리식으로 풀어서 변환하는 방법이나 Category theory 로 정의한 후, arrow 의 방향을 바꿔서 얻어지는경우가 많다.
  27. 말하자면 완전히 다르다. 수학(수리논리)에서는 알고리듬 '시스템'에 대한 증명 위주로 배우기때문에 시스템 내에서의 구체적인 알고리듬 관련해서는 온갖 복잡한 증명으로 다져진 논리력을 제외하면 사실 쌩초보와 크게 다를것이 없다. 사실 계산력도 딸리는 사람이 많다. 고교 졸업후 대학 초반을 제외하면 구체적인 계산은 거의 등장하질 않는다. 즉, 알고리듬 시스템에 대한 연구가 아닌 알고리듬을 만드는것 관련해서는 고교수학까지 배운 내용만 확실하다면 스타트 라인은 동일하다.
  28. Haskell팬들에게 Java는 덩치만 크고 무너지기 쉽고, C는 시한폭탄 같고, PHP는 사고가 터지면 대형참사급(..)이라고 인식한다. 반면에 하스켈은 전체적으로 인식이 아인슈타인(..)LISP는 뭐
  29. 사실, CS 자체를 응용수리논리학으로 보는 사람들도 있을정도로 한걸음만 넘어가면 부딪히는 의외로 가까운 분야이다.
  30. 인더스트리쪽을 중시하는 타 함수형 언어들은 imperative/oop 등도 첨가하여 묘하게 짬뽕된 경향이 강한데 반해, 하스켈은 보다 순수한 아카데믹 성향이 강하다.
  31. 공개된 부분은 필터의 일부인 데이터 액세스 라이브러리. 스팸 필터 중에서 가장 부하가 큰 부분이라고. 당연하지만 스팸 필터 그 자체는 공개 안했다.
  32. 2013년부터 사용되었음. 이 또한 순수 함수형 언어의 특징을 따온 DSL이었다. 하루에도 몇번이나 코드가 갱신되기 때문에 인터프리팅을 통해서 실행하고 있었지만 너무 느렸다고. 대신 컴파일해서 실행·교체(hot code swapping: CPU나 하드디스크를 실행중에 바꿔끼는 묘기 hot swapping의 소프트웨어 버전)할 수 있는 하스켈에는 상대가 되지 않았다고.
  33. 공개된 정보에 따르면 x3 이상
  34. haskel을 반대로 쓴 것.