하스켈 학교

1990년대 후반부터 많은 대학들이 프로그래밍 입문에 코스에 자바를 채택하기 시작하였습니다. 2001년 텍사스 오스틴 대학도 프로그래밍 입문 코스에 기존에 사용하던 언어인 Haskell을 자바로 대체하는 방안을 검토 중이었습니다. 당시 이 대학에 재직 중이던 저명한 컴퓨터 과학자 다익스트라(Edsger W. Dijkstra)는 공개 서한을 통해 이런 움직임을 비판하며 Haskell의 장점을 강조하였습니다.

하지만 텍사스 오스틴 대학은 결국 상업적으로 성공한 자바를 채택하였고, 이후 MIT를 비록한 많은 대학들이 Scheme, ML, Haskell 같은 함수 언어 대신 Java나 Python을 가르치기 시작하여 그 추세는 지금까지도 이어지고 있습니다.

저도 대학에서 C, C++, Java를 배웠고 졸업 후에도 10년 이상을 C, C++, JavaScript를 사용해 코드를 작성하였습니다. 자바가상머신(JVM), 웹킷(WebKit) 등의 프로젝트에 참여하며 복잡한 소프트웨어를 작성하기 위한 객체지향프로그래밍 방법론, 디자인 패턴, 테스트 케이스 작성 방법 등도 배웠습니다.

하지만 동시에 한계도 느꼈습니다. 어제 작성한 코드가 오늘 작성한 코드와 크게 다르지 않고 오늘 발생한 버그가 지난 번에 해결한 버그와 크게 다르지 않았습니다. 프로그래밍은 점점 기계적인 활동이 되었고 흥미도 잃게 되었습니다. 코드 재활용이나 쉬운 유지보수는 영원히 달성할 수 없는 이상향으로만 느껴지기도 했습니다.

그러던 와중에 지푸라기라도 잡는 심정으로 학부 때 잠깐 공부했던 Haskell을 다시 꺼내서 공부하기 시작하였습니다. 그리고 프로그래밍에 대해 다시 생각하게 되었습니다. 함수 합성(function composition)을 통한 코드 재활용, 지연 연산(lazy evaluation)을 통한 모듈화, 펑터(functor)와 모나드(monad) 등 카테고리 이론을 이용한 추상화 등은 그간 프로그래밍에 대해 가졌던 여러 고정관념들을 허물고 익숙한 코드를 낯선 관점에서 새롭게 바라볼 수 있는 시각을 제공해 주었습니다.

흔히 Haskell은 배우기 어려운 언어라고 합니다. 저도 꽤 오랜 시간 많은 시행착오를 하며 람다 대수, 타입 이론, 카테고리 이론 등 Haskel 이해에 필요한 이론들을 공부하였습니다. 하지만 돌이켜 생각해보면 Haskell은 단순히 어려운 언어가 아니라 “다른” 언어였다는 생각이 듭니다. 특히, 이미 C++이나 Java 같은 언어로 오랜 시간 프로그래밍을 해왔다면 Haskell이 더욱 다르고 낯설게 느껴질 수 있습니다.

국내에는 Haskell 관련 자료가 별로 없습니다. Haskell을 주된 프로그래밍 언어로 사용하는 회사도 없습니다. 그래서 Haskell은 배우기 어려운 언어라는 악순환이 반복되고 있습니다. 하지만 많은 분들이 Haskell을 배우고 싶어합니다. 대체 무엇이 있는지 알 수 없지만, 뭔가 흥미로운 게 있을 것 같다는 기대 때문입니다. 이 사이트가 Haskell 세상으로 탐험을 나서는 분들에게 미약하나마 작은 도움이 되어드릴 수 있으면 좋겠습니다.

하스켈 “Hello World” 프로그램

프로그래밍 언어 입문서는 화면에 “Hello World”를 출력하는 프로그램을 보여주는 것으로 설명을 시작하는 전통이 있습니다. 이 전통에 따라 하스켈 “Hello World” 프로그램을 소개하겠습니다.

main = putStrLn "Hello World!"

이 프로그램을 실행하려면 먼저 코드를 복사해서 HelloWorld.hs 파일을 만듭니다. 그런 다음에 runhaskell 명령을 이용해 프로그램을 실행합니다. runhaskell은 GHC를 설치하면 함께 설치되는 프로그램으로 인자로 주어진 하스켈 프로그램을 곧바로 실행시켜 주기 때문에 하스켈을 스크립트 언어로 쓸 수 있게 해줍니다.

