백엔드/Haskell

[Haskell] 하스켈 기초반 6강 - 패턴 매칭과 if 표현식 및 let 바인딩

Koras02 2023. 2. 17. 23:01

 

이번 시간에는 하스켈에 패턴 매칭이라는 것을 배워보고 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)

참고 문서

 

7 다음 과정

원문: [http://en.wikibooks.org/wiki/Haskell/Next_steps](http://en.wikibooks.org/wiki/Haskell/Next_ste…

wikidocs.net