2. Pattern matching
패턴 매칭은 함수를 만들 때 사용할 수 있는 굉장히 강력한 구문입니다. 패턴 매칭은 어떤 데이터가
가져야 할 패턴을 명시하고, 데이터가 있을 때 그 데이터가 해당 패턴에 맞춰 분해될 수 있는 지
확인하는 과정을 거칩니다. Haskell에서 함수는 데이터의 패턴에 따라 서로 다른 여러 개의 본체를
가질 수 있습니다.
f :: (Integral a, Num b) => a -> b
f 0 = 0
f 1 = 1
f n = f (n-1) + f (n-2)
Prelude> f 10
55
위와 같이 패턴 매칭을 이용해 피보나치 함수를 굉장히 쉽고 직관적으로 구현할 수 있습니다.
3. Pattern matching
패턴 매칭은 위에서부터 차례대로 내려오며 검사합니다. 즉, 패턴에 따른 함수 구현의 순서가 서로
다르면 결과 역시 달라집니다. 아래 두 함수를 봅시다.
f :: (Integral a) => a -> String
f n = "must run this function!"
f 0 = "it’s 0"
f2 :: (Integral a) => a -> String
f2 0 = "it’s 0"
f2 n = "must run this function!"
이 두 함수는 패턴에 따른 함수 선언의 순서만 바꿨을 뿐이지만 결과가 전혀 달라집니다.
4. Pattern matching
*Main> f 0
“must run this function!”
*Main> f 1
“must run this function!”
*Main> f2 0
“it’s 0”
*Main> f2 1
“must run this function!”
위와 같이 함수 f의 경우 f n에 대한 정의가 먼저 이루어지면서, 모든 인자에 대해 ‘n’이라는 패턴이
성립하므로 아래의 f 0 패턴까지 가지 못하고 함수가 실행됩니다. 그래서 패턴 매칭을 할 때는 명확한(
특수한) 패턴을 앞에 두고, 보다 일반적인 패턴을 뒤에 두게끔 작성하시는 것이 좋습니다.
5. Pattern matching
패턴 매칭은 실패할 수도 있습니다. 이 경우 예외가 발생합니다.
foo :: Int -> Int
foo 0 = 0
foo 1 = 1
foo 2 = 2
*Main> f 0
0
*Main> f 4
*** Exception: test.hs:(2,1)-(4,7): Non-exhaustive patterns in
function f
패턴 매칭을 쓸 때는 모든 종류의 패턴이 매칭될 수 있게끔 신경써서 코딩해야 합니다.
6. Pattern matching
튜플에 대한 패턴 매칭도 가능합니다. 두 개의 벡터 값을 더하는 함수를 생각해보죠.
addVector :: (Num a) => (a,a) -> (a,a) -> (a,a)
addVector a b = (fst a + fst b, snd a + snd b)
물론 위 함수도 잘 동작하지만, 아래 코드가 좀 더 깔끔합니다.
addVector :: (Num a) => (a,a) -> (a,a) -> (a,a)
addVector (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)
이런 패턴 매칭을 이용해서 원소가 3개인 페어(트리플)에서 특정 한 원소를 꺼내오는 함수를 작성할
수 있습니다.
7. Pattern matching
first :: (a,b,c) -> a
first (x, _, _) = x
second :: (a,b,c) -> b
second (_, y, _) = y
third :: (a,b,c) -> c
third (_, _, z) = z
패턴 매칭에서 해당 위치에 오는 값에 아무런 관심이 없는 경우(뭐가 와도 상관없는 경우)에는 _
기호를 이용합니다.
8. Pattern matching
패턴 매칭은 list comprehension에서도 이용할 수 있습니다.
pairToNum :: (Num a) => [(a,a)] -> [a]
pairToNum xs = [a+b | (a,b) <- xs]
*Main> pairToNum [(3,4), (1,2), (5,7), (6,3)]
[7,3,12,9]
9. Pattern matching
튜플과 비슷하게 리스트에서도 패턴 매칭을 이용할 수 있습니다. 텅빈 리스트([]), cons 연산자(:)
등을 이용해서 패턴에서 해당 리스트가 가져야 할 형태를 나타낼 수 있습니다. 패턴 매칭을 이용해서
한 번 head 함수를 구현해봅시다.
head' :: [a] -> a
head' [] = error "Can't call head on empty list."
head' (x:_) = x
error 함수는 런타임 에러를 발생시키는 함수입니다. 잘못된 케이스의 함수 호출에 대해 간략한
정보를 남긴 후 프로그램을 죽일 때 사용합니다.
10. Pattern matching
다른 몇 가지 예시도 한 번 살펴봅시다.
length' :: (Num b) => [a] -> b
length' [] = 0
length' (_:xs) = 1 + length' xs
sum' :: (Num a) => [a] -> a
sum' [] = 0
sum' (x:xs) = x + sum' xs
11. Pattern matching
패턴 및 해당 데이터 전체가 모두 필요한 경우 @를 이용할 수 있습니다.
headFirst :: (Show a) => [a] -> String
headFirst [] = "empty string"
headFirst all@(x:_) = show all ++ "'s first element is " ++ show x
*Main> headFirst [1,2,3,4]
"[1,2,3,4]'s first element is 1"
*Main> headFirst "abcd"
""abcd"'s first element is 'a'"
12. 연습 문제
• yesNo
Bool 값 하나를 인자로 받아 그 값이 True면 "Yes", False면 "No"를 리턴하는 함수를 만들어 봅시다.
예를 들어 yesNo (True || False) 는 "Yes"를 리턴해야 합니다.
• trueman
Bool 값 리스트를 인자로 받아 True인 원소의 개수를 리턴하는 함수를 만들어 봅시다. 예를 들어
trueman [True,True,False,False,True]는 3을 리턴해야 합니다.
• listToPairs
list를 인자로 받아 각 원소를 두 개씩 순서대로 짝 지은 페어의 리스트를 리턴하는 함수를 만들어
봅시다. 원소 개수가 홀수 개인 경우 마지막 원소는 버립니다. 예를 들어 listToPairs [1,2,3,4,5] 는
[(1,2), (3,4)]를 리턴해야합니다.
13. Guard
패턴 매칭이 데이터가 특정 패턴을 만족하는 지를 판단하는 방법인 반면 가드는 데이터가 특정 조건을
만족하는 지를 판단하는 방법입니다. 여러 개의 if ~ else if가 나열되어 있는 구조와 비슷한 형태라고
생각하면 됩니다. 우선 예제를 봅시다.
bmiTell :: (Floating a) => a -> String
bmiTell bmi
| bmi <= 18.5 = "You are underweight."
| bmi <= 25.0 = "you are normal."
| bmi <= 30.0 = "you are fat."
| otherwise = "you are very fat."
가드는 인자 다음에 파이프(|)와 논리 표현식을 이용해 표현합니다. 위에서부터 순서대로 인자가
조건을 만족하는지 확인한 다음에 만족한다면 거기에 해당하는 코드를 실행합니다.
14. Guard
가드는 보통 앞의 예제와 같이 여러 줄에 가드의 각 조건과 거기에 해당하는 코드를 나열합니다.
조건을 여러 줄에 나눠 쓸 때는 인덴트(indent)에 조심하셔야 합니다. 각 파이프의 위치가 서로
인덴트가 다르다면 컴파일 에러가 발생합니다.
보통 가드의 맨 마지막은 otherwise입니다. otherwise는 단순히 True로 정의되어 있으며, 다른
가드가 처리하지 못한 조건을 처리하기 위해 존재합니다.
패턴과 가드를 같이 쓸 경우, 가드에서 만족하는 조건이 없을 경우 단순히 다음 패턴으로 넘어갑니다.
다음 패턴이 존재하지 않는다면 마찬가지로 예외가 발생합니다. 그리고 가드를 쓸 때 한 줄에 모든
코드를 다 써도 상관없지만, 가독성이 떨어지기 때문에 별로 추천할만한 방식은 아닌 것 같네요.
max' :: (Ord a) => a -> a -> a
max' a b | a > b = a | otherwise = b
15. where
where 절은 해당 함수 내부에서 반복적으로 쓰이는 여러 표현식 등을 위해 사용됩니다. 앞의 bmiTell
함수를 where를 이용해 수정해보겠습니다.
bmiTell :: (Floating a) => a -> a -> String
bmiTell weight height
| bmi <= 18.5 = "You are underweight."
| bmi <= 25.0 = "You are normal."
| bmi <= 30.0 = "You are fat."
| otherwise = "You are very fat."
where bmi = weight / height ^ 2
키와 몸무게를 인자로 받아 bmi지수를 계산해서 판단하게끔 했고, 이 때 bmi 계산 공식은
반복적으로 쓰이기 때문에 where 절을 이용했습니다.
16. where
where는 코드의 가독성을 높여줄 뿐만 아니라 같은 계산식은 한 번만 계산해도 되게 만들어줌으로써
실행 속도의 향상까지 가져옵니다. where 절 내부의 내용은 해당 함수 내에서만 사용 가능합니다.
또 where 절에서도 패턴 매칭을 이용할 수 있습니다.
bmiTell :: (Floating a) => a -> a -> String
bmiTell weight height
| bmi <= skinny = “You are underweight.”
| bmi <= normal = “You are normal.”
| bmi <= fat = “You are fat.”
| otherwise = “You are very fat.”
where bmi = weight / height ^ 2
(skinny, normal, pat) = (18.5, 25.0, 30.0)
17. where
where 절 내부에서도 함수를 선언할 수 있으며, where 절 내부에 where절을 중첩해서 쓸 수도
있고, 함수를 만드는 데 필요한 문법들(패턴 매칭, 가드 등)도 모두 사용 가능합니다.
calcFibos :: (Integral a) => [a] -> a
calcFibos xs = [fibo x | x <- xs]
where fibo 0 = 0
fibo 1 = 1
fibo n = fibo (n-1) + fibo (n-2)
18. let in
let in 표현식은 where 절과 비슷한 역할을 합니다. let (bindings) in (expression)의 형태를 하고
있으며, where이 뒤에 binding이 오는 반면 let은 binding이 앞에 온다는 차이점이 있습니다.
cylinder :: (Num a) => a -> a -> a
cylinder r h =
let sideArea = 2 * pi * r * h
topArea = pi * r^2
in sideArea + 2 * topArea
let in 표현식은 let 절에서 바인딩한 값을 in 뒤에서 사용할 수 있습니다. 또 where절이 구문적인
구조인 반면 let in 표현식은 if문처럼 코드 상의 어디서든지 등장할 수 있습니다. 즉, let in 표현식은
그 자체로 평가되며 결과 값이 존재해야만 합니다.
19. let in
한 줄에 여러 개의 바인딩을 하고 싶다면 세미콜론(;)을 이용합니다.
Prelude> let a = 5 in a * a * a
125
Prelude> let squre x = x * x in (squre 5, squre 6)
(25, 36)
Prelude> let a = 100; b = 200; c = 300; in a + b + c
600
list comprehension에서도 let을 사용할 수 있습니다. 이 때 let은 바인딩의 역할만 합니다.
Prelude> [f x|x<-[0..10],let f 0=0;f 1=1;f n=f (n-1)+f (n-2)]
[0,1,1,2,3,5,8,13,21,34,55]
20. case expression
case 표현식은 명령형 언어의 switch - case 구문과 비슷한 개념이나, 훨씬 강력한 기능을 갖고
있습니다. case 표현식 역시 표현식이기 때문에 평가되는 값이고, 코드의 중간에 사용할 수 있습니다.
구문은 아래와 같은 구조를 갖고 있습니다.
case expression of pattern -> result
pattern -> result
pattern -> result
...
함수 인자의 패턴 매칭과 굉장히 유사합니다. 특정 표현식의 패턴에 따른 결과 값을 case 표현식을
통해 나타낼 수 있습니다. 다만 함수에서의 패턴 매칭과는 다르게 case 표현식은 표현식이기 때문에
어디에서든지 사용가능합니다.
21. case expression
head’ :: [a] -> a
head’ xs = case xs of [] -> error "Can’t call head on empty list."
(x:_) -> x
descList :: [a] -> String
descList xs = "this List is " ++ case xs of [] -> "Empty List"
[x]-> "Singleton List"
xs -> "Long List"
22. 연습 문제
• maximum
어떤 리스트가 주어져 있을 때, 리스트의 원소중 가장 큰 원소를 리턴하는 함수를 구현해 봅시다. 빈
리스트의 경우 error 함수를 사용해서 런타임 에러를 일으키게끔 만들어 보세요.
• take
숫자와 리스트가 주어졌을 때, 리스트에서 처음부터 해당 숫자만큼의 원소를 잘라서 리턴하는 함수를
만들어 봅시다. 단, 숫자가 0이하인 경우 빈 리스트([])를 리턴해야 합니다.
• zip
두 개의 리스트가 주어졌을 때 두 리스트의 원소 각각을 튜플로 짝지은 리스트를 리턴하는 함수를
만들어 봅시다.
위 함수는 모두 표준에 존재하는 함수입니다. 이름을 조금 다르게 작성하셔야 합니다.
23. 연습 문제
• quickSort (* hard)
어떤 리스트가 주어져 있을 때, 그 리스트를 quickSort를 이용해 정렬하는 함수를 구현해봅시다.
처음에는 풀기 상당히 어려운 문제니 천천히 시간을 들여서 고민해봅시다.