$ runhaskell HelloWorld.hs
Hello World!

ghc 컴파일러를 이용해서 실행 파일을 생성하는 것도 가능합니다.

$ ghc --make HelloWorld.hs

컴파일 후에 HelloWorld(윈도에서는 HelloWorld.exe)라는 실행 파일이 생성된 것을 확인할 수 있습니다. 실행하면 runhaskell HelloWorld.hs를 했을 때와 마찬가지로 화면에 “Hello World!”를 출력하는 것을 보실 수 있습니다.

$ ./HelloWorld
Hello World!

추가로 같은 디렉토리에 HelloWorld.hiHelloWorld.o 파일도 생성된 것을 볼 수 있는데, .hs 파일은 각 모듈을 따로 컴파일(separate compilation)을 하기 위한 인터페이스 파일이고, .o 파일은 해당 모듈을 컴파일한 오브젝트 파일입니다. 이에 대해서는 하스켈 모듈 시스템을 설명할 때 좀 더 자세히 설명하겠습니다.

이제 코드 내용을 살펴보겠습니다. 우선 main은 하스켈 코드가 실행되는 시작점(entry point)입니다. 모든 하스켈 프로그램은 main에서 시작해서 main으로 끝나게 됩니다.

ghci을 이용해서 main을 타입을 확인하면 다음과 같습니다. 참고로 :l 명령을 이용하면 외부 파일을 ghci로 로드할 수 있습니다. HelloWorld를 인자로 주면 HelloWorld.hs파일을 컴파일하여 ghci로 로드합니다.

$ ghci
> :l HelloWorld
[1 of 1] Compiling Main             ( HelloWorld.hs, interpreted )
Ok, modules loaded: Main.
> :t main
main :: IO ()

main의 타입은 IO ()라는 것을 알 수 있습니다. 여기서 () 타입은 유닛(unit) 타입으로()가 유일한 값입니다. 주로 유용한 리턴 값이 없을 때 사용하는 타입인데, C++이나 Java의 void와 유사한 역할을 합니다. 다만, 하스켈의 () 타입은 void와 달리 리턴 값이 없는 것이 아니라 () 타입의 유일한 값인 ()을 리턴합니다. 타입과 값 모두 ()이라는 심볼을 사용하는 것에 유의하시기 바랍니다.

> () -- 여기서 ()는 값입니다.
()
> :t () -- 여기서 ()는 타입입니다.
() :: ()

main의 리턴 값은 ()이 아니라 IO ()인데, 여기서 IO는 사이드 이펙트가 있는 함수라는 뜻입니다. 쉽게 말해, 화면에 입출력을 할 수 있는 함수는 ()가 아닌 IO () 타입으로 표시합니다. IO 타입에 대한 자세한 설명은 입출력(IO)에 대한 설명을 할 때 자세히 이야기하겠습니다.

putStrLn의 타입은 String -> IO ()입니다. 타입에 ->가 포함되어 있으므로 함수임을 알 수 있습니다. 인자 타입은 String이고 리턴 타입은 IO ()입니다. putStrLn은 문자열을 하나 받아서 화면에 출력하는 함수입니다. 화면에 문자열을 출력하는 것도 사이드 이펙트이기 때문에 리턴 타입이 ()가 아닌 IO ()임을 알 수 있습니다.

> :t putStrLn
putStrLn :: String -> IO ()

putStrLn "Hello World!"의 타입은 String -> IO () 타입 함수에 String 인자를 호출하였기 때문에 putStrLn의 리턴 타입인 IO ()가 됩니다. main의 타입인 IO ()putStrLn "Hello World!"의 타입이 같기 때문에 이 프로그램을 컴파일 에러 없이 정상적으로 컴파일이 됩니다.

하스켈 시작하기

설치

하스켈을 설치하는 가장 쉬운 방법은 하스켈 플랫폼(Haskell Platform)을 이용하는 것입니다. 하스켓 플랫폼은 하스켈 컴파일러 GHC, 빌드 시스템 Cabal, 자주 사용되는 35개의 하스켈 패키지를 한 번에 설치할 수 있게 도와주는 하스켈 배포판이라고 생각하시면 됩니다. Mac OS X, Windows, Linux를 모두 지원합니다.

GHC와 GHCi

