이번 시간에는 하스켈에 패턴 매칭이라는 것을 배워보고 if 표현식 및 let 바인딩을 알아보겟습니다.
if / then / else
하스켈 문법은 if...then...(else...) 형태의 흔한 조건 표현식을 지원합니다
가령 인자가 a보다 작으면 (-1) 을반환하는 함수를 생각 해보면 인자가 0이면 0을,
인자가 0보다 크면 1을 반환합니다.
그런 일을 하는 signum이라는 함수가 이미 정의되 있지만 설명을 위해서 직접 정의해 봅시다.
mySignum x =
if x < 0
then - 1
else if x > 0
then 1
else 0
이것을 다음과 같이 실행해볼 수 있습니다.
*Main> mySignum 5
1
*Main> mySignum 0
0
*Main> mySignum (5-10)
-1
*Main> mySignum (-1)
-1
마지막의 "-1"을 감싼 괄호는 필수사항으로 이것을 빼먹을 시 시스템은 mySignum에서 1을 뺴려 한다고
생각할 것이고 이것은 타입 불량 현상을 불러 일으킵니다.
if/then/else 구조에서는 맨 처음에 조건식(x < 0)으로 평가됩니다. 그 결과 True이면 구조 전체가
then 표현식으로 평가됩니다. 아니면(조건이 False일 경우) else 표현식으로 평가됩니다.
모든 것은 직관적입니다, 하지만 명령어 언어로 프로그래밍을 해보았다면
하스켈에서는 항상 then과 else 모두를 요구한다는 점이 놀라울 수 있습 니다.
이는 if 구조가 두 경우 모두 어떠한 값을 결과로 내야하기 때문이며,
좀더 명확히 두 경우 모두 같은 타입의 값을 결과로 내야 합니다.
위와 같은 if/ then / else 함수 정의는 가드 문법으로 쉽게 재작성이 가능합니다.
mySignum x
| x < 0 = -1
| x > 0 = 1
| otherwise = 0
비슷하게, 진위값에 절대값 함수를 if/then/else로 만들 수 있습니다.
abs x =
if x < 0
then - x
else x
왜 가드를 나두고 if를 사용하는지는 직접 프로그래밍 할 때도 보게 되겠지만 조건부를 다루는
각각의 방식은 상황에 따라서 가독성이 더 좋거나 편리합니다. 대부분의 경우 두 방식 모두 잘 작동합니다.
패턴 매칭의 도입
경주자들이 각 경주에서 순위에 따라서 점수를 받는 경쟁에서 통계치를 추적하는 프로그램을
작성하는 것을 생각해봅시다. 점수 규칙은 아래와 같습니다.
- 승자는 10점
- 2등 6점
- 3등 4점
- 4등 3점
- 5등 2점
- 6등 2점
- 나머지 0점
순위(1등은 정수 1로 나타내는 등)를 받아 흭득한 점수를 반환하는 함수는 쉽게 작성할 수 있습니다.
한가지 해결책은 if/then/else를 사용하는 것 입니다.
pts :: Int -> Int
pts x =
if x == 1
then 10
else if x == 2
then 6
else if x == 3
then 4
else if x == 4
then 3
else if x == 5
then 2
else if x == 6
then 1
else 0
위 코드를 보면 굉장히 번거로운 언어로 보이지만 더 나은 방법도 존재합니다.
pts :: Int -> Int
pts 1 = 10
pts 2 = 6
pts 3 = 4
pts 4 = 3
pts 5 = 2
pts 6 = 1
pts _ = 0
위보다 훨씬 좋은 방식입니다. 그런데 pts를 이런 식으로 정의하면 분명 코드를 읽는 사람은 이러한 함수가 무엇을 의미하는지 명확히 알 수 있으나 그 문법은 우리가 지금까지 본 하스켈 언어 코드 라고 보기에는 아주 어렵습니다.
왜 pts에 등식이 7개나 있을까요? 좌변의 숫자들은 무엇이며 x는 어디로 간걸까요?
하스켈의 이러한 특성은 패턴 매칭이라고 하며, pts를 호출하면 그 인자는 각 등식의 좌변에 있는 숫자
즉 패턴이라는 것과 비교(매칭)됩니다. 이러한 비교는 등식들을 작성한 순서로 행해집니다.
그래서 인자는 첫번째 등식의 1과 비교됩니다. 인자가 1이라면 맞는 걸 찾는 것이고
첫 번째 등식이 사용됩니다. 따라서 pts 1은 10으로 평가됩니다.
아니면 같은 절차를 따라 다른 등식들도 시도되는데 마지막은 사뭇 다릅니다.
( _ )는 특수 패턴으로, 종종 "와일드 카드"라 불리는데, "아무거나"라고 읽을 수 있습니다.
( _ )는 모든 것과 일치하고 그래서 인자가 앞선 패턴들 중 어느 거솓 일치하지 않으면
pts는 0을 반환합니다. 인자를 나타낼 x 같은 변수가 없는 이유로는, 정의를 작성할 때
그런 게 필요하지 않기 떄문입니다. 여기서 모든 가능한 반환값이 바로 상수입니다.
변수는 정으의 우변과의 관계를 표현하기 위해 사용하는 것으로 우리의 pts 함수에서는
x가 필요없습니다. 그렇지만 변수를 활용하면 pts를 보다 간결히 만들 수 있습니다.
경주자들에게 주어지는 점수는 3등에서 6등까지 균일하게 1점씩 감소합니다.
여기에 착안해서 방정식 7개 중 3개를 제거할 수 있습니다.
pts :: Int -> Int
pts 1 = 10
pts 2 = 6
pts x
| x <= 6 = 7 - x
| otherwise = 0
즉 정의의 두가지 스타일을 섞을 수 있습니다. 사실은 등식의 좌변에 pts x라고 쓸때도
패턴 매칭을 이용하고 있는 것 입니다. 패턴으로서 x는 _ 처럼 모든 것과 일치합니다.
유일한 차이점은 우변에서도 그 이름을 쓸 수 있다는 것 입니다.
점수 이외에 패턴 매칭은 여러 타입의 값과 작동합니다. 한가지 유용한 예로 불리언입니다.
가령 진위값에서 만났던 논리합 연산자 ( | | )는 다음과 같이 정의할 수 있습니다.
(||) :: Bool -> Bool -> Bool
False || False = False
_ || _ = True
or
(||) :: Bool -> Bool -> Bool
True || _ = True
False || y = y
한 번에 두개 이상의 인자들을 비교하면, 모든 인자가 일치할 떄만 등식이 사용됩니다.
패턴 매칭을 쓸때 잘못될 수 있는 것들을 몇 개 정리해봅시다.
- 모든 것에 일치하는 패턴을 더 구체적인 패턴의 앞에 놓으면
후자는 무시됨. 그런 경우 GHCi는 패턴 매칭이 중복된다고
에러 메시지를 발생시킴 - 일치하는 패턴이 하나도 없으면 오류가 발생
통상 패턴들이 모든 경우를 다루도록 하는 게 좋은 생각이며
otherwise 가드가 필수는 아니지만 강력히 권장하고 있음 - 마지막으로 (&&) 를 여러 방식으로 재정의할 수 있고
이것은 현재 작동하지 않는 버전임
(&&) :: Bool -> Bool -> Bool
x && x = x -- oops!
_ && _ = False
튜플 패턴과 리스트 패턴
위 예제들은 패턴 매칭이 더 우아한 코드 작성에 도움이 된다는 것을 보여주나
패턴 매칭의 중요성은 설명하지 않고 있습니다. 그럼 fst의 정의를 작성하는 문제를
생각해봅시다. 이 함수는 짝의 첫번째 원소를 추출합니다.
지금 시점에는 이는 불가능 한 일 처럼 보이나 짝의 첫 번째 값에 접근하는 유일한 방법은
fst 자체를 이용하는 것이기 떄문입니다.. 그런데 다음 함수는 fst와 같은 일을 합니다.
fst' :: (a, b) -> a
fst' (x, _) = x
등식의 좌변에서 정규 변수를 쓰는 대신 인자를 2-짝의 패턴으로 기입했습니다
즉 변수와 _ 패턴으로 채워진 ( , ) 이며 이 변수는 튜플의 첫 번째 성분과
자동으로 연관되고 등식의 우변에서 이용할 수 있게 됩니다. snd의 정의도
물론 비슷합니다. 위에서 설명한 기교는 리스트에서도 할 수 있습니다.
다음은 head 와 tail의 실제 정의 입니다.
head :: [a] -> a
head (x:_) = x
head [] = error "Prelude.head: empty list"
tail :: [a] -> [a]
tail (_:xs) = xs
tail [] = error "Prelude.tail: empty list"
이전 예제와 근본적으로 다른 점은 ( , ) 를 cons 연산자 ( : )의 패턴으로 대체한 것 뿐입니다.
이 함수들도 빈 리스트 [ ] 의 패턴을 활용하는 등식을 포함합니다.
하지만 빈 리스트는 머리나 꼬리가 없기 떄문에 error로 오류 메시지를 출력하는 것 말고는
할 수 있는 것이 없습니다. 요약하면, 패턴 매칭의 진정한 힘은 이것을 이용해 복잡한 값의
일부에 접근할 수 있다는 것입니다.
let 바인딩
let 바인딩을 짧게 소개하자면 let은 지역 선언을 위한 where 절의 대체문입니다.
예를 들어 아래 식을통해 다항식의 해를 구하는 문제를 봅시다.
x의 두 값을 계산하는 다음 함수를 작성할 수 있습니다.
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) 항을 두 번 쓰는 것은 성가시기 떄문에 대신
where 또는 밑에서 설명한 let 선언을 이용해 지역 바인딩이 가능합니다.
roots a b c =
let sdisc = sqrt (b*b - 4*a*c)
in ((-b + sdisc) / (2*a),
(-b - sdisc) / (2*a))
let 키워드를 선언 앞에 놓고, in을 사용해 함수의 "주" 몸체로 돌아온다는
신호를 보냅니다. 하나의 let...in 블록 내에 여러 선언을 놓을 수 있습니다.
이것들이 같은 양만큼 들여쓰기가 되었는지 확실히 해야합니다.
그렇지 않으면 문법 오류가 발생합니다.
roots a b c =
let sdisc = sqrt (b*b - 4*a*c)
twice_a = 2*a
in ((-b + sdisc) / twice_a,
(-b - sdisc) / twice_a)
참고 문서
'백엔드 > Haskell' 카테고리의 다른 글
[Haskell] 하스켈 기초반 5강 - 타입의 기초2 (0) | 2023.02.05 |
---|---|
[Haskell] 하스켈 기초반 4강 - 리스트와 튜플 (0) | 2023.01.29 |
[Haskell] 하스켈 기초반 3강 - 타입의 기초 (0) | 2023.01.12 |