6. 흐름 제어

일, 2006-07-16 18:41 — 귤
프로그램은 경우 따라 다른 방식으로 작동할 필요가 있다. 이것을 흐름 제어(flow control)라고 한다. C에서는 if, select-case같은 조건문이나 for, while 같은 반복문을 사용한다. Haskell에서는 반복 대신 순환을 사용하기 때문에 흐름 제어가 좀 더 간단하다. 리스트의 원소를 세는 함수를 하나 만들어보자.
length1 [] = 0 length1 (x:xs) = 1 + length1 xs
이미 이와 같은 예를 여러 번 보았기 때문에 자세한 설명은 하지 않을 것이다. 이렇게 똑같은 함수를 각각의 경우에 따라 만드는 방법을 Haskell에서는 패턴 맞춤(pattern matching)이라고 한다. length1 함수는 []과 (x:xs)라는 두 가지 패턴을 가지고 있다. 함수를 사용할 때 Haskell은 맞는 패턴을 가진 함수를 실행시킨다.
패턴 중 일부분은 함수의 정의에 전혀 쓰이지 않을 수 있다. 이때는 그 매개변수의 자리를 빈칸(wildcard)으로 남겨놓으면 코드를 읽기가 더 쉬워진다. 다음은 리스트의 첫 원소를 구하는 함수와 리스트의 첫 원소를 제외한 나머지를 구하는 함수이다.
head (x:_) = x tail (_:xs) = xs
반대로 패턴 전체가 함수의 정의에 쓰이는 경우도 있다. 이때는 패턴 별명(as-pattern)을 사용한다. 아래는 리스트의 첫 원소를 하나 더 붙여주는 함수다.
doubleHead1 (x:xs) = x:x:xs doubleHead2 s@(x:xs) = x:s doubleHead1 [1,2,3] ⇒ [1,1,2,3]
doubleHead1의 패턴은 리스트를 하나 받아 첫 원소 x와 나머지 리스트 xs로 쪼갠다. 그런데 함수의 정의에서 원래의 리스트도 필요하기 때문에 x:x:xs처럼 번잡한 표현을 쓰게된다. 이때 패턴 앞에 ‘별명@’를 붙여주면 패턴에 해당하는 매개변수를 별명으로 부를 수 있게 되어 편리하다.
head 함수는 첫 원소 x 뒤에 따르는 내용에 해당하는 부분이 빈칸(_)으로 남겨져있다. 이 내용은 함수의 정의에서 쓰이지 않기 때문에 이름을 붙여주지 않는 것이다. tail의 경우도 마찬가지다.
패턴이 많을 경우 일일이 함수를 정의하는 것이 번거로울 수 있다. 이때는 가드(guard)라는 것을 쓴다. 가드는 패턴에 조건을 달아주는 것이다. 다음은 length1을 가드로 다시 정의한 것이다.
length2 xs | xs == [] = 0 | otherwise = 1 + length2 (tail xs)
|는 가드를 나타내는 기호다. | xs == []는 xs가 []와 같아야 한다는 뜻이다. 가드에는 == 외에도 크다(>), 작다(<) 등 부울값(True, False)을 반환하는 함수라면 무엇이든 사용할 수 있다. Haskell은 가드의 조건을 순서대로 맞춰본다. 그러다 참인 경우가 있으면 실행하고, 더 조건을 맞춰보지 않는다. otherwise는 문법 요소가 아니라 항상 참(True)을 반환하는 함수이다. 마지막 가드의 조건에 otherwise를 쓰면, 다른 모든 조건에 맞지 않을 때 무조건 실행하라는 뜻이 된다.
패턴 매칭을 함수 정의 내에서 흐름 제어를 할 때 사용하려면 경우 표현식(case expression)을 쓴다. 다음은 length2를 경우표현식으로 정의한 것이다.(언제나 그렇듯이 →는 실제로 입력할 때는 ->이다.)
length3 xs = case xs of [] → 0 (y:ys) → 1 + length3 ys
또 명령형 언어에서 익숙한 if-then-else를 사용할 수도 있다.
length4 xs = if xs == [] then 0 else 1 + length4 (tail xs)
Haskell에서는 이렇게 다양한 흐름 제어 방법을 제공하지만 사실 내부적으로는 모두 경우표현식으로 처리된다. 예컨대 if는 경우 표현식으로 다음과 같이 된다.
if a > b then a else b case a > b of True → a False → b