ghc는 하스켈 컴파일러입니다. 다음과 같이 --version 옵션을 주고 실행했을 때 버전 정보가 출력되면 정상적으로 설치된 것입니다.

$ ghc --version
The Glorious Glasgow Haskell Compilation System, version 7.10.3

추가로 ghci라는 REPL도 함께 설치되었을 겁니다. 여기서 REPL은 Read–eval–print loop의 약자로 프로그램 코드를 입력하면 수행 결과를 화면에 보여주는 프로그램을 통칭하는 말입니다.

쉘에서 ghci 명령을 입력하면 다음과 같이 Prelude>라는 프롬프트가 뜨는 것을 확인하실 수 있습니다.

$ ghci
GHCi, version 7.10.3: http://www.haskell.org/ghc/  😕 for help
Prelude>

프롬프트에 간단한 하스켈 프로그램 1+2를 입력하면 계산 결과인 3이 화면에 표시됩니다.

Prelude> 1 + 2
3
Prelude>

이제 하스켈 프로그래밍을 시작할 준비가 완료되었습니다.

하스켈 함수

함수 언어

하스켈은 함수 언어입니다. 함수 언어의 대표적인 특징은 함수를 다른 함수에 인자로 넘기거나 함수가 다른 함수를 리턴하는 일이 가능하다는 것입니다. 이런 함수를 고차 함수(higher order function)라고 부릅니다. 이 글에서는 이런 특징을 가진 하스켈의 함수에 대해 살펴봅니다.

함수 호출

하스켈에서 함수 호출(function application)은 f a입니다. 여기서 f는 함수, a는 인자를 의미합니다. 함수 호출이 빈도가 높기 때문에 가장 간결한 문법을 사용하고 있습니다. 명령형 언어가 대개 f(a)와 같이 괄호를 사용하는 것과는 대비됩니다.

간단한 함수 호출의 예는 아래와 같습니다. isLower 함수는 인자 'a'를 입력으로 받아True를 값으로 돌려줍니다. 인자 'A'을 입력하면 False를 값으로 돌려줍니다. isLower함수는 Data.Char 모듈에 선언되어 있기 때문에 함수를 사용하기 위해서는 먼저 import구문을 사용해 임포트를 해주어야 합니다.

> import Data.Char
> isLower 'a' 
True
> isLower 'A'
False

또 다른 예로 fstsnd 함수가 있습니다. 둘 다 튜플을 인자로 받아 fst는 튜플의 첫 번째 요소를 리턴하고, snd는 튜플의 두 번째 요소를 리턴합니다.

> fst ('a', 1)
'a'
> snd ('a', 1)
1

함수의 인자 개수

하스켈 함수의 인자 개수는 항상 하나입니다. fstsnd 함수는 인자 'a'1 두 개의 인자를 받는 함수가 아니라 ('a', 1)이라는 튜플 값 하나를 인자로 받는 함수입니다.

하스켈에는 인자를 2개 이상 받는 함수가 존재하지 않습니다. 2개 이상의 인자가 필요하면 튜플을 넘기거나, 조금 있다 살펴볼 커리 함수(curried function)을 사용합니다.

함수의 타입

ghci에서 :t 명령을 이용하면 주어진 값의 타입을 확인할 수 있습니다. 아래 예를 보면,'a'의 타입은 Char임을 확인할 수 있습니다.

> :t 'a'
'a' :: Char

마찬가지 방법으로 함수의 타입도 확인할 수 있습니다. 앞서 살펴본 isLower 함수의 타입을 확인해보면 다음과 같은 결과가 출력됩니다.

> :t isLower
isLower :: Char -> Bool

isLower 함수의 타입이 Char -> Bool임을 확인할 수 있는데, 여기서 ->는 함수 타입을 나타냅니다. ->의 왼쪽에 있는 타입은 함수의 인자의 타입이고, ->의 오른쪽에 있는 타입은 함수의 리턴 타입입니다. 즉, isLower의 인자 타입은 Char, 리턴 타입은 Bool입니다.

같은 방법으로 fst의 타입도 확인할 수 있습니다.

> :t fst
fst :: (a, b) -> a

fst의 인자 타입은 (a, b), 리턴 타입은 a입니다. 튜플 타입은 (,)로 표현하는데, a타입을 첫 번째 요소로, b 타입을 두 번째 요소로 가지는 튜플 타입을 뜻합니다.

