· KLDP.org · KLDP.net · KLDP Wiki · KLDP BBS ·
Yet Another Haskell Tutorial

Yet Another Haskell Tutorial 요약본

Contents

1. 문서정보
1.1. 저작권 및 라이선스
1.2. 출처
1.3. 알림
1.4. 문서의 역사
1.5. 공헌자
2. 시작하면서 ...
2.1. 대화식 환경와 컴파일러
2.2. 표기법
3. 하스켈 언어의 기초
3.1. 산술연산자
3.2. 순서쌍(tuple)
3.3. 리스트
3.3.1. 문자열
3.3.2. 기본적인 리스트 함수
3.4. 소스코드 파일
3.5. 함수
3.5.1. let 엮기 (let binding)
3.5.2. 중위 연산자
3.6. 주석
3.7. 재귀
3.8. 입출력
4. 형(type)
4.1. 기본형
4.2. 다형적 형
4.3. 형 클래스
4.3.1. 형 클래스의 필요성
4.3.2. equality 테스트
4.3.3. Num 클래스
4.3.4. Show 클래스
4.4. 함수의 형
4.4.1. 람다 대수 (Lamda Calculus)
4.4.2. 고차원 형
4.4.3. 성가신 IO 형
4.4.4. 명시적 형 선언
4.4.5. 함수를 인자로 사용하기
4.5. 자료형
4.5.1. Pair
4.5.2. 다중 생성자
4.5.3. 재귀적 자료형
4.5.4. 이진 나무 (Binary Tree)
4.5.5. Enumerated Sets
4.5.6. Unit 형
5. 기본 입출력
5.1. RealWorld
5.2. 행동
5.3. 입출력 라이브러리
5.4. 파일 읽기 프로그램
6. 고급 기능
6.1. 섹션과 중위연산자
6.2. 지역 선언
6.3. 함수의 부분적용
6.4. 패턴 매칭
6.5. 가드
6.6. 인스턴스 선언
6.6.1. Eq 클래스
6.6.2. Show 클래스
6.6.3. 그 밖의 중요한 클래스
6.6.4. 클래스 문맥
6.6.5. 클래스의 파생 (deriving class)
6.7. 자료형
6.8. 리스트
6.9. 배열
6.10. Map
7. 부록
7.1. 부록 1.
7.2. 부록 2. Prelude.hs 의 내장함수들
7.3. 부록 3. 입출력 관련 자료형 및 함수

1. 문서정보

1.1. 저작권 및 라이선스


본 문서는 GFDL(GNU Free Document License)로 배포되므로 자유롭게 수정 및 재배포할 수 있습니다.

단, GFDL 제 4조 D항과 E항에 의해 다음 저작권 표시를 반드시 포함해야 하며 저작권 표시 부분은 절대 바꿔서는 안됩니다.


Copyright (c) Hal Daume III, 2002-2006. The preprint version of this tutorial is intended to be free to the entire Haskell community. It may be distributed under the terms of the GNU Free Document License, as permission has been granted to incorporate it into the Wikibooks projects.

Copyright (c) redneval@kldp.org. 2007,2009. 본 문서는 GNU Free Document License로 배포됩니다. Yet Another Haskell Tutorial의 원저작자는 Hal Daume III 이며, 원문의 번역/요약/수정/추가된 부분의 저작권은 redneval@kldp.org 에 있습니다.


1.2. 출처

영어 원문은 다음 주소에서 받을 수 있습니다.


다음 주소로 가면 온라인상으로 볼 수 있습니다.


하스켈에 관한 더 많은 문서는 다음 주소에서 볼 수 있습니다.


1.3. 알림


본 문서는 Yet Another Haskell Tutorial의 요약판으로,

완전한 내용을 보기 위해서는 영어 원문을 보시기 바랍니다.

1.4. 문서의 역사


2007년 12월 18일 번역문 최초 게시

2009년 7월 5일 개정

1.5. 공헌자


QuietJoon


2. 시작하면서 ...

2.1. 대화식 환경와 컴파일러

하스켈 소스코드를 실행하기 위한 프로그램 중 잘 알려진 것은 두 가지가 있습니다. 첫번째로 Hugs는 http://haskell.org/hugs 에서 받을 수 있으며, 대화방식(interactive mode)으로만 동작합니다. GHC(Glasgow Haskell Compiler)는 http://haskell.org/ghc 에서 받을 수 있으며, 대화식(interactive mode)으로도 동작하고, 소스코드를 컴파일할 수도 있습니다. GHC는 사실상(de facto) 표준구현으로 여겨집니다. 본 글에서는 GHC를 사용하여 설명하겠습니다.

편집기는 Emacs/XEmacs의 haskell-mode를 추천합니다.

2.2. 표기법

대화식 환경에서 실행하는 코드는 다음과 같이 `모듈명>' 으로 시작합니다.
Prelude> print "Hello, world"
그렇지 않다면 파일로 작성되는 코드입니다.

3. 하스켈 언어의 기초


첫번째로, 하스켈은 `느긋한' 언어라는 점이 가장 중요합니다. 여기서 `느긋한'은, 결과값이 계산되지 않으면 안될 시점까지 계산을 늦춘다는 의미입니다. 느긋한 언어의 장점의 예를 하나 들어보면, 무한크기의 자료형을 다룰 수 있다는 점입니다.

만약 명령형의 언어에서 무한크기의 자료구조를 만들기 위해서는, 예를들어 다음과 같은 같은 형태의 코드를 사용해야할 것입니다.
List makeList()
{
  List current = new List();
  current.value = 1;
  current.next = makeList();
  return current;
}
그러나 이는 무한순환(infinite loop)을 돌게 될 것이고 프로그램은 종료되지 않을 것입니다. 코드의 결과값이 필요하지 않아도 전부 계산하는 방식을 `엄격한 계산법'이라고 합니다. 반면에 `느긋한 계산법'을 사용하는 언어는 결과값이 필요하지 않다면 계산하지 않습니다. 또한, 엄격한 언어는 `값에 의한 호출'이라고 하고, 느긋한 언어는 `이름에 의한 호출' 이라고 합니다. 위의 코드에서 5번째 줄에서 makeList 실행될 때 그 `값'을 가져오게 되므로, 이로 인해 무한순환에 빠지게 됩니다. 하스켈에서는 다음과 같이 작성할 수 있습니다.
makeList = 1 : makeList
이 코드는 다음과 같이 읽을 수 있습니다. 좌변 : 우리는 makeList 를 정의하고 있다. 우변 : makeList 의 정의는 `1 : makeList' 이다. 참고로, 하스켈에서 콜론(:)연산자는 리스트를 만드는데 사용됩니다. (뒤에서 자세히 설명하겠습니다.) 그러므로 우변에서는, makeList은 1과 makeList로 이뤄진 값이라는 것을 알려줍니다. 그런데 하스켈은 느긋한 언어이므로, (또는 "이름에 의한 호출") 실제로는 이 시점에서 makeList의 값을 계산하지 않습니다. 이를 계산하는 시점은 화면에 출력하거나 리스트의 합을 계산하는 등, 결과값이 필요한 시점에만 이뤄지게 됩니다. 그러므로, 예를 들면 makeList의 첫 10개 원소를 가진 새로운 리스트를 정의한다면, makeList의 길이가 무한이라고 해도 상관없게 됩니다. 그 경우에는 makeList의 첫 10개 원소만 계산됩니다. 이것이 바로 `느긋함' 입니다.