앞서 isLower 함수와 달리 구체적인 타입이 아닌 ab 같은 소문자로 표시된 타입은 다른 타입으로 대체될 수 있기 때문에 Char와 같은 타입과 구분하여 타입 변수(type variable)라고 합니다.

예를 들어, 튜플 ('x', 1::Int)(Char, Int) 타입을 가지는데 이 튜플로 fst 함수를 호출하면 fst 함수의 인자 타입 aChar로, bInt로 대체되어 fst의 타입은(Char, Int) -> Char가 됩니다. 같은 방식으로 (False, 'y') 튜플을 호출하면 aFalse의 타입인 Bool, b'y'의 타입인 Char가 되어 fst의 타입은(Bool, Char) -> Bool이 됩니다. 이처럼 하나의 함수가 인자에 따라 여러 타입을 가질 수 있는데, 이런 함수를 다형 함수(polymorphic function)라고 합니다.

연산자

하스켈은 +, -, &&, ||와 같이 특수 문자로 된 함수도 제공합니다. 이런 함수들을 연산자(operator)라고 부르는데, 연산자와 일반 함수의 차이점은 연산자는 infix로 일반 함수는 prefix로 사용한다는 점입니다. 즉, + 1 2가 아니라 1 + 2와 같은 형태로 함수를 호출하게 됩니다.

> 1 + 2
3

하지만 + 연산자를 prefix로 호출할 수도 있습니다. (+)와 같이 연산자를 괄호로 싸주면 일반 함수와 마찬가지로 prefix로 호출이 가능합니다.

> (+) 1 2
3

커리 함수(curried function)

앞서 하스켈 함수는 항상 인자를 하나만 받는다고 이야기하였습니다. 2개 이상의 인자가 필요할 경우, 튜플을 넘길 수 있다는 사실도 배웠습니다. 하지만 논리 연산자 (&&)의 예를 보면 인자를 2개 받는 것처럼 보입니다.

> False && True
False

분명 하스켈 함수는 인자를 1개만 받을 수 있다고 이야기했는데, (&&)는 어떻게 2개의 인자를 받을 수 있는 걸까요? :t 명령을 이용해서 (&&) 함수의 타입을 확인해 보겠습니다.

> :t (&&)
(&&) :: Bool -> Bool -> Bool

(&&)의 타입은 Bool -> Bool -> Bool입니다. 이 타입을 어떻게 해석해야 할까요? 앞서->는 함수 타입이고 ->의 왼쪽은 함수의 인자, ->의 오른쪽은 함수의 리턴 타입이라고 이야기했었습니다. 하지만 ->가 2개 있기 때문에 어디가 인자 타입이고, 어디가 리턴 타입인지 구분하기가 어렵습니다.

->은 우결합(right-associative)하기 때문에 Bool -> (Bool -> Bool)와 같이 괄호로 묶을 수 있습니다. 이 타입을 놓고 다시 해석을 해보면, (&&) 함수는 인자가 Bool 타입이고, 리턴 값은 함수 타입인 Bool -> Bool임을 알 수 있습니다.

바꿔 말해, (&&) 함수는 인자 2개를 받는 함수가 아니라 Bool 타입의 인자를 받아서Bool -> Bool 타입의 함수를 리턴하는 함수입니다. 하스켈은 고차 함수를 지원하는 언어이기 때문에 함수가 함수를 인자로 받거나 함수를 리턴하는 것이 가능합니다.

실제로 인자를 하나만 넘겨보겠습니다.

> (&&) False
Prelude> (&&) False

<interactive>:5:1: No instance for (Show (Bool -> Bool)) (maybe you haven't
applied enough arguments to a function?) arising from a use of ‘print’ In a stmt
of an interactive GHCi command: print it

에러 메시지는 ghci가 함수는 화면에 제대로 출력할 수 없기 때문에 발생합니다. 정말 함수를 리턴한 것이 맞는지는 :t 명령으로 확인할 수 있습니다.

> :t (&&) False
(&&) False :: Bool -> Bool

이처럼 리턴된 값이 Bool -> Bool 타입을 가진 함수라는 사실을 확인할 수 있습니다. 이 함수를 f라는 이름으로 바인딩하여 다시 True를 호출해 보겠습니다.

> let f = (&&) False
> f True
False

False && True(&&) 함수에 2개의 인자 FalseTrue를 넘긴 것이 아니라, 먼저False를 호출하고 그 결과로 함수가 리턴되면 다시 True를 인자로 리턴된 함수를 호출한 것입니다. 이 과정을 좀 더 명시적으로 보기 위해 괄호를 추가하면 다음과 같습니다.

> ((&&) False) True
False

이렇게 함수가 함수를 리턴하는 방식으로 여러 개의 인자를 받을 수 있게 만든 함수를 커리 함수(curried function)라고 부릅니다. 이와 달리 여러 값을 하나의 튜플로 묶어 넘기는 것을 언커리(uncurried function) 함수라고 부릅니다. (&&)는 커리 함수의 예이고, 앞서 살펴본 fstsnd 함수는 언커리 함수의 예입니다.

섹션

(+), (-)와 같은 연산자들은 모두 커리 함수이기 때문에 인자를 하나만 줘서 호출하면 함수를 리턴합니다. 예를 들어, (+) 1은 주어진 인자에 1을 더해서 리턴하는 함수가 됩니다.

> let f = (+) 1
> f 2
3

이렇게 바이너리 연산자에 인자 하나만 줘서 새로운 함수를 만드는 경우가 흔하기 때문에 하스켈은 (+1)과 같은 특별한 문법을 제공합니다. 이렇게 연산자에 인자 하나만 줘서 새로운 함수를 만드는 방법을 섹션(section)이라고 부릅니다.

> (+1) 2
3

피연산자의 위치에 따라 섹션을 만드는 방법은 2가지가 있습니다. 예를 들어 (1-)는 1에서 주어진 값을 빼는 함수이고, (-1)는 주어진 값에서 1을 빼는 함수입니다.

> (-5) 3
-2
> (5-) 3
2

하스켈 입문서

Learn You a Haskell for Great Good!

보통 줄여서 LYHGG라고 부르는 Learn You a Haskell for Great Good!는 가장 쉬운 하스켈 입문서입니다. 원서는 무료로 공개되어 있어 온라인으로 읽으실 수 있고, 국내에는가장 쉬운 하스켈 책 느긋하지만 우아하고 세련된 함수형 언어이란 제목으로 번역 출간되었습니다.

책 전반부에서는 하스켈 타입, 함수, 문법, 재귀, 고차 함수, 모듈 등 하스켈 언어의 기본적인 내용을 쉽고 간결하게 설명하고 있고, 후반부에는 타입클래스, 입출력, Functor, Applicative Functor, Monoid, Monad 등 고급 주제들을 다루고 있습니다.

참고로 쉬운 입문서라고는 하지만 책 후반부의 난이도가 전반부에 비해 상대적으로 높아서 독학하시는 분들이 대개 Functor나 Monad에 막혀서 그만 두는 경우가 많습니다.

Real World Haskell

LYHGG가 주로 하스켈 언어의 특징에 대해 소개한 책이라면, Real World Haskell은 하스켈을 실제 업무에 사용할 수 있도록 다양한 예제를 통해 하스켈의 특징을 설명하고 있습니다. 저자인 Bryan O’Sullivan과 Don Stewart는 각각 페이스북과 스탠다드 차타드 은행에서 하스켈 팀을 이끌고 실제로 하스켈로 개발 업무를 수행하고 있습니다.

덕분에 이 책에는 JSON 처리, 파서 작성, 시스템 프로그래밍, 데이터베이스, GUI, 네트워크 프로그래밍 등의 하스켈을 실제 업무에 사용하기 위해 필요한 구체적인 지식들이 포함되어 있습니다.

한 가지 아쉬운 점은 2007년 이 책이 출간된 이후 하스켈 라이브러리에도 여러 변경 사항이 생기면서 일부 챕터의 코드는 더 이상 제대로 동작하지 않는다는 점입니다. 특히, 챕터 19 “Error handling”의 경우 하스켈의 예외 처리 라이브러리가 완전히 개정되면서 더 이상 유효하지 않게 되었습니다.

원서는 온라인에서 무료로 읽으실 수 있고, 아쉽게도 국내에 번역 출간된 적은 없습니다.

Haskell Book

Haskell Book은 요즘 하스켈 커뮤니티에서 가장 핫한 책입니다. 이 책은 Christopher Allen과 Julie Moronuki이 공동 집필하였는데, 특이한 점은 Julie Moronuki은 이 책을 쓰기 전까지 개발자가 아니었다는 사실입니다. 대학에서 철학을 전공하고 교사 및 사서로 일하던 Julie는 Christopher의 설득으로 하스켈을 생애 첫 프로그래밍 언어로 배웠고 그 내용을 함께 정리하여 책으로 출간하게 되었습니다. 덕분인지 기존의 하스켈 책보다 훨씬 이해하기 쉽게 잘 작성되었다는 평가를 받고 있습니다.