두번째로 하스켈은 대소문자를 구분합니다. 다른 언어들이 대소문자를 구분하는 것과는 약간 다른 특징이 있습니다. 하스켈에서 어떤 것에 이름을 붙이는 것은 6가지 종류가 있습니다. 변수(variable), 생성자(constructor), 형 변수(type variable), 형 생성자(type constructor), 형 클래스(type class), 모듈(module)입니다. 이중에서 변수와 형 변수는 소문자 또는 밑줄문자(`_')로 시작해야하고, 나머지 넷은 대문자로 시작해야합니다. (다른 언어에서와는 다르게, 하스켈에서 변수는 함수를 의미합니다.)

함수형 언어답게, 하스켈은 부수효과를 최대한 피합니다. 부수효과는 함수의 출력과는 관련이 없는 무엇인가를 의미합니다. C와 자바에서는 함수에서 전역변수를 수정할 수 있는데, 전역변수는 함수의 출력과는 무관하므로 이는 부수효과입니다. 게다가, 시스템의 상태를 변화시키는 것은 부수효과입니다. 화면에 무엇인가를 출력하거나 하는 것도 부수효과입니다. 부수효과가 없는 함수를 `순수한' 함수라고 합니다. 함수가 순수한지 알아볼 수 있는 간단한 테스트는 다음과 같은 질문입니다. `같은 인자가 주어졌을 때 함수가 항상 같은 출력을 발생시키는가?' 그러므로 C 나 자바에서 했던 것과는 생각을 다르게 해야합니다. 만약 x 라는 값이 있다면, 이를 레지스터나 메모리같은 것으로 생각해서는 안됩니다. x 는 단지 이름일뿐입니다. 마치 이 글의 원저자의 이름이 `Hal'인 것처럼 변하지 않는 무언가입니다. 그러므로 다음 C 코드와 같이 나타내는 것은 하스켈에서는 불가능합니다.
int x = 5;
x = x + 1;
x = x + 1 와 같이 x 가 예전에 가졌던 값을 없애고 새로운 값으로 대체하는 것을 파괴적 갱신(destructive update)이라고 부릅니다. 하스켈에서는 파괴적 갱신이 없습니다. 하스켈에서는 파괴적 갱신을 허용하지 않았기 때문에, 코드를 이해하기가 매우 쉽습니다. 우리가 f라는 함수를 정의하면, 같은 인자가 주어지면 언제 실행되는지에 관계없이 같은 결과가 나옵니다. 이런 것을 참조적 투명성'이라고 부릅니다. 또한, 같은 인자가 주어지면 f와 g가 항상 같은 출력을 갖는다면, f는 g로 대체할 수 있습니다. (참조적 투명성'의 정의에 대한 다른 견해가 있을 수 있습니다.)

3.1. 산술연산자


먼저 Hugs나 GHCi 같은 대화식 환경을 실행합니다. GHCi가 실행되면 다음과 같이 화면에 뜨게 될 것입니다.
Prelude>
더하기(+), 빼기(-), 곱하기(*), 나누기(/), 승(^), 제곱근(sqrt)과 같은 기본적인 산술연산자가 제공됩니다. 그러므로 하스켈 쉘은 강력한 계산기로 사용할 수 있습니다.
Prelude> 5*4+3
23
Prelude> 5^5-2
3123
Prelude> sqrt 2
1.4142135623730951
Prelude> 5*(4+3)
35
함수에 인자를 주기 위해 괄호는 필요하지 않습니다. 그러므로 sqrt(2) 라고 쓸 수도 있고, sqrt 2 라고 쓸 수도 있습니다. (C와 같은 언어의 경험이 있다면, sqrt 2 에서 2가 정수형처럼 보이는데 왜 결과값이 실수형으로 나오는지 의아해할 것입니다. 이것은 형 클래스에서 자세히 다루겠습니다.)

3.2. 순서쌍(tuple)


하나의 값 말고도 여러 개의 값을 순서쌍으로 표현할 수 있습니다. 소괄호로 둘러싸고 쉼표로 구분하면 순서쌍이 됩니다.
Prelude> (5,3)
(5,3)
이 때, 각각의 요소가 같은 형이 아니어도 상관없습니다. 두 개의 값으로 이뤄진 순서쌍을 짝(pair)이라고 합니다. fst라는 함수는 짝의 첫번째 요소를 반환합니다. 마찬가지로 snd는 두번째 요소를 반환합니다.
Prelude> fst (5, "hello")
5
Prelude> snd (5, "hello")
"hello"
다음과 같이 두 개 이상의 요소가 있을 수도 있습니다. 다만, 이 경우에는 fst와 snd 함수는 사용할 수 없습니다.
Prelude> (1,2,3)
(1,2,3)
Prelude> (1,2,3,4)
(1,2,3,4)
이를 순서쌍이라고 부르며, 고정된 수의 자료를 저장하게 됩니다. 순서쌍의 내부에 순서쌍을 가질 수 있습니다. ((1,'a'),"foo") 과 같이 말입니다.

3.3. 리스트


순서쌍은 고정된 수의 요소가 있지만, 리스트는 임의의 수의 요소가 있을 수 있습니다. 순서쌍과는 다르게 대괄호가 사용됩니다.
Prelude> [1,2]
[1,2]
Prelude> [1,2,3]
[1,2,3]
리스트는 비어있을 수도 있는데, 빈 리스트를 [] 로 나타냅니다. 리스트의 앞에 콜론 연산자를 사용하여 요소를 추가할 수도 있습니다.
Prelude> 0:[1,2]
[0,1,2]
Prelude> 5:[1,2,3,4]
[5,1,2,3,4]
Prelude> 5:1:2:3:4:[]
[5,1,2,3,4]
사실은 5,1,2,3,4 는 설탕 구문인 셈이고, 컴파일러는 5:1:2:3:4:[] 와 같이 해석합니다. 그러므로 하스켈의 리스트는 (C 에서의 배열이 아닌) 연결 리스트(linked list)와 유사합니다. (설탕 구문이란, 반드시 필요한 구문은 아니지만 코드 작성과 이해를 쉽게 해주는 구문을 말합니다.)

순서쌍과 리스트의 또 다른 점은, 리스트에는 같은 종류의 요소들만 들어갈 수 있다는 것입니다. 정수와 문자열을 하나의 리스트에 넣으려고 하면 오류가 발생합니다. 또한 리스트 내에는 리스트나 순서쌍이 올 수 있습니다.
Prelude> [(1,1),(2,4),(3,9),(4,16)]
[(1,1),(2,4),(3,9),(4,16)]
Prelude> ([1,2,3,4],[5,6,7])
([1,2,3,4],[5,6,7])
리스트의 기본 함수에는 head와 tail이 있습니다. head는 첫번째 요소, tail은 첫번째 요소를 빼고 뒷 부분을 반환합니다. 다만, 빈 리스트인 경우에는 오류가 발생합니다.
Prelude> length [1,2,3,4,10]
5
Prelude> head [1,2,3,4,10]
1
Prelude> tail [1,2,3,4,10]
[2,3,4,10]
다시 한번 말하지만 하스켈은 무한 길이의 리스트를 다룰 수도 있습니다.
Prelude> let ones = 1 : ones
Prelude> take 5 ones
[1,1,1,1,1]
물론 다음과 같이 무한 길이의 리스트를 출력하려한다면 무한순환(infinite loop)를 돌게 됩니다.
Prelude> ones

3.3.1. 문자열

하스켈에서는 문자열은 문자의 리스트입니다.
Prelude> 'H':'e':'l':'l':'o':[]
"Hello"
그러므로 문자열을 합치기 위해서는 리스트를 합치는 연산자가 필요합니다.
Prelude> "Hello " ++ "World"
"Hello World"
show 함수는 어떠한 값을 문자열로 바꿔주고, read 함수는 반대로 문자열을 어떠한 값으로 바꿔줍니다. read의 인자로 값으로 바뀔 수 없는 문자열이 오게 되면 실행시 오류가 발생합니다. (컴파일시에는 오류가 발생하지 않습니다.)
Prelude> "Five squared is " ++ show (5*5)
"Five squared is 25"
Prelude> read "5" + 3
8
Prelude> read "Hello" + 3
Program error: Prelude.read: no parse

3.3.2. 기본적인 리스트 함수

리스트를 다루기 위한 세 가지 기본 함수가 있습니다. 바로 map, filter, foldr (또는 foldl) 입니다. map 함수는 리스트의 각 값들에 대해 어떤 함수를 적용하게 됩니다. 예를 들어 Char.toUpper라는 내장함수는 문자 입력을 받아서 대문자로 바꿔주는데, 이를 이용하면 문자열 전체를 대문자로 바꿀 수 있습니다.
Prelude> map Char.toUpper "Hello World"
"HELLO WORLD"
리스트에서 어떤 값을 빼버리고자 하면, filter 함수를 사용하면 됩니다. Char.isLower 라는 함수는 어떤 문자가 소문자인지를 알려주는 함수입니다. 이를 이용하면 다음과 같이 어떤 문자열에서 소문자만 뽑아낼 수 있습니다.
Prelude> filter Char.isLower "Hello World"
"elloorld"
foldr은 함수, 초기값, 리스트 세 개의 인자를 받습니다.
Prelude> foldr (+) 0 [3,8,12,5]
28
리스트는 3 : 8 : 12 : 5 : [] 과 같고, 빈 리스트는 초기값인 0 으로 바뀌며, `:' 는 `+' 로 바뀌면, 3 + 8 + 12 + 5 + 0 과 같이 바뀌어서 결과값 28이 나오게 됩니다.

물론, 다음과 같이 곱하기를 수행할 수도 있습니다.
Prelude> foldr (*) 1 [4,8,5]
160
그러나 함수가 결합법칙이 성립하지 않을 수 있습니다. 결합법칙은 a · (b · c) = (a · b) · c) 과 같은 수식이 성립함을 말합니다. 예를 들어 함수 (·) 가 결합법칙이 성립하지 않는다면, ((4 · 8) · 5) · 1 과 4 · (8 · ((5 · 1)) 의 결과값이 다를 것입니다. 그리고 foldr은 왼쪽부터 먼저 계산하며, foldl은 오른쪽부터 먼저 계산합니다. 그러므로 foldr (-) 1 4,8,5 은 4 - (8 - (5 - 1)) 이므로 0 이 되지만,
    foldr (-) 1 [4,8,5]
==> 4 - (foldr (-) 1 [8,5])
==> 4 - (8 - foldr (-) 1 [5])
==> 4 - (8 - (5 - foldr (-) 1 []))
==> 4 - (8 - (5 - 1))
==> 4 - (8 - 4)
==> 4 - 4
==> 0
foldl (-) 1 4,8,5 은 (((1 - 4) - 8) - 5) 이므로 -16 이 됩니다.
    foldl (-) 1 [4,8,5]
==> foldl (-) (1 - 4) [8,5]
==> foldl (-) ((1 - 4) - 8) [5]
==> foldl (-) (((1 - 4) - 8) - 5) []
==> ((1 - 4) - 8) - 5
==> -16
map과 foldl함수는 쉽게 구현할 수 있습니다.
my_map f [] = []
my_map f (x:xs) = f x : my_map f xs

my_foldl f n [] = n
my_foldl f n (x:xs) = my_foldl f (f n x) xs
여기서 (x:xs) 라고 된 것은 x는 head, xs는 tail을 나타냅니다.


지금까지 배운 것을 이용하여 my_drop, my_take 등의 함수를 만들어봅시다. 작성된 소스코드를 불러오는 방법은 3.4에서 설명하겠습니다.
module MyList where

my_drop 0 list = list
my_drop n (x:xs) = my_drop (n-1) xs

my_take 0 list = []
my_take n (x:xs) = x:(my_take (n-1) xs)

index 0 (x:xs) = x
index n (x:xs) = index (n-1) xs

my_last (x:[]) = x
my_last (x:xs) = my_last xs
*MyList> my_take 5 [1..]
[1,2,3,4,5]
*MyList> my_drop 3 [1,3..9]
[7,9]
*MyList> index 5 [0..]
5
*MyList> my_last 5 [1..10]
10

여기서 1..10 은 1부터 10까지 모든 정수를 요소로 갖는 리스트이며, 1.. 는 모든 자연수를 요소로 갖는 리스트입니다. 1,3..9 1,3,5,7,9 를 나타냅니다.

3.4. 소스코드 파일


Test.hs 라는 파일을 다음과 같이 작성합니다.
module Test where
x = 5
y = (6, "Hello")
z = x * fst y
여기서 Test 는 모듈이름인데, 이는 파일이름과 같아야합니다. 파일을 저장했으면 다음과 같이 대화식 환경(ghci)으로 불러올 수 있습니다.
% hugs Test.hs
% ghci Test.hs
대화식 환경이 이미 실행중이라면 :load 또는 :l 명령어로 불러올 수 있습니다.
Prelude> :l Test.hs
...
Test>
`Test>' 라고 나타나는 것은 Test 모듈을 불러왔음을 의미합니다. 마찬가지로 Prelude도 하나의 모듈이라는 것을 알 수 있습니다. 이제 Test 모듈을 제대로 불러왔는지 확인해봅시다.
Test> x
5
Test> y
(6,"Hello")
Test> z
30
예상대로 잘 동작함을 알 수 있습니다. 이제 컴파일하는 방법을 알아봅시다. 실행가능한 파일로 컴파일하기 위해서는 Main 모듈이 있어야 하고 main이라는 함수도 필요합니다. 그러므로 Test.hs 파일을 다음과 같이 작성합니다.
module Main where
main = putStrLn "Hello World"
% ghc --make Test.hs -o test

그러면 test 라는 실행파일로 컴파일되는데, 윈도우즈에서는 실행파일 확장자가 exe 이므로 "-o test.exe" 와 같이 하면 되겠습니다. 이제 실행해 봅시다.
% ./test
Hello World
윈도우즈에서는 다음과 같습니다.
C:\> test.exe
Hello World

3.5. 함수


제곱을 구하는 함수를 정의해봅시다.
square x = x * x
함수의 인자가 0보다 작으면 -1, 0이면 0, 0보다 크면 1을 반환하는 my_signum이라는 함수를 정의해봅시다. Test.hs를 다음과 같이 작성하면 됩니다.
module Test where

my_signum x =
    if x < 0
      then -1
      else if x > 0
        then 1
        else 0
이제 위에서 했던 것처럼 Test 모듈을 불러와서, 다음과 같이 실행해봅시다. Test 모듈을 이미 불러온 상태에서 다시 불러오려면 :reload 또는 :r 명령어를 실행하면 됩니다.
*Test> my_signum 5
1
*Test> my_signum 0
0
*Test> my_signum (5-10)
-1
*Test> my_signum (-1)
-1
마지막 실행문에는 소괄호가 꼭 들어가야하는데, 그 이유는 소괄호가 빠지면 my_signum 이라는 값에서 1을 뺀 값으로 컴파일러가 인식하기 때문입니다.

하스켈에서 if문은 다른 언어와 비슷하지만 then과 else가 항상 필요하다는 점이 다릅니다.

하스켈에는 case 구문도 존재합니다. 함수의 인자가 0 인 경우에는 1, 1인 경우에는 5, 2인 경우에는 2, 나머지 경우에는 -1 를 반환하는 함수를 정의해봅시다.
f x =
  case x of
    0 -> 1
    1 -> 5
    2 -> 2
    _ -> -1
여기서 밑줄문자 `_' 는 나머지 모든 경우를 의미합니다. 이는 만능패(wildcard)를 나타내며 어떠한 값과도 매치됩니다.

하스켈은 파이썬과 유사하게도 들여쓰기가 중요합니다. 이를 `레이아웃'이라고 부르는데, 다음과 같은 규칙을 생각합시다.

  • (1) where, let, do, of의 키워드 뒤에는 `{' 가 삽입된다. 그 다음에 오는 명령문의 들여쓰기 위치가 기억된다.
    where, let, do, of가 중첩된다면 들여쓰기 깊이는 점점 더 깊어져야한다.
  • (2) 들여쓰기가 깊어지면 이전 명령문이 계속 이어지는 것을 나타낸다.
  • (3) 들여쓰기가 같으면 이전 명령문이 끝나고 새로운 명령문이 시작됨을 나타낸다. 이전 명령문 끝에는 `;' 가 붙는다.
  • (4) 들여쓰기의 깊이가 얕아지면 `;' 와 `}' 가 삽입된다.

레이아웃 규칙을 모두 적용하면 다음과 같습니다.
f x = case x of{
        0 -> 1;
        1 -> 5;
        2 -> 2;
        _ -> -1;
      }
레이아웃 규칙을 잘 이용하면 세미콜론과 중괄호가 없이도 코드를 작성할 수 있습니다.

세미콜론과 괄호를 사용하면 레이아웃을 맞추지 않아도 됩니다. 그러므로 다음과 같이 나타낼 수도 있습니다.
f x = case x of { 0 -> 1 ; 1 -> 5 ; 2 -> 2 ; _ -> 1 }
이 경우에는 들여쓰기는 무시됩니다. 그러므로 다음과 같은 코드가 가능합니다.
f x =
   case x of { 0 -> 1 ;
     1 -> 5 ; 2 -> 2
  ; _ -> 1 }
물론 이런 식으로 쓰는 것은 코드를 읽기 어렵게 만들 뿐입니다.

함수의 정의를 여러 번에 나눠서 할 수도 있습니다. 다음과 같은 코드는 위의 코드와 같은 결과를 가져옵니다.
f 0 = 1
f 1 = 5
f 2 = 2
f _ = -1
그러나 이 경우에는 순서에 유의해야합니다. f _ = -1 가 마지막줄이 아닌 첫번째 줄에 있다면, f 함수는 항상 -1 값을 반환하는 함수로 정의됩니다. 컴파일러에 따라서 그런 식의 구문에 대해 경고를 하게 됩니다.

복잡한 함수를 만들기 위해서는 함수를 합성하면 됩니다. 함수합성이란 함수의 결과값을 다른 함수의 인자로 사용하는 것을 말합니다. 5*4+3 도 일종의 함수합성인데, * 함수의 결과값을 + 함수가 인자로 사용하기 때문입니다. 그러면 square와 f 함수를 합성해봅시다.
*Test> square (f 1)
25
*Test> square (f 2)
4
*Test> f (square 1)
5
*Test> f (square 2)
-1
여기서도 소괄호는 필수적인데, square (f 1) 에서 소괄호가 없다면, square f 의 값을 계산하려 할 것이고, 오류가 발생하게 됩니다. 이런 함수합성을 . 연산자로 수행할 수 있습니다. 수학에서 ◦ 와 같이 동작합니다. 수학에서 (f ◦ g)(x) = f (g(x)) 과 같이 해석되므로 하스켈에서도 오른쪽에 있는 함수부터 계산됩니다.
*Test> (square . f) 1
25
*Test> (square . f) 2
4
*Test> (f . square) 1
5
*Test> (f . square) 2
-1
여기서도 괄호가 필수적인데, (square . f) 1 에서 괄호가 없다면 square 와 f 1 을 합성하려하는데 f 1 은 함수가 아닌 5라는 값이므로 오류가 발생합니다.

Prelude 모듈에는 다음과 같은 함수들이 정의돼있습니다.

sqrt : 제곱근을 구하는 함수
id   : 함수의 인자를 그대로 반환하는 함수. id x = x
fst  : 크기가 2인 순서쌍의 첫번째 요소를 반환함.
snd  : 크기가 2인 순서쌍의 두번째 요소를 반환함.
null : 리스트가 비어있는지 알려줌.
head : 리스트의 첫번재 요소를 반환함. (단, 빈 리스트는 예외)
tail : 리스트의 첫번재 요소를 제외한 나머지 요소들을 반환함. (단, 빈 리스트는 예외)
++   : 두개의 리스트를 연결함.
==   : 두 개의 값이 같은지 비교함.
/=   : 두 개의 값이 다른지 비교함.
Prelude> sqrt 2
1.41421
Prelude> id "hello"
"hello"
Prelude> id 5
5
Prelude> fst (5,2)
5
Prelude> snd (5,2)
2
Prelude> null []
True
Prelude> null [1,2,3,4]
False
Prelude> head [1,2,3,4]
1
Prelude> tail [1,2,3,4]
[2,3,4]
Prelude> [1,2,3] ++ [4,5,6]
[1,2,3,4,5,6]
Prelude> [1,2,3] == [1,2,3]
True
Prelude> 'a' /= 'b'
True
Prelude> head []
Program error: {head []}

3.5.1. let 엮기 (let binding)


이차방정식의 근을 구하기 위한 프로그램을 작성해봅시다. a * x ^ 2 + b * x + c = 0 이라는 이차방정식이 있다면,
roots a b c =
    ((-b + sqrt(b*b - 4*a*c)) / (2*a),
     (-b - sqrt(b*b - 4*a*c)) / (2*a))
sqrt(b*b - 4*a*c) 라는 부분은 공통적이므로, let/in 구문을 이용하여 roots 라는 함수 내부에서만 사용되는 det 함수를 정의하면 중복을 줄일 수 있습니다.
roots a b c =
    let det = sqrt (b*b - 4*a*c)
    in ((-b + det) / (2*a),
         (-b - det) / (2*a))
let 에는 두 개 이상의 함수 정의가 포함될 수 있습니다. 그러므로 다음과 같이 바꿀 수 있습니다.
roots a b c =
    let det = sqrt (b*b - 4*a*c)
        twice_a = 2*a
    in ((-b + det) / twice_a,
         (-b - det) / twice_a)

3.5.2. 중위 연산자


+ 연산자는 다음과 같이 중위 연산자로 사용됩니다.
Prelude> 5 + 10
15
그러나 다음과 같이 괄호로 묶어주면 다른 함수들과 마찬가지로 인자들 왼쪽에서 쓸 수 있습니다.
Prelude> (+) 5 10
15
반대로, 다른 함수들은 ` 문자로 묶어주면 중위 연산자처럼 사용할 수 있습니다.
Prelude> map Char.toUpper "Hello World"
"HELLO WORLD"
Prelude> Char.toUpper ‘map‘ "Hello World"
"HELLO WORLD"

3.6. 주석


한 줄 주석은 -- 으로 시작하고, 여러줄 주석은 {- 로 시작해서 -} 로 끝납니다. C에서와는 다르게, 주석의 중첩도 허용합니다. 예를 들면 다음과 같습니다.
module Test2 where
main =
    putStrLn "Hello World"  -- write a string
                            -- to the screen
{- f is a function which takes an integer and
produces integer. {- this is an embedded
comment -} the original comment extends to the
matching end-comment token: -}
fx=
    case x of
      0 -> 1    -- 0 maps to 1
      1 -> 5    -- 1 maps to 5
      2 -> 2    -- 2 maps to 2
      _ -> -1   -- everything else maps to -1

3.7. 재귀


명령형 언어에서는 제어구문으로 반복문이 많이 쓰이지만 하스켈에서 반복문은 가능하지 않습니다. 대신에 재귀를 사용합니다. C에서는 재귀를 사용하지 않고 다음과 같이 만들수 있습니다.
int factorial(int n) {
    int fact = 1;
    for (int i=2; i <= n; i++)
        fact = fact * i;
    return fact;
}
물론 C에서도 재귀를 사용할 수 있습니다.
int factorial(int n) {
    if (n == 1)
        return 1;
    else
        return n * factorial(n-1);
}
반면, 하스켈에서는 재귀를 사용하여 다음과 같이 나타낼 수 있습니다.
factorial 1 = 1
factorial n = n * factorial (n-1)
승을 구하는 함수는 다음과 같습니다.
exponent a 1 = a
exponent a b = a * exponent a (b-1)
재귀함수는 리스트에 대해서도 사용할 수 있습니다. 리스트의 길이를 구하는 함수를 재작성하면 다음과 같습니다.
my_length [] = 0
my_length (x:xs) = 1 + my_length xs
filter 함수도 재작성해봅시다.
my_filter p [] = []
my_filter p (x:xs) =
    if p x
      then x : my_filter p xs
      else my_filter p xs
그러면 이번에는 피보나치수열을 만들어봅시다.
fibo x y = x : (fibo y (x+y))
fibonacci = fibo 1 1

3.8. 입출력


입출력을 처리하기 위해서 모나드라는 개념이 사용되지만 여기서 설명하지는 않겠습니다. 여기서는 대화식(interactive) 함수를 만들기 위해서 사용되는 것이 do 구문입니다. 이는 순서대로 명령어를 수행하도록 강제합니다. (하스켈은 느긋한 언어이기 때문에 보통은 명령어의 순서가 정해지지 않습니다.) Name.hs 파일을 다음과 같이 작성하고 불러옵니다.
module Main where
import IO
main = do
    hSetBuffering stdin LineBuffering
    putStrLn "Please enter your name: "
    name <- getLine
    putStrLn ("Hello, " ++ name ++ ", how are you?")
실행은 다음과 같이 main 함수를 호출하면 됩니다.
*Main> main
Please enter your name:
Hal
Hello, Hal, how are you?
*Main>
hSetBuffering stdin LineBuffering는 버퍼를 설정한다고만 알면 되고, 다음으로 넘어가겠습니다.

putStrLn는 화면에 문자열을 출력하는 함수입니다.

name <- getLine 는 name = getLine 가 돼야하는 것이 아닌가 생각이 들지도 모르겠습니다만, getLine 은 순수한 `함수'가 아니고 부수효과가 있는 `행동'이기 때문에 <- 연산자가 사용되야합니다. 그래서 `getLine이라는 행동을 수행하고 그 결과를 name에 저장하라'는 뜻이 됩니다. 여기서, `행동'은 부수효과를 가지는 명령문이라고 생각하고 넘어갑시다.

그러면 다음과 같이 숫자맞추기 게임인 "Guess.hs" 를 작성해봅시다.
import IO
import Random
main = do
    hSetBuffering stdin LineBuffering
    num <- randomRIO (1::Int, 100)
    putStrLn "I'm thinking of a number between 1 and 100"
    doGuessing num
doGuessing num = do
    putStrLn "Enter your guess:"
    guess <- getLine
    let guessNum = read guess
    if guessNum < num
      then do putStrLn "Too low!"
              doGuessing num
      else if read guess > num
             then do putStrLn "Too high!"
                     doGuessing num
             else do putStrLn "You Win!"
import Random 구문은 Random 모듈에 있는 randomRIO라는 함수를 사용하기 위해 필요합니다. randomRIO (1::Int, 100) 은 1부터 100까지의 정수 중에서 하나의 수를 랜덤하게 반환하는데, 정수라는 것을 알리기 위해 ::Int 가 필요합니다. read guess 는 순수한 함수이기 대문에 <- 구문을 사용할 필요가 없으며 사실 사용할 수도 없습니다. 또, do 구문 내에서는 let 구문에 in이 필요없습니다.

위의 예에서는 return이 필요없었지만 값을 반환하는 함수를 만들기 위해서는 return함수가 필요합니다.
askForWords = do
  putStrLn "Please enter a word:"
  word <- getLine
  if word == ""
    then return []
    else return (word : askForWords)
위 코드는 오류가 있기 때문에 실행되지 않습니다. 앞에서 배웠다시피 : 연산자의 오른쪽에 리스트가 와야하는데 askForWords 는 리스트가 아니고, 리스트를 반환하는 함수도 아닙니다. askForWords는 리스트를 반환하는 행동이기 때문에 오류가 발생합니다. 그러므로 다음과 같이 명시적으로 <- 를 사용하여 작성해야합니다.
askForWords = do
  putStrLn "Please enter a word:"
  word <- getLine
  if word == ""
    then return []
    else do
      rest <- askForWords
      return (word : rest)

4. 형(type)


하스켈은 정적형 언어(static type language)입니다. 이 말의 의미는, 하스켈에는 모든 표현식에는 형이 배정된다는 것입니다. 예를 들면, 'a'의 형은 Char입니다. 함수의 인자에도 형이 필요합니다. 만약 다른 형을 인자로 넣게 되면 컴파일 오류가 발생합니다. 이런 특징으로 인해 많은 버그를 줄일 수 있습니다. 게다가 하스켈은 형 추론 시스템을 사용합니다. 이는 모든 표현의 형을 지정해줄 필요가 없다는 말입니다. C에서는 변수를 선언할 때 int, char, .. 같은 형을 지정해야했지만, 하스켈에서는 형을 문맥에서 추론하게 됩니다. 또한 형을 명시적으로 지정하는 것도 가능합니다. 이 경우에는 디버깅하는데 도움을 줄 것입니다.

Hugs 와 GHCi 에서는 :t 명령어로 표현식의 형을 알아낼 수 있습니다.
Prelude> :t 'c'
'c' :: Char
이를 통해 'c' 는 Char형이라는 것을 알 수 있습니다.

4.1. 기본형


내장된 기본형에는, Int (정수), Double (실수), Char (문자), String (문자열) 등이 있습니다. 이번에는 문자열의 형을 알아봅시다.
Prelude> :t "Hello"
"Hello" :: String
좀 더 복잡한 수식에 대해서도 할 수 있습니다.
Prelude> :t 'a' == 'b'
'a' == 'b' :: Bool
여기서 Bool은 참 또는 거짓값을 가지는 Boolean(`불리언'이라고 발음되는)의 약자입니다. 만약 형이 다른 값을 비교연산자를 이용하여 비교하면 다음과 같이 오류가 발생합니다.
Prelude> :t 'a' == "a"
ERROR - Type error in application
*** Expression     : 'a' == "a"
*** Term           : 'a'
*** Type           : Char
*** Does not match : [ Char ]
앞에서 배웠듯이 :: 를 이용하여 형을 명시적으로 지정할 수 있습니다.
Prelude> :t (5 :: Int)
(5 :: Int) :: Int
Prelude> :t (5 :: Double)
(5 :: Double) :: Double
5라는 값은 정수형 또는 실수형으로 지정할 수 있다는 것을 발견할 수 있습니다. 그럼, 형을 지정하지 않으면 어떤 결과가 나오는지 살펴봅시다.
Prelude> :t 5
5 :: Num a => a
이에 대한 자세한 내용은 `형 클래스'에서 살펴보기로 하고 넘어갑시다.

4.2. 다형적 형


하스켈에는 다형적 형 시스템을 가지고 있습니다. 즉, 형 변수를 사용할 수 있습니다. tail 함수를 살펴보면,
Prelude> tail [5,6,7,8,9]
[6,7,8,9]
Prelude> tail "hello"
"ello"
Prelude> tail ["the","man","is","happy"]
["man","is","happy"]
리스트 내부의 요소의 형에 상관없이 사용할 수 있다는 것을 알 수 있습니다. 이는 tail 함수는 다형적 형 a a 를 사용하기 때문입니다. 이로 인해 임의의 형을 다룰 수 있습니다.
Prelude> :t tail
tail :: [a] -> [a]
fst도 살펴볼겠습니다.
Prelude> :t fst
fst :: (a, b) -> a
그러므로 a, b의 형에 상관없이, fst 함수는 (a, b) 에서 a 값을 반환합니다.

4.3. 형 클래스


앞에서 숫자 5라는 값은 정수형 또는 실수형으로 지정할 수 있다는 것과 형 클래스와 관련이 있다는 것을 배웠습니다. 이제 형 클래스에 대해서 좀 더 자세히 알아봅시다.

4.3.1. 형 클래스의 필요성


많은 언어에서는 오버로딩이라는 시스템이 있습니다. 즉, 함수의 인자가 다르면 함수의 동작이 달라집니다. 예를 들어 두 객체를 비교하는 equal 함수가 있다고 하면, 정수를 비교하기 위한 방법과, 실수를 비교하기 위한 동작, 문자를 비교하기 위한 동작, 문자열을 비교하는 동작 모두 다릅니다. 일반적으로 우리가 α형의 값 두 개를 비교하고 싶을 때, α-compare을 사용한다고 합시다. α는 형 정보를 저장하는 변수이므로, 형 변수라고 합니다.

그러나 불행하게도 정적형 시스템에서는 몇가지 문제가 있습니다. 어떤 형에서 어떤 동작이 정의돼야할지를 형 검사기가 알지 못합니다. 이런 문제를 풀기 위한 많은 방법들이 있는데, (사실 별로 많지는 않습니다.) 하스켈에서 이런 문제를 해결하는 방식은 형 클래스입니다.

4.3.2. equality 테스트


== 라는 함수(또는 연산자, 하스켈에서는 연산자가 곧 함수임.)가 두 값을 비교하는 경우를 살펴봅시다. 우선 두 값의 형이 같아야 하고, (이를 α 라고 합시다.) 반환형은 Bool 입니다. == 라는 함수는 몇몇 형에서만 정의돼있습니다. (모든 형에 정의된 것이 아닙니다.) 그리고 == 함수는 Eq라는 형 클래스와 결합돼있습니다. `결합돼있다'는 의미는, α라는 형이 Eq 클래스에 속해있다면 == 함수가 α형에 대해 정의돼있다는 것을 뜻합니다. 이때, α는 Eq 클래스의 인스턴스라고 합니다. 예를들어 Int는 Eq의 인스턴스입니다. 그리고 하스켈에서는 형 클래스를 `클래스'라고 부르기도 합니다.

4.3.3. Num 클래스


하스켈에서 == 와 같이 연산자를 오버로딩할 뿐만 아니라, 숫자 상수 (1, 2, 3, ... 등) 들도 오버로딩합니다. 숫자 5와 같은 경우는 Num 클래스로 정의되어있고, Num 클래스의 인스턴스에는 기본 숫자형인 Int와 Double가 정의되어있습니다. 그러므로 숫자 5는 컴파일러가 볼때, 정수형으로 볼 수도 있고, 실수형으로 볼 수도 있게 됩니다.

4.3.4. Show 클래스


Show 클래스의 show 함수를 사용하면 문자열이 아닌 것을 문자열로 변환할 수 있습니다.
Prelude> show 5
"5"
Prelude> show 'a'
"'a'"
Prelude> show "Hello World"
"\"Hello World\""
문자는 작은 따옴표로 둘러싸고, 문자열은 큰 따옴표로 둘러싸게 됩니다. 마지막 줄에 역슬래쉬가 있는데, 이는 대화식 환경에서 특수문자를 출력하기 위한 것입니다. 실제 문자열은 역슬래쉬를 포함하지 않습니다.

4.4. 함수의 형


1이나 'c'과 같은 값들은 형을 가지고 있다고 했습니다. 그런데 하스켈에서는 함수도 역시 형을 가지고 있습니다.

4.4.1. 람다 대수 (Lamda Calculus)


`람다 대수' 은 함수를 표현하는 간단한 시스템입니다. 제곱을 구하는 함수는 다음과 같은 `람다식'으로 쓸 수 있습니다. λx.x*x

λ는 람다 추상 또는 람다라고 부르며, 람다는 하나의 인자만을 가질 수 있습니다. 그러므로 두개의 인자를 받기 위해서는 람다가 2개가 필요합니다. 예를 들어 다음과 같습니다. λxλy.2*x+y

람다식을 계산하기 위해서는 가장 바깥쪽에 있는 λ 를 없애고 람다 변수를 모두 그 값으로 변환합니다. 예를 들어 (λx.x * x)5 를 계산하려면 λx 를 없애고 (x * x) 에서 x 를 5로 바꾸면 (5 * 5) 가 되므로 25가 됩니다.

하스켈에서는 람다식을 사용할 수 있는데, λ는 \ 로 바꾸고, . 은 -> 로 바꾸면 됩니다. 그리고 여러 개의 λ는 하나의 \로 바뀝니다.
square = \x -> x*x
f = \x y -> 2*x + y
하스켈에서는 람다계산도 가능합니다.
Prelude> (\x -> x*x) 5
25
Prelude> (\x y -> 2*x + y) 5 4
14

4.4.2. 고차원 형


다른 값들과는 다르게, 함수의 형을 나타내기 위해서는 고차원 형이 필요합니다. 다시 제곱함수 λx.x+x 를 생각해 봅시다.

만약 x가 Int라고 하면, 제곱함수는 하나의 정수형 숫자을 받아서 정수형의 제곱값을 반환하게 됩니다. 이 때, 제곱함수의 형을 Int → Int 라고 합시다. 이제, λxλy.2*x+y 라는 함수 f를 생각해봅시다. x 에 5를 대입하면, 위에서 배운 것처럼 (λxλy.2x + y)5 가 되고 이는 λy.10 + y 가 됩니다. 그런데 이 결과값은 Int → Int 라는 함수가 나왔습니다. 그러므로 함수 f 는 Int 형의 인자를 받아서 Int → Int 라는 함수를 결과값으로 내보내므로, 함수 f의 형은 Int → (Int → Int) 가 됩니다. 여기서 괄호는 없어도 상관없습니다. 즉, (Int → Int) → Int 나 Int → Int → Int 모두 같습니다. 일반화해서 Int가 아닌 a라는 형에 적용시켜보면, 함수 f의 형은 a → a → a 로 나타낼 수 있습니다.

그럼 몇가지 기본 함수들의 형을 알아봅시다.
Prelude> :t head
head :: [a] -> a
Prelude> :t tail
tail :: [a] -> [a]
Prelude> :t null
null :: [a] -> Bool
Prelude> :t fst
fst :: (a,b) -> a
Prelude> :t snd
snd :: (a,b) -> b
중위연산자에 괄호를 붙이면 함수처럼 다룰 수 있으므로, 다음과 같이 할 수도 있습니다.
Prelude> :t (+)
(+) :: Num a => a -> a -> a
Prelude> :t (*)
(*) :: Num a => a -> a -> a
Prelude> :t (++)
(++) :: [a] -> [a] -> [a]
Prelude> :t (:)
(:) :: a -> [a] -> [a]

4.4.3. 성가신 IO 형


입출력 함수들은 어떤 형을 가졌는지 살펴봅시다.
Prelude> :t putStrLn
putStrLn :: String -> IO ()
Prelude> :t readFile
readFile :: FilePath -> IO String

사실 이들은 부수효과가 있는 `행동'입니다. 이런 차이로 인해 행동과 순수한 함수를 결합할 수 있는 방법은 do 구문 뿐입니다. 만약 어떤 파일로부터 문자열을 받아서, 문자열을 숫자를 바꾸는 함수 f 를 이용하여 숫자로 바꾸고, 이를 화면에 출력하기 위해서는 다음과 같이 해야합니다.
main = do
    s <- readFile "somefile"
    let i = f s
    putStrLn (show i)
여기서 <- 구문은 입출력 행동으로부터 문자열을 받는다는 뜻이며, 그 이후에 f 함수를 사용할 수 있습니다. 반면에 f가 행동이 아닌 순수한 함수이기 때문에 i <- f s 라고 쓸 수는 없습니다.

4.4.4. 명시적 형 선언


명시적 형 선언은 다음과 같은 면에서 장점이 있습니다. 명확성, 속도, 디버깅

형 선언은 함수의 정의와는 별도로 이뤄집니다.
square :: Num a => a -> a
square x = x*x
두 문장이 인접해야할 필요는 없습니다. 다만, 함수정의에 의해 추론된 형과 일치하거나 더 구체적이어야합니다. 더 구체적이라는 의미는, 다음과 같이 함수인자를 정수형으로 제한하는 것을 의미합니다.
square :: Int -> Int
square x = x*x
확장기능을 이용해서 (Hugs 에서는 "-98" 또는 GHC(i) 에서는 "-fglasgow-exts" 로 실행옵션을 주고 실행한다.) 다음과 같이 함수정의와 함께 형을 지정해 줄 수도 있습니다.
square (x :: Int) = x*x
다음과 같이 대화식 환경에서 square 함수의 형을 알아봅시다.
Prelude> let square (x :: Int) = x*x
Prelude> :t square
square :: Int -> Int
물론, 다음과 같이 람다식을 사용할 수도 있습니다.
Prelude> :t (\(x :: Int) -> x*x)
그러면 이번에는 Int 형을 Double 형으로 바꾸는 함수 intToDouble을 만들어봅시다. realToFrac이라는 표준함수는 Real 클래스의 형을 Fractional 클래스의 형으로 바꿔줍니다.
Prelude> :t realToFrac
realToFrac :: (Fractional b, Real a) => a -> b
그러면 intToDouble을 만들기 위해서는 형 선언을 하고 realToFrac을 이용하면 됩니다.
intToDouble :: Int -> Double
intToDouble = realToFrac
*MyModule> let x = intToDouble 7
*MyModule> x
7.0
*MyModule> :t x
x :: Double

4.4.5. 함수를 인자로 사용하기


map이나 filter 같은 함수는 또 다른 함수를 인자로 사용합니다. 먼저 map 함수를 생각해봅시다. 리스트를 인자로 받아서 또 다른 리스트를 반환합니다. 이때 두 리스트의 형은 다를 수도 있습니다. 이 형은 인자로 받는 함수의 형에 의해 결정됩니다. 그러므로 map의 형은 (a → b) → ab 가 됩니다. filter 함수의 형은 (a → Bool) → aa 입니다. 그러면 foldl 의 형은 무엇일까요? 아마 (a → a → a) → a → a → a 를 생각했을지 모릅니다. 그러나 실제로는 이보다는 더 일반적인 (a → b → a) → a → b → a 가 foldl의 형입니다. 비슷하게 foldr 의 형은 (a → b → b) → b → a → b 입니다.

리스트의 요소의 형과 초기값의 형이 다를 수도 있다는 사실을 이용하여 함수를 하나 만들어봅시다. 리스트의 요소 중에서 어떤 조건을 만족하는 요소의 개수를 반환하는 함수 count를 정의해봅시다. 물론 length와 filter를 사용해서 my_count p l = length (filter p l) 로 만들수도 있지만, 이번에는 foldr 를 사용해서 만들어봅시다.
module Count where
my_count p l = foldr (\x c -> if p x then c+1 else c) 0 l
이 때, my_count 함수의 형은 (Num b) => (a → Bool) → a → b 가 됩니다.

4.5. 자료형

순서쌍과 리스트도 훌륭한 자료구조이지만, 자신만의 자료구조를 정의하고 싶을 때도 있습니다. 이런 자료형은 data 구문을 이용하여 정의할 수 있습니다.

4.5.1. Pair

Pair 라는 순서쌍을 나타내는 자료구조를 정의해봅시다.
data Pair a b = Pair a b
좌변을 살펴보면, data는 자료형을 선언한다는 것을 알려주고, Pair는 자료구조의 이름을 나타내고, a b 는 자료형의 매개변수의 형이 각각 a와 b라는 것을 나타냅니다. 우변을 살펴보면, Pair는 생성자를 나타냅니다. 이 생성자는 자료형의 이름과 일치할 필요는 없지만, 이 경우에는 자료형의 이름과 같게 정했습니다. 뒷 부분의 a b 는 생성자 Pair의 인자로 받는 두 개의 값의 형이 각각 a와 b라는 것을 나타냅니다. 그러면 Pair 생성자(생성자도 역시 함수입니다.)의 형을 살펴봅시다.
Datatypes> :t Pair
Pair :: a -> b -> Pair a b
Datatypes> :t Pair 'a'
Pair 'a' :: b -> Pair Char b
Datatypes> :t Pair 'a' "Hello"
Pair 'a' "Hello" :: Pair Char [ Char ]
그러면 다음과 같이 fst snd 함수와 유사한 함수를 만들어 봅시다.
pairFst (Pair x y) = x
pairSnd (Pair x y) = y
물론 다음과 같이 _ 문자를 사용할 수도 있습니다.
pairFst (Pair x _) = x
pairSnd (Pair _ y) = y

4.5.2. 다중 생성자


리스트의 요소 중에서 어떤 조건을 만족하는 첫번째 요소를 반환하는 함수를 작성해봅시다.
findElement p (x:xs) =
    if p x
      then x
      else findElement p xs
만약 위와 같이 작성했다면, 조건을 만족하는 요소가 하나도 없는 경우는 오류가 발생합니다. 그러므로 조건을 만족하는 요소가 없는 경우는 예외처리를 해야합니다. error함수를 이용하여 오류를 발생시킬 수도 있지만, 그렇게 하면 오류를 다루기가 어렵다는 문제점이 있습니다. 다른 방법으로는 오류를 처리할 수 있는 자료형을 이용하는 것입니다. Maybe는 하스켈의 기본자료형인데, Prelude에 다음과 같이 정의돼있습니다.
data Maybe a = Nothing
             | Just a
Maybe a 라는 자료형을 생성하는 방법에는 두 가지가 있습니다. Nothing 생성자를 이용하는 방법과, a형의 값과 함께 Just 생성자를 이용하는 것입니다.
Prelude> Nothing
Prelude> Just "foobar"
그러면 my_head 함수를 만들어 봅시다. 이 함수는, 빈 리스트인 경우는 Nothing을 반환하고, 그렇지 않으면 첫번째 요소를 반환합니다.
my_head :: [a] -> Maybe a
my_head []     = Nothing
my_head (x:xs) = Just x
이제 findElement를 다시 만들어 봅시다.
findElement :: (a -> Bool) -> [a] -> Maybe a
findElement p [] = Nothing
findElement p (x:xs) =
    if p x then Just x
    else findElement p xs
이것으로 findElement 함수의 예외처리가 끝났습니다.

또 한가지 유용한 자료형은 Either 입니다.
data Either a b = Left a
                | Right b
Either 자료형은 a형이나 b형이 될 수 있습니다.

4.5.3. 재귀적 자료형


다음과 같이 리스트를 정의하기 위해서 재귀적 자료형을 사용할 수 있습니다. 재귀적 자료형이란, 정의에 자기 자신을 포함하는 것을 말합니다.
data List a = Nil
            | Cons a (List a)
이 List는 하스켈에 내장된 리스트와 거의 같습니다. length에 해당되는 함수도 만들어보면 다음과 같습니다.
listLength Nil = 0
listLength (Cons x xs) = 1 + listLength xs

4.5.4. 이진 나무 (Binary Tree)


이번에는 리스트보다 더 복잡한 자료구조를 만들어봅시다.
data BinaryTree a
    = Leaf a
    | Branch (BinaryTree a) a (BinaryTree a)
이 이진 나무의 크기를 구하는 함수를 정의해봅시다.
treeSize (Leaf x) = 1
treeSize (Branch left x right) =
  1 + treeSize left + treeSize right

4.5.5. Enumerated Sets


이번에는 (다른 언어에서 enum 으로 부르는) 열거형을 만들어봅시다.
data Color
    = Red
    | Orange
    | Yellow
    | Green
    | Blue
    | Purple
    | White
    | Black
프로그램에서 다루기 좋도록 색이름을 RGB 값으로 바꾸는 함수를 만들면,
colorToRGB Red    = (255,0,0)
colorToRGB Orange = (255,128,0)
colorToRGB Yellow = (255,255,0)
colorToRGB Green  = (0,255,0)
colorToRGB Blue   = (0,0,255)
colorToRGB Purple = (255,0,255)
colorToRGB White = (255,255,255)
colorToRGB Black = (0,0,0)
임의의 RGB 값으로 색을 만들기 위해서 생성자를 하나 더 추가해봅시다.
data Color
    = Red
    | Orange
    | Yellow
    | Green
    | Blue
    | Purple
    | White
    | Black
    | Custom Int Int Int -- R G B components
colorToRGB 함수에도 한 가지 더 추가합시다.
colorToRGB (Custom r g b) = (r,g,b)

4.5.6. Unit 형


하스켈의 Prelude에 정의된 자료형 중에서 Unit형이 있습니다.
data () = ()
Unit형이 가질 수 있는 유일한 값은 () 입니다. 이는 C나 Java에서의 void와 근본적으로 같습니다. 입출력을 다룰 때 유용하게 사용됩니다.


5. 기본 입출력


기본 입출력함수에는 다음과 같이 최소한 네 종류가 있습니다. 순수한 함수는 아니므로 `행동'이라고 부르겠습니다.

  • 화면에 문자열을 출력함
  • 키보드로부터 문자열을 입력받음
  • 파일읽기
  • 파일쓰기

첫번째 행동은 문자열 입력을 받아서 무엇인가를 반환해야합니다. 그 무엇인가는 일단 () 라고 합시다. 두번째 행동의 반환값은 String이고 인자는 필요없을 것처럼 보입니다. 앞에서도 말했지만 이들은 순수한 함수가 아닙니다. 두번째 행동은, 매번 같은 String을 반환하지 않기 때문입니다. 첫번째 행동이 순수한 함수라면, 참조적 투명성에 의해 f _ = () 로 대체될 수 있어야 하지만, 그렇지 않습니다.

5.1. RealWorld


이들 행동이 순수한 함수가 아닌 이유는 실세계와 상호작용하기 때문입니다. RealWorld라는 형이 있다고 생각하고, 다음과 같이 정의해봅시다.
printAString :: RealWorld -> String -> RealWorld
readAString :: RealWorld -> (RealWorld, String)
이렇게 RealWorld를 하나의 값으로 생각하면, 3.8 에서 다뤘던 Name.hs를 다음과 같이 바꿔서 생각할 수 있습니다. RealWorld의 상태를 main 함수의 인자로 받는 것입니다.
-- 의사코드(pseudo code)임. 컴파일 오류 발생함.
main rW =
    let rW' = printAString rW "Please enter your name: "
        (rW'', name) = readAString rW'
    in printAString rW'' ("Hello, " ++ name ++ ", how are you?")
하지만 그렇게 하면, 항상 RealWorld 인자를 전달해야하므로 읽기도 어렵고 코딩하기도 번거롭습니다.

5.2. 행동


입출력을 다루기 위해 Phil Wadler가 생각해낸 것은 `모나드' 입니다. 입출력을 다루는 모나드를 IO 모나드라고 부릅니다. 함수의 형에 IO 모나드가 들어가면 입출력함수라고 보면 됩니다. (물론 항상 그런 것은 아닙니다.) 먼저 putStrLn의 형을 살펴보면,
putStrLn :: String -> IO ()
예상대로 인자는 String를 받습니다. 반환값은 IO () 라는 형입니다. IO 모나드이므로 putStrLn가 행동이라는 것을 짐작할 수 있습니다. (행동을 만들기 위해서 IO 모나드를 만들었기 때문입니다.) 그리고 이 행동의 결과값은 () 라는 형을 갖습니다. getLine의 형은 짐작한대로 다음과 같습니다.
getLine :: IO String
앞에서 배운 대로 do 구문을 사용하여 여러 개의 행동을 사용할 수 있습니다.
main = do
    hSetBuffering stdin LineBuffering
    putStrLn "Please enter your name: "
    name <- getLine
    putStrLn ("Hello, " ++ name ++ ", how are you?")
do 구문내에서도 if/then/else 구문, case/of 구문, where 구문을 사용할 수 있습니다.

예를 들어 숫자 맞추기 게임을 생각해봅시다.
do ...
   if (read guess) < num
     then do putStrLn "Too low!"
             doGuessing num
     else if read guess > num
            then do putStrLn "Too high!"
                    doGuessing num
            else do putStrLn "You Win!"
if/then/else 구문에서는 세 가지가 필요합니다. 조건문, `then' branch, 그리고 `else' branch. 조건문의 형은 Bool이어야 합니다. `then' branch와 `else' branch는 같은 형이어야 합니다. if/then/else의 형은 `then' branch와 `else' branch의 형과 같습니다.

그럼 `then' branch 를 살펴봅시다.
do putStrLn "Too low!"
   doGuessing num
첫번째 줄의 putStrLn "Too low!" 는 IO () 형입니다. 두번째 줄도 IO () 형입니다. do 구문 전체의 형은 마지막 표현식의 형과 같으므로 `then' branch의 형은 역시 IO () 입니다. 마찬가지 방식으로 `else' branch의 형을 살펴보면 IO () 라는 것을 알 수 있으며, if/then/else의 형은 IO () 가 됩니다. 한가지 주의할 점은, `do 구문을 시작했으니 더 이상 do 는 필요없겠지'라는 생각으로 다음과 같이 쓰는 것은 옳지 않다는 점입니다.
do ...
   if (read guess) < num
     then putStrLn "Too low!"
          doGuessing num
     else ...
왜냐하면 then 뒤에 두 개의 표현식(putStrLn "Too low!" 과 doGuessing num)이 올 수 없기 때문입니다. 뒤에 여러 개의 표현식이 오기 위해서는 do, let, where, of 가 반드시 필요합니다.

이번에는 case 구문을 살펴봅시다.
doGuessing num = do
    putStrLn "Enter your guess:"
    guess <- getLine
    case compare (read guess) num of
        LT -> do putStrLn "Too low!"
                 doGuessing num
        GT -> do putStrLn "Too high!"
                 doGuessing num
        EQ -> putStrLn "You Win!"
여기서 compare 함수는 (Ord 클래스의) 같은 형의 값 두 개를 비교하여, 첫번째가 크면 GT, 같으면 EQ, 첫번째가 작으면 LT 을 반환합니다.

C나 Java와는 달리 return이 사용되지 않는지 의문을 가질지 모릅니다. 하스켈에서 return는 흐름제어를 하는 구문이 아닙니다. 단지 어떤 값을 모나드 값으로 바꿔주는 행동에 불과합니다.
Prelude > :t return
return :: (Monad m) => a -> m a

위에서 만든 것과 같은 프로그램을 명령형 언어로 다음과 같이 작성할 수도 있습니다.
void doGuessing(int num) {
  print "Enter your guess:";
  int guess = atoi(readLine());
  if (guess == num) {
    print "You win!";
    return ();
  }
  // we won't get here if guess == num
  if (guess < num) {
    print "Too low!";
    doGuessing(num);
  } else {
    print "Too high!";
    doGuessing(num);
  }
}

이와 비슷하게 이번에는 하스켈로 작성해봅시다.
doGuessing num = do
  putStrLn "Enter your guess:"
  guess <- getLine
  case compare (read guess) num of
    EQ -> do putStrLn "You win!"
             return ()
  -- guess == num 인 경우에 이 밑에 식은 실행되지 않을까?
  if (read guess < num)
    then do print "Too low!";
            doGuessing
    else do print "Too high!";
            doGuessing
그러나 이 프로그램은 원하는대로 동작하지 않습니다. (read guess) == num 인 경우에는 "You win!" 이라는 문자열이 출력되지만 프로그램이 종료하지 않고, if 문을 실행합니다. if문 안의 조건문이 참이 아니기 때문에 "Too high!" 라는 문자열이 출력되고 프로그램은 계속 돌아갑니다. 반면에 (read guess) == num 이 아닌 경우에는 case 문의 계산결과는 LT 이나 GT 가 되는데 이는 case 문에 없으므로 예외를 발생하고 프로그램은 종료됩니다.

5.3. 입출력 라이브러리


입출력 라이브러리는 IO 모듈을 불러옴(import)으로써 사용할 수 있습니다.
data IOMode  = ReadMode  |  WriteMode  |  AppendMode  |  ReadWriteMode
openFile     :: FilePath -> IOMode -> IO Handle
hClose       :: Handle -> IO ()
hIsEOF       :: Handle -> IO Bool
hGetChar     :: Handle -> IO Char
hGetLine     :: Handle -> IO String
hGetContents :: Handle -> IO String
getChar      :: IO Char
getLine      :: IO String
getContents  :: IO String
hPutChar     :: Handle -> Char -> IO ()
hPutStr      :: Handle -> String -> IO ()
hPutStrLn    :: Handle -> String -> IO ()
putChar      :: Char -> IO ()
putStr       :: String -> IO ()
putStrLn     :: String -> IO ()
readFile     :: FilePath -> IO String
writeFile    :: FilePath -> String -> IO ()
bracket      :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c
(여기서 FilePath 는 String 과 같습니다. 이런 것을 형 동의어라고 합니다.)

bracket 함수는 행동을 안전하게 수행하기 위해서 사용됩니다. bracket 함수는 세 개의 인자가 필요합니다. 첫번째 인자로 오는 행동이 가장 먼저 수행됩니다. 두번째 인자는, 오류가 있든 없든 가장 나중에 수행됩니다. 세번째 인자는 중간에 수행되는데, 이는 오류가 발생할 수도 있는 행동이 오게 됩니다. 예를들어 다음과 같이 파일에 문자 하나를 저장하는 함수를 만들 수 있습니다.
writeChar :: FilePath -> Char -> IO ()
writeChar fp c =
    bracket
        (openFile fp ReadMode)
        hClose
        (\h -> hPutChar h c)

5.4. 파일 읽기 프로그램


이번에는 파일을 읽는 프로그램을 작성해봅시다. 오류처리가 안된 미완성 프로그램이지만, 입출력 함수를 어떻게 사용하는지 배울수 있는 좋은 예제가 될 것입니다. 다음 코드를 FileRead.hs 파일로 저장하고 컴파일 후 실행해봅시다.
module Main where
import IO
main = do
    hSetBuffering stdin LineBuffering
    doLoop
doLoop = do
    putStrLn "Enter a command rFN wFN or q to quit:"
    command <- getLine
    case command of
        'q':_ -> return ()
        'r':filename -> do putStrLn ("Reading " ++ filename)
                           doRead filename
                           doLoop
        'w':filename -> do putStrLn ("Writing " ++ filename)
                           doWrite filename
                           doLoop
        _            -> doLoop
doRead filename =
    bracket (openFile filename ReadMode)
            hClose
            (\h -> do contents <- hGetContents h
                      putStrLn "The first 100 chars:"
                      putStrLn (take 100 contents))
doWrite filename = do
    putStrLn "Enter text to go into the file:"
    contents <- getLine
    bracket (openFile filename WriteMode)
            hClose
            (\h -> hPutStrLn h contents)


6. 고급 기능

6.1. 섹션과 중위연산자


다음과 같이 map 함수와 람다 함수를 사용하면 리스트의 모든 요소의 값을 두 배로 만들 수 있습니다.
Prelude> map (\x -> x*2) [1,2,3,4]
[2,4,6,8]
물론 다음과 같이 할 수도 있습니다.
Prelude> map (\x -> x*2) [1..4]
[2,4,6,8]
그런데 이를 더 간단하게 나타낼 수 있습니다.
Prelude> map (*2) [1..4]
[2,4,6,8]
Prelude> map (2*) [1..4]
[2,4,6,8]
이는 중위연산자는 모두 이런 방식으로 사용할 수 있습니다.
Prelude> map (+5) [1..4]
[6,7,8,9]
Prelude> map (/2) [1..4]
[0.5,1.0,1.5,2.0]
Prelude> map (2/) [1..4]
[2.0,1.0,0.666667,0.5]
Prelude> map (5-) [1..4]
[4,3,2,1]
그러나 - 연산자에 대해서는 주의가 필요합니다. map (-2) 1..4 와 같이 하면, 여기서 - 는 이항연산자인 `빼기'연산자로 보는 것이 아니고 단항연산자로 보므로 여기서 -2 는 λx.x - 2 라는 함수가 아니고 숫자 -2 가 됩니다. 그러므로 다음과 같이 합시다.
Prelude> map (+(-2)) [1..4]
[-1,0,1,2]
Prelude> map (-2+) [1..4]
[-1,0,1,2]
(반면에, 다른 언어와는 다르게 하스켈에서는 +5 는 숫자 5로 보지않고 λx.x + 2 라는 함수로 봅니다.)

괄호를 사용하면 중위연산자를 함수처럼 사용할 수 있다고 배웠습니다.
Prelude> (+) 5 3
8
Prelude> (-) 5 3
2
다음과 같이 중위연산자와 인자를 함께 괄호로 묶으면 `섹션'이 됩니다.
Prelude> (+5) 3
8
Prelude> (/3) 6
2.0
Prelude> (3/) 6
0.5
중위 연산자가 아닌 함수는 ` 로 둘러싸면 중위 연산자처럼 사용할 수 있습니다.

그럼 지금까지 배운 것을 복습해봅시다.
Prelude> (+2) `map` [1..10]
[3,4,5,6,7,8,9,10,11,12]

6.2. 지역 선언


챕터 3.5의 이차방정식의 근을 구하는 프로그램을 다시 살펴봅시다.
roots a b c =
    ((-b + sqrt(b*b - 4*a*c)) / (2*a),
     (-b - sqrt(b*b - 4*a*c)) / (2*a))
let/in 구문을 사용할 수도 있지만 이번에는 where 구문을 사용해봅시다.
roots a b c =
    ((-b + det) / (2*a), (-b - det) / (2*a))
    where det = sqrt(b*b-4*a*c)
where에서 정의된 값은 외부에서는 사용할 수 없습니다.
det = "Hello World"
roots a b c =
    ((-b + det) / (2*a), (-b - det) / (2*a))
    where det = sqrt(b*b-4*a*c)
f _ = det
where 구문의 det 정의는 최상위계층에는 영향을 주지 않습니다. 그러므로 f라는 함수는 결과값으로 "Hello World" 를 반환하는 함수가 됩니다.

where 구문에 두 개 이상의 표현식이 올 수도 있습니다.
roots a b c =
    ((-b + det) / (a2), (-b - det) / (a2))
    where det = sqrt(b*b-4*a*c)
          a2 = 2*a
let/in 구문으로도 가능합니다.
roots a b c =
    let det = sqrt (b*b - 4*a*c)
        a2 = 2*a
    in ((-b + det) / a2, (-b - det) / a2)
이 두 가지 스타일을 섞어서 사용하지 않기를 강력히 권합니다.
f x =
  let y = x + 1
  in y
  where y = x + 2
f 5 의 값은 7이 아니고 6입니다. 굳이 이유를 설명하자면 let 구문에서 where 는 보이지 않기 때문인데, 이런 식으로 섞은 구문은 사용하지 않기를 바랍니다.

let/in 과 where 의 선택은 개인의 기호에 달린 문제이지만, 개인적인 생각으로는 where가 더 많이 쓰이는 것 같습니다.

6.3. 함수의 부분적용


n개의 인자를 필요로 하는 함수가 있는 경우, n 보다 적은 수의 인자만 사용하는 것을 함수의 부분적용이라고 합니다. 예를 들어, map (+1) 1,2,3 에서 (+1) 는 + 함수의 부분적용입니다. 왜냐하면 + 함수는 인자가 두 개가 필요한데, 하나만 사용됐기 때문입니다. 이러한 함수의 부분적용은 함수의 정의에도 사용할 수 있습니다. 문자열을 소문자로 바꾸는 함수 lcaseString을 생각해봅시다.
lcaseString s = map (\x->toLower x) s
그리고 toLower 함수에 부분적용을 이용하면 다음과 같이 바꿀 수 있습니다.
lcaseString s = map toLower s
그러면 이제 map 함수에도 부분적용을 이용해봅시다.
lcaseString = map toLower
이를 η-변환이라고도 합니다.

어떻게 이런 일이 가능한지 생각해봅시다. map은 인자를 두 개 받는데, 하나의 함수이고, 다른 하나는 리스트입니다. 앞에서도 말했듯이, map의 형은 (a → b) → (ab) 입니다. toLower 함수의 형은 Char → Char 입니다. 그러므로 map 함수에 toLower 함수를 인자로 사용하면 그 결과값은 Char Char 가 됩니다. 그러므로 lcaseString의 형은 Char Char 이 되고, 이는 우리가 원하는 결과와 같습니다.

이번에는 함수합성을 이용하여, 문자열에서 알파벳이 아닌 문자를 제외하고 그 문자열을 소문자로 바꾸는 함수를 만들어봅시다.
lcaseLetters s = map toLower (filter isAlpha s)
그리고 . 연산자를 이용하면 다음과 같이 바꿀 수 있습니다.
lcaseLetters s = (map toLower . filter isAlpha) s
부분적용을 이용하면 다음과 같습니다.
lcaseLetters = map toLower . filter isAlpha
이번에는 `$' 연산자에 대해서 알아보겠습니다. `$' 의 정의는 매우 간단합니다.
f $ x = f x
$의 우선순위가 매우 낮기 때문에 괄호대신에 사용할 수 있습니다. 만약 다음과 같은 함수정의가 있다면,
foo x y = bar y (baz (fluff (ork x)))
다음과 같이 $ 를 사용하여 바꿀 수 있습니다.
foo x y = bar y $ baz $ fluff $ ork x
짝을 요소로 가진 리스트를 인자로 하고, 첫번째 요소가 0보다 큰 짝들만 뽑아내는 함수를 만들어봅시다.
fstGt0 l = filter (\ (a,b) -> a > 0) l
η-변환에 의해 다음과 같고,
fstGt0 = filter (\ (a,b) -> a > 0)
fst 함수를 이용하면 다음과 같습니다.
fstGt0 = filter (\x -> fst x > 0)
연산자의 부분적용과 함수합성을 이용하면 다음과 같고,
fstGt0 = filter (\x -> ((>0) . fst) x)
마지막으로 부분적용을 이용하면 다음과 같습니다.
fstGt0 = filter ((>0).fst)
이렇게 부분적용을 이용하여 함수를 정의하는 스타일을 point-free 프로그래밍이라고 합니다. 함수의 정의에서 인자를 빼버리고, 함수가 동작하는 것에 초점을 맞췄기 때문에 point-free 라는 이름이 붙었습니다. 이런 point-free 프로그래밍을 도와주는 몇몇 함수들이 있습니다. uncurry 함수는 a → b → c 형의 함수를 (a, b) → c 형의 함수로 바꿔줍니다. 어떻게 사용하는지 한 번 봅시다.
Prelude> map (uncurry (*)) [(1,2),(3,4),(5,6)]
[2,12,30]
curry 함수는 uncurry 함수의 반대입니다.

flip 함수는 인자의 순서를 바꿉니다. a → b → c 형의 함수를 인자로 받아서, b → a → c 형의 함수로 바꿉니다.

리스트를 오름차순으로 정렬하기 위해서는 다음과 같이 합니다.
Prelude> List.sortBy compare [5,1,8,3]
[1,3,5,8]
내림차순으로 정렬하기 위해서, 람다함수를 쓸 수도 있지만
Prelude> List.sortBy (\a b -> compare b a) [5,1,8,3]
[8,5,3,1]
flip 함수를 이용하여 좀 더 짧게 만들어 봅시다.
Prelude> List.sortBy (flip compare) [5,1,8,3]
[8,5,3,1]

이런 point-free 프로그래밍이 항상 가능한 것은 아닙니다. 예를 들어 제곱을 구하는 함수의 경우는,
square x = x * x
point-free 방식을 쓸 수 없습니다. 다음과 같이 추가적으로 pair 함수를 정의하면 square 함수는 point-free 방식으로 쓸 수 있지만, pair 함수는 point-free 방식이 아닌데다가 괜히 복잡해지기만 합니다.
pair x = (x,x)
square = uncurry (*) . pair

6.4. 패턴 매칭


챕터 4.5 의 Color 문제로 돌아가봅시다.
data Color
    = Red
    | Orange
    | Yellow
    | Green
    | Blue
    | Purple
    | White
    | Black
    | Custom Int Int Int
    deriving (Show,Eq)

colorToRGB Red = (255,0,0)
colorToRGB Orange = (255,128,0)
colorToRGB Yellow = (255,255,0)
colorToRGB Green = (0,255,0)
colorToRGB Blue   = (0,0,255)
colorToRGB Purple = (255,0,255)
colorToRGB White = (255,255,255)
colorToRGB Black = (0,0,0)
colorToRGB (Custom r g b) = (r,g,b)

대화식 환경에서 다음과 같이 실행해봅시다.
*Color> colorToRGB Yellow
(255,255,0)
자, 이제 어떤 일이 발생했는지를 알아봅시다. 먼저 Yellow라는 값을 가진 어떤 값을 만들어 냅니다. 그 값을 일단은 x라고 부릅시다. 그리고 colorToRGB 함수에 적용해봅니다. 결과값을 계산하기 위해서, colorToRGB 함수의 정의를 살펴봅시다. 정의에 따라서, 먼저 Red와 x가 매치되는지 살펴봅니다. Color의 정의에서 Red와 Yellow는 다른 값으로 정의되어있으므로 이 매치는 실패하게 됩니다. 그 다음에는 Orange와 x가 매치되는지 살펴봅니다. 이 매치도 역시 실패입니다. 그러면 다음에 Yellow와 x가 매치되는지 살피게 되고, 이 매치는 성공합니다. colorToRGB Yellow = (255,255,0) 의 우변은 (255,255,0) 이므로 (255,255,0) 를 반환합니다.

이번에는 Custom 생성자를 사용해봅시다.
*Color> colorToRGB (Custom 50 200 100)
(50,200,100)
이번에도 매치를 해보면, Red부터 Black까지의 매치는 실패합니다. 마지막으로 colorToRGB (Custom r g b) 과 매치해보면, 매치가 성공합니다. 함수 정의에 의해서 r 값은 50이 되고, g 값은 200, b 값은 100이 되므로 반환값은 (50,200,100) 이 됩니다.

이러한 패턴 매칭을 이용하여 isCustomColor 라는 함수를 작성할 수 있습니다.
isCustomColor (Custom _ _ _) = True
isCustomColor _ = False
r,g,b 값 중에서 하나라도 255인 값이 있다면 True를 반환하는 isMaxBright 함수를 작성할 수 있습니다.
isMaxBright = isMaxBright' . colorToRGB
    where isMaxBright' (255,_,_) = True
          isMaxBright' (_,255,_) = True
          isMaxBright' (_,_,255) = True
          isMaxBright' _         = False
그러면 이번에는 반대로 rgbToColor 함수를 만들어봅시다. 한가지 어려운 점은, 예를들어 (600,-40,99) 와 같은 값이 입력으로 들어오는 경우를 예외처리해야한다는 점입니다. 앞에서 배웠듯이 이는 Maybe 자료구조를 이용하여 해결할 수 있습니다.
data Maybe a = Nothing
             | Just a

rgbToColor 255   0   0 = Just Red
rgbToColor 255 128   0 = Just Orange
rgbToColor 255 255   0 = Just Yellow
rgbToColor   0 255   0 = Just Green
rgbToColor   0   0 255 = Just Blue
rgbToColor 255   0 255 = Just Purple
rgbToColor 255 255 255 = Just White
rgbToColor   0   0   0 = Just Black
rgbToColor   r   g   b=
    if 0 <= r && r <= 255 &&
       0 <= g && g <= 255 &&
       0 <= b && b <= 255
      then Just (Custom r g b)
      else Nothing   -- invalid RGB value
위 함수를 이용하여 rgb 값이 올바른지 검사하는 함수를 만들 수 있습니다.
rgbIsValid r g b = rgbIsValid' (rgbToColor r g b)
    where rgbIsValid' (Just _) = True
          rgbIsValid' _        = False
패턴매칭을 사용할 때 한가지 주의할 점은, 자료형에만 매치가 이뤄진다는 점입니다. 그러므로 다음과 같이 함수에 대해서는 매치를 할 수 없습니다.
f x = x + 1
g (f x) = x    -- 잘못된 함수정의

6.5. 가드


패턴 매칭과 비슷하게, 함수의 정의에 가드를 사용할 수 있습니다. 함수의 인자 뒤, 등호 부호의 앞에서 쓸 수 있습니다. 가드는 | 문자를 앞에 붙임으로써 나타냅니다. 그럼 다음 예를 살펴봅시다.
comparison x y | x < y = "The first is less"
               | x > y = "The second is less"
               | otherwise = "They are equal"
*Guards> comparison 5 10
"The first is less"
*Guards> comparison 10 5
"The second is less"
*Guards> comparison 7 7
"They are equal"
그러므로 isMaxBright 함수는 다음과 같이 바꿀 수 있습니다.
isMaxBright2 c | r == 255 = True
               | g == 255 = True
               | b == 255 = True
               | otherwise = False
    where (r,g,b) = colorToRGB c

6.6. 인스턴스 선언


어떤 형을 형 클래스의 인스턴스로 만들기 위해서는, 인스턴스 선언이 필요합니다.

대다수의 클래스에는 "최소한의 완전한 정의"가 제공됩니다. 말그대로 클래스의 정의에 필요한 최소한의 구현되야할 함수들을 의미합니다.

6.6.1. Eq 클래스


Eq 클래스의 멤버 (즉, 함수) 에는 2개가 있습니다.
(==) :: Eq a => a -> a -> Bool
(/=) :: Eq a => a -> a -> Bool
Eq 클래스의 완전한 정의를 위해서는 최소한 위의 두 함수 중 하나가 정의돼야합니다. 이를 정의하기 위해서는 인스턴스 선언이 필요합니다. 설명을 위해 다시 Color 문제롤 돌아가봅시다.
data Color
    = Red
    | Orange
    | Yellow
    | Green
    | Blue
    | Purple
    | White
    | Black
    | Custom Int Int Int -- R G B components
Color라는 자료형을 Eq의 인스턴스로 만들기 위해서, 다음과 같이 인스턴스 선언을 합니다.
instance Eq Color where
    Red == Red = True
    Orange == Orange = True
    Yellow == Yellow = True
    Green == Green = True
    Blue == Blue = True
    Purple == Purple = True
    White == White = True
    Black == Black = True
    (Custom r g b) == (Custom r' g' b') =
            r == r' && g == g' && b == b'
    _ == _ = False
instance라는 키워드로 인스턴스 선언임을 알리고, Eq 이라는 클래스, Color라는 형을 적어줍니다. 그리고 마지막으로 where라는 키워드가 필요합니다.

6.6.2. Show 클래스


Show 클래스는 어떤 값을 문자열로 바꾸기 위해 사용됩니다. 이 클래스의 멤버는 세 개가 있습니다.
show :: Show a => a -> String
showsPrec :: Show a => Int -> a -> String -> String
showList :: Show a => [a] -> String -> String
Show 클래스의 최소한의 완전한 정의를 위해서는, show 또는 showsPrec 둘 중 하나의 함수가 정의돼야합니다.

Color형의 인스턴스 선언을 해봅시다.
instance Show Color where
    show Red = "Red"
    show Orange = "Orange"
    show Yellow = "Yellow"
    show Green = "Green"
    show Blue = "Blue"
    show Purple = "Purple"
    show White = "White"
    show Black = "Black"
    show (Custom r g b) =
        "Custom " ++ show r ++ " " ++
        show g ++ " " ++ show b

6.6.3. 그 밖의 중요한 클래스


먼저 Ord 클래스를 살펴보면,
compare :: Ord a => a -> a -> Ordering
(<=) :: Ord a => a -> a -> Bool
(>) :: Ord a => a -> a -> Bool
(>=) :: Ord a => a -> a -> Bool
(<) :: Ord a => a -> a -> Bool
min :: Ord a => a -> a -> a
max :: Ord a => a -> a -> a
최소한의 완전한 정의를 위해서는, 위의 7개 함수중에서 하나가 정의되야합니다. 선택은 자유지만, compare 함수를 정의할 것을 권합니다.

그리고 Ordering 자료형은 다음과 같습니다.
data Ordering = LT | EQ | GT
다음과 같이 확인해볼 수 있습니다.
Prelude> compare 5 7
LT
Prelude> compare 6 6
EQ
Prelude> compare 7 5
GT
Ord 클래스의 인스턴스로 선언하기 위해서는 먼저 Eq 클래스의 인스턴스로 선언해야합니다. 다른 말로 하면, Ord 클래스는 Eq 클래스의 서브클래스입니다.

다음으로 Enum 클래스를 살펴보겠습니다. 이는 열거형을 다루기 위해 사용됩니다.
pred :: Enum a => a -> a
succ :: Enum a => a -> a
toEnum :: Enum a => Int -> a
fromEnum :: Enum a => a -> Int
enumFrom :: Enum a => a -> [a]
enumFromThen :: Enum a => a -> a -> [a]
enumFromTo :: Enum a => a -> a -> [a]
enumFromThenTo :: Enum a => a -> a -> a -> [a]
최소한의 완전한 정의를 위해서는, toEnum과 fromEnum이 정의돼야합니다.

그 다음은 Num 클래스입니다. 산술연산자를 멤버함수로 제공합니다.
(-) :: Num a => a -> a -> a
(*) :: Num a => a -> a -> a
(+) :: Num a => a -> a -> a
negate :: Num a => a -> a
signum :: Num a => a -> a
abs :: Num a => a -> a
fromInteger :: Num a => Integer -> a
Read 클래스는 Show 클래스의 반대역할을 합니다.
readsPrec :: Read a => Int -> String -> [(a, String)]
readList :: String -> [([a], String)]
최소한의 완전한 정의를 위해서는, readsPrec이 정의돼야 합니다.

6.6.4. 클래스 문맥


Maybe형은 다음과 같다고 배웠습니다.
data Maybe a = Nothing
             | Just a
그러면 Maybe형을, Eq의 인스턴스로 선언해보도록 하겠습니다.
instance Eq a => Eq (Maybe a) where
    Nothing == Nothing = True
    (Just x) == (Just x') = x == x'
Eq a => Eq (Maybe a) 에서 `Eq a =>' 는 a가 Eq 클래스의 인스턴스임을 알려줍니다. `Eq (Maybe a)'는 Maybe a 형은 Eq 클래스의 인스턴스임을 선언합니다. 그러므로 위의 선언에 따르면, Eq 클래스의 인스턴스인 a형이 있다면 Maybe a 형도 Eq 클래스의 인스턴스가 됩니다.

6.6.5. 클래스의 파생 (deriving class)


Eq, Ord, Read, Show 등과 같은 클래스들의 인스턴스로 선언하는 것은, deriving 키워드를 사용하여 자동적으로 처리할 수 있습니다.
data Color
    = Red
    | ...
    | Custom Int Int Int -- R G B components
    deriving (Eq, Ord, Read, Show)

data Maybe a = Nothing
             | Just a
             deriving (Eq, Ord, Read, Show)

이렇게 파생시킬 수 있는 클래스에는 6개가 있습니다. Eq, Ord, Enum, Bounded, Show, Read.

6.7. 자료형


다음과 같은 자료형을 정의해봅시다.
data Configuration =
    Configuration String          -- user name
                  String          -- local host
                  String          -- remote host
                  Bool            -- is guest?
                  Bool            -- is super user?
                  String          -- current directory
                  String          -- home directory
                  Integer         -- time connected
              deriving (Eq, Show)
user name, local host 등을 반환하는 함수를 만들어 봅시다.
getUserName (Configuration un _ _ _ _ _ _ _) = un
getLocalHost (Configuration _ lh _ _ _ _ _ _) = lh
getRemoteHost (Configuration _ _ rh _ _ _ _ _) = rh
getIsGuest (Configuration _ _ _ ig _ _ _ _) = ig
...
그러나 이는 비효율적입니다. 자료구조의 각 필드에 이름을 붙여봅시다.
data Configuration =
    Configuration { username      :: String,
                    localhost     :: String,
                    remotehost    :: String,
                    isguest       :: Bool,
                    issuperuser   :: Bool,
                    currentdir    :: String,
                    homedir       :: String,
                    timeconnected :: Integer
                  }
이렇게 하면 다음과 같은 함수가 자동으로 생성됩니다.
username :: Configuration -> String
localhost :: Configuration -> String
...
게다가 필드의 값을 갱신할 수도 있습니다. 다음 코드를 살펴봅시다.
changeDir :: Configuration -> String -> Configuration
changeDir cfg newDir =
    -- make sure the directory exists
    if directoryExists newDir
      then -- change our current directory
           cfg{currentdir = newDir}
      else error "directory does not exist"
postWorkingDir :: Configuration -> String
  -- retrieve our current directory
postWorkingDir cfg = currentdir cfg
일반적으로 자료형 a의 필드 x의 값을 x'로 바꾸고 싶다면, a{ x = x' } 와 같이 쓰면 됩니다. 여러 개를 바꾸기 위해서는 a{ x = x', y = y', z = z' } 와 같이 할 수도 있습니다.

다음과 같이 여러 개의 필드 값을 가져오는 함수가 필요할 수도 있습니다.
getHostData (Configuration _ lh rh _ _ _ _ _) = (lh,rh)
그런 경우에는 다음과 같이 패턴매칭을 이용하면 가독성이 좋은 코드를 작성할 수 있습니다.
getHostData (Configuration{ localhost = lh, remotehost = rh }) = (lh,rh)
그리고 마지막으로, Configuration형의 값을 생성하는 방법에는 두 가지 방법이 있습니다.
initCFG =
    Configuration "nobody" "nowhere" "nowhere"
                  False False "/" "/" 0
initCFG' =
    Configuration
       { username = "nobody",
         localhost = "nowhere",
         remotehost = "nowhere",
         isguest = False,
         issuperuser = False,
         currentdir = "/",
         homedir = "/",
         timeconnected = 0 }
물론, 두번째 방법이 훨씬 이해하기가 쉽습니다.

6.8. 리스트


수학에서 집합을 정의할 때, 다음과 같이 쓰기도 합니다.

{f(x) | x ∈ s ∧ p(x)}

이는 s의 요소중에서 p를 만족하는 모든 요소의 집합을 나타냅니다. 하스켈에서는 이와 비슷한 문법을 지원합니다.
[f x | x <- s, p x]
이런 식으로 리스트를 표현하는 것을 리스트 내포구문(list comprehension)이라고 합니다. 리스트 내포구문을 이용하여 문자열에서 대문자만을 골라 소문자로 변환한 리스트를 반환하는 함수를 작성해봅시다.
Prelude> map toLower (filter isUpper "Hello World")
"hw"
Prelude> [toLower x | x <- "Hello World", isUpper x]
"hw"
다음과 같이 순서쌍에 적용할 수도 있습니다.
Prelude> [(x,y) | x <- [1..5], y <- [x..7]]
[(1,1),(1,2),(1,3),(1,4),(1,5),(1,6),(1,7),(2,2),(2,3),
(2,4),(2,5),(2,6),(2,7),(3,3),(3,4),(3,5),(3,6),(3,7),
(4,4),(4,5),(4,6),(4,7),(5,5),(5,6),(5,7)]

6.9. 배열


리스트는 여러 모로 좋은 점이 있지만, 임의접근(random access)의 시간 복잡도가 O(n) 이라는 단점이 있습니다. 그렇기 때문에 임의접근이 빈번한 경우에는 배열을 사용하는 것이 좋습니다. 배열을 사용하기 위해서는 우선 Array 모듈을 불러옵니다. 배열을 생성하는 함수에는 array, listArray 그리고 accumArray 가 있습니다. array 함수는 두 개의 인자를 받습니다. 첫번째 인자는 배열의 크기를 나타냅니다. 두번째 인자는 배열의 값을 나타내는데, 짝을 요소로 갖는 리스트입니다. listArray의 첫번째 인자도 배열의 크기를 나타냅니다. 두번째 인자는 배열의 값을 요소로 갖는 리스트입니다. accumArray의 세번째, 네번째 인자는 각각 array의 첫번째, 두번째 인자와 같습니다. array 함수와의 차이점은, 첫번째 인자로 받는 함수와 두번째 인자로 받는 초기값을 이용하여 새로운 배열을 만들어 반환한다는 점입니다.
Prelude Array> array (1,5) [(i,2*i) | i <- [1..5]]
array (1,5) [(1,2),(2,4),(3,6),(4,8),(5,10)]
Prelude Array> listArray (1,5) [3,7,5,1,10]
array (1,5) [(1,3),(2,7),(3,5),(4,1),(5,10)]
Prelude Array> accumArray (+) 2 (1,5) [(i,i) | i <- [1..5]]
array (1,5) [(1,3),(2,4),(3,5),(4,6),(5,7)]
! 연산자를 이용하여 배열값에 접근할 수 있습니다.
Prelude Array> (listArray (1,5) [3,7,5,1,10]) ! 3
5
// 연산자를 이용하여 여러 값을 한 번에 갱신할 수 있습니다.
Prelude Array> (listArray (1,5) [3,7,5,1,10]) // [(2,99),(3,-99)]
array (1,5) [(1,3),(2,99),(3,-99),(4,1),(5,10)]
그러나 !와 // 연산자를 이용하여 배열을 변화하는 것은 O(1)이 아니고 O(n)입니다. 왜냐하면 함수의 순수함을 유지하기 위해서, 새로운 배열을 만들 때 원래의 배열을 지우지 않고 복사하여 만들기 때문입니다. 배열보다 좀 더 빠르게 갱신할 수 있는 자료구조를 원한다면 Map을 사용해야합니다.
bounds : 배열의 크기를 반환
indices : 배열의 모든 색인(index)을 요소로 하는 리스트를 반환
elems : 배열의 모든 값을 요소로 하는 리스트를 반환
assocs : 색인과 값의 짝을 요소로 갖는 리스트를 반환
Arrays> bounds arr
(1,5)
Arrays> indices arr
[1,2,3,4,5]
Arrays> elems arr
[3,7,5,1,10]
Arrays> assocs arr
[(1,3),(2,7),(3,5),(4,1),(5,10)]

6.10. Map


Map이라는 자료구조는 Data.Map이라는 모듈을 불러옴으로써 사용할 수 있습니다.

이것은 balanced tree를 구현한 것인데, 리스트와 배열과 비교를 해보면 다음과 같습니다.

                   리스트   배열     Map
삽입     (1개)     O(1)     O(n)     O(log n)
수정     (1개)     O(n)     O(n)     O(log n)
삭제     (1개)     O(n)     O(n)     O(log n)
임의접근 (1개)     O(n)     O(1)     O(log n)
전체변환 (n개)     O(n)     O(n)     O(n log n)

리스트   배열    Map
(:)      *2      insert
*1       (//)    adjust
*1       *2      delete
(!!)     (!)     lookup
map      amap    map

*1 : map이나 filter를 이용하여 함수를 만들어야 한다.
*2 : 배열전체를 읽어와서 새로운 배열을 만들면 된다.


기본적인 함수와 그 형은 다음과 같습니다.
empty   :: Map key elt
insert  :: Ord key => key -> elt -> Map key elt -> Map key elt
delete  :: Ord key => key -> Map key elt -> Map key elt
member  :: Ord key => key -> Map key elt -> Bool
lookup  :: Ord key => key -> Map key elt -> Maybe elt
사용법은 다음과 같습니다.
Prelude> :m Data.Map
Prelude Data.Map> let m = fromList [('a',5),('b',10),('c',1),('d',2)]
Prelude Data.Map> let m' = insert 'e' 6 m
Prelude Data.Map> toList m
[('a',5),('b',10),('c',1),('d',2)]
Prelude Data.Map> toList m'
[('a',5),('b',10),('c',1),('d',2),('e',6)]
Prelude Data.Map> Data.Map.lookup 'e' m'
Just 6
Prelude Data.Map> Data.Map.lookup 'e' m
Nothing

7. 부록

7.1. 부록 1.


자주 사용하는 산술형

클래스 설명
Integer Integral Arbitrary-precision integers
Int Integral Fixed-precision integers
(Integral a) => Ratio a RealFrac Rational numbers
Float RealFloat Real floating-point, single precision
Double RealFloat Real floating-point, double precision
(RealFloat a) => Complex a Floating Complex floating-point


클래스 관계

클래스 파생된 클래스
Real Num, Ord
Integral Real, Enum
Fractional Num
Floating Fractional
RealFrac Real, Fractional
RealFloat RealFrac, Floating


7.2. 부록 2. Prelude.hs 의 내장함수들

compare, <, <=, >=, >, max, min  : 두 값을 비교
==              : 두 개의 값이 같은지 비교
/=              : 두 개의 값이 다른지 비교

abs  : 절대값
fromInteger     : Integer형의 값을 받아 변환
toInteger       : Integer형의 값으로 변환
fromRational    : Rational형의 값을 받아 변환
toRational      : Rational형의 값으로 변환
realToFrac      : Real형의 값을 Fractional형으로 변환
show            : String형의 값으로 변환
read            : String형의 값을 받아 변환

pi, (**), exp, log, sqrt, logBase, sin, cos, tan, asin, acos, atan, sinh, cosh, tanh, asinh, acosh, atanh

truncate, round, ceiling, floor

div, mod        : 나눗셈과 나머지
gcd, lcm        : 최대공약수와 최소공배수
even, odd       : 어떤 값이 짝인인지 홀수인지

id              : id x = x
flip            : flip f x y = f y x

$               :  f $  x    =  f x
$!              :  f $! x    =  x `seq` f x

(&&), (||)      : 불린 연산자
True, False     : True  && x =  x
                  False && _ =  False
                  True  || _ =  True
                  False || x =  x
not             : not True  =  False
                  not False =  True
otherwise       : otherwise = True

succ, pred

fst, snd
curry, uncurry

error           : 오류 문구를 출력하고 실행을 멈춤. error = primError
undefined       : undefined = error "Prelude.undefined"

:               : 리스트의 앞에 새로운 요소를 추가함
++              : 리스트 두 개를 연결할 때
null            : 리스트가 비어있는지
head            : 리스트의 처음 요소
last            : 리스트의 마지막 요소
tail            : 리스트의 처음 요소를 제외한 나머지
init            : 리스트의 마지막 요소를 제외한 나머지
length          : 리스트의 길이
!!              : 리스트의 i번째 요소를 반환
reverse         : 리스트를 뒤집는다.
concat          : 리스트의 리스트를 인자로 받아서 모든 리스트를 합친다.
map, filter
foldl, foldr
scanl, scanl1, scanr, scanr1
iterate                 : [ x, f x, f(f x), ... ] 을 반환
repeat                  : 모든 요소의 값이 x인 무한길이 리스트
replicate               : 모든 요소의 값이 x인 유한길이 리스트
cycle                   : 주어지 리스트를 무한반복하는 리스트를 반환
take, drop              : take는 첫번째 n개의 요소를 반환, drop은 첫번째 n개를 제외한 요소를 반환
splitAt                 :  splitAt n xs = (take n xs, drop n xs)
takeWhile, dropWhile    : take, drop과 비슷하다. 함수를 인자로 받아서 요소를 테스트하는데 그 함수를 사용한다.
words, unwords          : words 함수는 문자열을 공백을 기준으로 여러 개의 리스트로 나눈다.
lines, unlines          : lines 함수는 new line 문자를 기준으로 여러 개의 리스트로 나눈다.
and                     =  foldr (&&) True
or                      =  foldr (||) False
any p                   =  or . map p
all p                   =  and . map p
elem, notElem           : 리스트에 어떤 요소의 값이 존재하는지 알려준다. notElem은 elem의 반대
sum, product            : 리스트의 합과 곱
maximum, minimum 
zip                     : zip 함수는 두 개의 리스트를 받아서 짝의 리스트로 만듦
unzip
zipWith

7.3. 부록 3. 입출력 관련 자료형 및 함수

data IOMode           =  ReadMode | WriteMode | AppendMode | ReadWriteMode
data BufferMode       =  NoBuffering | LineBuffering |  BlockBuffering (Maybe Int)
data SeekMode         =  AbsoluteSeek | RelativeSeek | SeekFromEnd

stdin, stdout, stderr :: Handle

openFile              :: FilePath -> IOMode -> IO Handle
hClose                :: Handle -> IO ()

hFileSize             :: Handle -> IO Integer
hIsEOF                :: Handle -> IO Bool
isEOF                 :: IO Bool
isEOF                 =  hIsEOF stdin

hSetBuffering         :: Handle  -> BufferMode -> IO ()
hGetBuffering         :: Handle  -> IO BufferMode
hFlush                :: Handle -> IO () 
hGetPosn              :: Handle -> IO HandlePosn
hSetPosn              :: HandlePosn -> IO () 
hSeek                 :: Handle -> SeekMode -> Integer -> IO () 

hWaitForInput         :: Handle -> Int -> IO Bool
hReady                :: Handle -> IO Bool 
hReady h              =  hWaitForInput h 0
hGetChar              :: Handle -> IO Char
hGetLine              :: Handle -> IO String
hLookAhead            :: Handle -> IO Char
hGetContents          :: Handle -> IO String
hPutChar              :: Handle -> Char -> IO ()
hPutStr               :: Handle -> String -> IO ()
hPutStrLn             :: Handle -> String -> IO ()
hPrint                :: Show a => Handle -> a -> IO ()

hIsOpen               :: Handle -> IO Bool
hIsClosed             :: Handle -> IO Bool
hIsReadable           :: Handle -> IO Bool
hIsWritable           :: Handle -> IO Bool
hIsSeekable           :: Handle -> IO Bool

isAlreadyExistsError  :: IOError -> Bool
isDoesNotExistError   :: IOError -> Bool
isAlreadyInUseError   :: IOError -> Bool
isFullError           :: IOError -> Bool
isEOFError            :: IOError -> Bool
isIllegalOperation    :: IOError -> Bool
isPermissionError     :: IOError -> Bool
isUserError           :: IOError -> Bool

ioeGetErrorString     :: IOError -> String
ioeGetHandle          :: IOError -> Maybe Handle
ioeGetFileName        :: IOError -> Maybe FilePath

try                   :: IO a -> IO (Either IOError a)
bracket               :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c
bracket_              :: IO a -> (a -> IO b) -> IO c -> IO c

ID
Password
Join
Love is in the offing. Be affectionate to one who adores you.


sponsored by andamiro
sponsored by cdnetworks
sponsored by HP

Valid XHTML 1.0! Valid CSS! powered by MoniWiki
last modified 2009-07-05 18:48:18
Processing time 0.0396 sec