이 책의 1장은 하스켈이 아닌 람다 대수(lambda calculus)로 시작합니다. 람다 대수는 하스켈을 비롯한 모든 함수 언어의 이론적 배경이 되는 수학으로 함수 언어를 제대로 사용하려면 언젠가는 배워야 하는 내용인데, 이 책은 과감하게 1장에서 람다 대수를 설명하고 2장부터 하스켈에 대한 소개로 이어집니다.

이 책은 LYHGG와 Real World Haskell의 장점을 고루 섞어놓았습니다. 하스켈 언어에 대한 기본 설명도 충실하고, 배운 내용을 실제로 사용할 수 있게 여러 실례도 소개하고 있습니다. 새로 하스켈을 공부하시는 분이라면 제가 가장 추천 드리는 책이기도 합니다.

이 책은 아직 완간되지 않습니다. 대신 홈페이지에서 $59에 Early Access 버전을 구매하실 수 있습니다. 당연히 아직 한글 번역서도 존재하지 않습니다. 국내 출판사에서 번역 출간 계약을 하면 좋겠습니다.

하스켈 무한 리스트

하스켈은 유한한 리스트뿐만 아니라 무한히 긴 리스트를 정의할 수 있습니다. 예를 들어,[m..]은 m에서 시작하는 모든 정수의 리스트가 됩니다.

> [1..]
[1,2,3,4,5,6,7,8Interrupted.
>

ghci[1..]을 입력하면 이 리스트를 화면에 출력하는데, 무한히 긴 리스트이므로 끝이 나지 않고 계속해서 숫자를 출력하는 것을 보실 수 있습니다. Ctrl+C 키를 눌러서 종료하면 Interrupted 메시지와 함께 출력이 종료됩니다.

리스트 기초에서 살펴본 take 함수를 이용하면 무한히 긴 리스트에서 일부 원소만 뽑아낼 수 있습니다.

> take 10 [1..]
[1,2,3,4,5,6,7,8,9,10]

같은 리스트를 [1..]과 같은 표기법을 이용하지 않고 재귀적으로 정의할 수도 있습니다. 이렇게 정의한 ints 리스트에서 take 함수로 10개 처음 10개 원소를 꺼내보면 [1..]과 마찬가지로 1부터 10까지 정수의 리스트를 리턴하는 것을 볼 수 있습니다.

> let ints = 1 : map (+1) ints
> take 10 ints
[1,2,3,4,5,6,7,8,9,10]

ints가 어떻게 동작하는지를 이해하기 위해 원소를 차례대로 하나씩 꺼내보겠습니다.ints는 머리 1과 꼬리 map (+1) ints로 구성된 리스트이기 때문에 ints의 첫 번째 원소는 1입니다. 두 번째 원소는 map (+1) ints의 머리입니다. map (+1)은 주어진 리스트의 모든 원소에 1을 더해주는데, 여기서는 다시 ints를 인자로 넘겼습니다. 이 ints의 첫 번째 인자는 1이였으므로 map (+1) ints의 첫 번째 원소는 11을 더한 2가 됩니다. 따라서 전체 ints의 두 번째 인자도 2가 됩니다. 같은 방법으로 세 번째 원소를 꺼내면 3이 되는 것을 확인할 수 있습니다.

이런 무한 리스트도 유한한 리스트와 마찬가지로 map이나 filter 같은 함수들을 사용해 원소를 원하는 대로 바꿀 수 있습니다. 아래 예제에서 evensints 리스트의 각 원소에2를 곱한 짝수의 리스트입니다. take 함수로 10개를 뽑아보면 2부터 20까지 짝수 10개가 리턴되는 것을 볼 수 있습니다.

> let ints = 1 : map (+1) ints
> let evens = map (*2) ints
> take 10 evens
[2,4,6,8,10,12,14,16,18,20]

조금 더 간단한 예로 1이 무한히 반복되는 리스트를 정의해 볼 수도 있습니다.

> let ones = 1 : ones
take 10 ones
[1,1,1,1,1,1,1,1,1,1]

ones의 머리는 1이고 꼬리는 다시 ones입니다. 따라서 ones의 두 번째 원소는 ones의 머리이므로 1이고, 세 번째, 네 번째 원소도 모두 1이 되는 것을 알 수 있습니다.

하스켈은 무한 리스트를 정의하는 경우가 많으므로 Prelude 모듈에서는 편의를 위해iterate를 함수를 제공합니다. iterate 함수는 함수 f를 인자 x에 0번, 1번, 2번, … 반복해서 호출하여 리스트를 만들어줍니다.

iterate f x == [x, f x, f (f x), ...]

iterate 함수를 이용해서 ints를 정의할 수도 있습니다.

> let ints = iterate (+1) 1

iterate (+1) 1[1, (+1) 1, (+1) $ (+1) 1, (+1) $ (+1) $ (+1) 1, ...]와 동일하고,(+1) $ (+1) 1은, (+2) 1과 같으므로 위 리스트는 [1..]과 같다는 사실을 쉽게 확인할 수 있습니다.

iterate 함수로 ones도 정의할 수 있습니다.

> let ones = iterate id 1

여기서는 iterate 함수의 인자로 id를 넘겼습니다. idSKI 콤비네이터에서 살펴보았던 항등 함수(identity function)로 인자를 받아 그대로 리턴합니다. iterate id 1[1, id 1, id $ id 1, id $ id $ id 1, ..]이므로 모든 원소가 1이 되는 것을 확인할 수 있습니다.

이런 경우에는 repeat 함수를 이용하면 더 간단합니다. repeat 함수는 주어진 인자 x를 무한히 반복합니다.

> let ones = repeat 1

이런 무한 리스트는 zip 함수와 같이 사용하는 경우가 많습니다. zip 함수는 리스트 두 개를 인자로 받아 각 리스트이 원소를 하나씩 뽑아 묶은 튜플의 리스트를 리턴합니다. 간단한 예를 보면,

> zip [1,2,3,4,5] ['a', 'b', 'c', 'd', 'e']
[(1,'a'),(2,'b'),(3,'c'),(4,'d'),(5,'e')]

두 리스트 중 한 쪽이 짧으면 먼저 끝나는 쪽을 기준으로 끝이 납니다.

zip [1,2,3,4,5] ['a', 'b', 'c']
[(1,'a'),(2,'b'),(3,'c')]

zip 함수를 이용하여 리스트 각 원소의 인덱스를 매기고 싶은데, 리스트이 길이를 미리 알 수 없다면 어떻게 해야 할까요? 이런 경우 [1..]와 같이 무한히 긴 인덱스의 리스트를 넘기면 됩니다.

> let ls = ['a', 'b', 'c', 'd', 'e']
> zip [1..] ls
[(1,'a'),(2,'b'),(3,'c'),(4,'d'),(5,'e')]

하스켈 검색 엔진 Hoogle

구글(Google)과 유사한 발음의 Hoogle은 하스켈 API 검색 엔진입니다. API의 이름뿐만 아니라 타입으로도 검색할 수 있다는 점이 가장 큰 특징입니다.

예를 들어 (a -> b) -> [a] -> [b] 타입의 함수를 찾고 싶으면 검색 창에(a -> b) -> [a] -> [b]라고 입력해주면 됩니다. 그러면 Hoogle은 검색 결과로 PreludeData.List 모듈에 정의된 map 함수를 보여줍니다.

map :: (a -> b) -> [a] -> [b]

cabal-installhoogle 패키지를 설치하면 커맨드라인에서도 검색이 가능합니다.

$ cabal install hoogle

->가 포함된 타입을 검색할 때는 검색어를 ""로 묶어줍니다.

$ hoogle "(a -> b) -> [a] -> [b]"
Prelude map :: (a -> b) -> [a] -> [b]
Data.List map :: (a -> b) -> [a] -> [b]
...

~/.ghc/ghci.conf 파일에 다음 내용을 추가해주면 ghci 내에서도 검색이 가능합니다.

:def hoogle \s -> return $ ":! hoogle --count=15 \"" ++ s ++ "\""

ghci에서 검색할 때는 :hoogle 뒤에 검색어를 입력하면 됩니다.

>:hoogle (a -> b) -> [a] -> [b]
 Prelude map :: (a -> b) -> [a] -> [b]
 Data.List map :: (a -> b) -> [a] -> [b]
 ...