gimmesilver's blog

Agbird.egloos.com

포토로그



하스켈로 웹크롤러 구현하기...4 하스켈 스프링노트

앞서 예고드린대로 이번에는 서버에서 전송받은 웹문서에서 <a href> 태그에 있는 링크 url 을 추출하는 소스를 설명하기 위해 우선 하스켈에서 파싱 구문을 처리하는 방법에 대해 소개하겠습니다.
저는 html 파싱을 위해 Parsec 이라고 하는 파싱 라이브러리를 사용했습니다. 파섹은 모나드 기반의 라이브러리인데 대단히 직관적이고 사용하기 쉬우면서도 성능도 괜찮습니다.(사실 성능을 직접 측정해본적은 없지만 그렇다고 하데요...^^;)

파섹은 앞서 언급했듯이 모나드 방식으로 동작합니다. 때문에 C++나 자바와 같은 명령형 언어처럼 수행 순서가 정해져 있습니다. 즉, 모나드 함수들은 수행 순서가 달라지면 결과가 달라집니다. (원래 하스켈과 같은 함수형 언어에서 일반적인 함수들은 수행 순서에 상관없이 동일한 결과를 갖습니다.)

자질구레한 설명은 차차 하기로 하고 먼저 간단한 파섹 라이브러리 동작 구조를 알아보겠습니다. 파섹에는 우선 파싱을 수행하는 parse 라고 하는 함수가 있습니다. 이 함수의 자료형은 아래와 같습니다.

parse :: GenParser tok () a -> SourceName -> [tok] -> Either ParseError a

왠지 모르게 복잡해 보이는데 간단히 설명하면 parse 함수는 세 개의 매개 변수를 받아 그 결과를 Either 타입의 값으로 반환하는 함수입니다. parse가 받는 세 가지 매개 변수 및 결과값은 아래와 같습니다.

1) GenParser tok() a : 첫 번째 매개 변수는 실제 파싱을 수행할 때 사용될 파싱 규칙과 동작을 기술한 함수입니다. parse 함수는 이 매개 변수로 전달되는 함수를 이용해서 파싱을 수행합니다.
2) SourceName : 이건 참조 문서에 보면 에러 메시지를 처리하는 파일을 지정하는 거라고 나와 있는데 무시해도 됩니다. 문서에서도 보통 빈 문자열인 ""을 넣으면 된다라고 나와 있군요...그냥 무시
3) [tok] : 파싱을 수행할 문자열입니다.
4) Either ParseError a : 1)에서 전달한 함수를 이용해서 파싱을 수행해서 규칙대로 파싱에 성공하면 해당 파싱 함수가 빈환하는 값을, 실패하면 실패한 위치와 그 이유가 기술된 에러 메시지를 반환합니다. 이 때 파싱 함수가 반환하는 값과 에러 메시지의 타입이 다를 수 있으므로 반환값을 Either라는 타입을 이용해서 한번 감싸서 반환합니다. Either 타입은 각각 Left와 Right 라는 생성자를 갖으며 Left 생성자는 에러 메시지를, Right 생성자는 GenParser가 반환한 값을 갖습니다. 그러므로 parse 함수의 일반적인 사용법은 보통 아래와 같습니다.

case (parse parsingFunc "" "test.txt") of
    Left err -> print err    -- 파싱 실패! 에러 메시지 출력
    Right ret -> print ret    -- 파싱 성공! 파싱 함수가 반환하는 값을 출력

아마 하스켈에 익숙하지 않으면 위의 내용이 잘 이해가 가지 않을 수도 있습니다. 이전 글에서 언급한 Maybe나 여기에 나온 Either와 같은 것들은 저 역시 처음에 접하고는 '엥 이건 뭐지?' 라는 반응을 보였으니까요... 너무 집요하게 이해하려 하지 말고 처음에는 관용구처럼 이해하도록 합시다... 우선은 Maybe가 에러 처리를 위한 하나의 idiom 같은 거라면 Either는 다른 성격을 가진 두 종류의 값을 반환하는 함수를 만들고자 할 때 사용하는 idiom이라고 생각하시기 바랍니다. 즉, parse 함수는 파싱 결과와 에러 메시지라는 서로 다른 두 종류의 값 중 하나를 반환하는 함수이기 때문에 이 두 자료형을 동시에 처리하기 위해서 Either를 사용한 것입니다.

이제 실제 파싱을 수행하는 함수인 GenParser 에 해당하는 부분을 살펴보겠습니다. 하스켈에는 파싱을 위한 다양한 함수들이 존재하며 이들을 적절히 조합하면 대부분의 파싱 작업을 수행할 수 있습니다. 예를 들어 "1+2" 같은 문자열을 받으면 이를 파싱해서 3이라는 결과를 제공해주는 간단한 덧셈 함수를 만든다고 합시다. 아마도 아래와 같이 만들 수 있을 것입니다.

addParser expr = case (parse addParser' "" expr) of
    Left err -> print err
    Right ret -> print ret
 
addParser' = do
    left <- many digit >>= return . read
    op <- char '+' >> return (+)
    right <- many digit >>= return . read
    return $ left `op` right

위 함수는 아래와 같이 사용할 수 있습니다.

addParser "12+34"
46

실제 파싱을 수행하는 핵심 부분은 addParser' 이며 위에 빨간색으로 강조한 부분이 파섹에서 제공하는 파싱함수입니다. 각 함수는 이름이 무척 직관적이기 때문에 별다른 설명이 필요없을 정도입니다만 간략하게 언급을 하자면,

digit : 하나의 문자를 읽어서 이 문자가 숫자값이면 해당 숫자값을 반환합니다.
char : 역시 하나의 문자를 읽어서 이 문자가 char 함수 뒤에 나온 문자와 일치하면 해당 문자를 그대로 반환합니다.
many : many 뒤에 나온 함수가 파싱에 실패할 때까지 계속 문자열을 읽습니다.

따라서 위의 addParser' 함수는 다음과 같은 동작을 수행합니다.

1) left <- many digit >>= return . read : 주어진 문자열에서 숫자값이 아닐때까지 계속 읽어서 이 숫자값들을 반환합니다. 예에서처럼 "12+34"를 입력하면 + 앞부분인 12를 읽어서 12를 left에 저장합니다.
2) op <- char '+' >> return (+) : 다음 문자열이 '+' 라면 덧셈함수인 (+) 연산자를 op에 저장합니다.
3) right <- many digit >>= return . read : 다시 주어진 문자열에서 숫자값이 아닌 값이 나오거나 문자열이 끝날 때까지 숫자를 읽어서 right에 저장합니다.
4) return $ left `op` right : read함수를 사용해서 left와 right에 대해 덧셈 연산자를 저장하고 있는 op 함수를 적용한 값을 반환한다.
 
만약 위 함수에 잘못된 수식을 입력하면 에러 메시지가 발생합니다. 예를 들어 덧셈대신 뺄셈을 입력하면 아래와 같은 결과가 나옵니다.

addParser "12-34"
(line 1, column 3):
unexpectted "-"
expecting digit or "+"


말그대로 숫자나 '+' 값이 나와야 하는데 '-'값이 나와서 파싱에 실패했다는 에러 메시지가 출력됩니다.
그렇다면 만약 뺄셈도 처리하고 싶다면 어떻게 해야 할까요? 위의 2)번 부분에서 '+' 이면 덧셈 연산자를 리턴하고 '-'이면 뺄셈 연산자를 리턴하면 됩니다. 이렇게 'a 아니면 b를 처리해라' 라고 기술하고 싶을 때 사용하는 연산자가 <|> 입니다. 사용법은 다음과 같습니다.

op <- (char '+' >> return (+)) <|> (char '-' >> return (-))

이렇게 하면 먼저 '+' 인지를 먼저 검사해보고 실패하면 '-' 인지를 검사하게 됩니다. 위의 2) 부분을 이렇게 고치면 이제 뺄셈도 처리할 수 있습니다.

addParser "12-34"
-22

다음에는 이렇게 기초적인 파섹 함수들을 사용하여 <a href> 태그의 링크를 추출하는 소스에 대해 설명하도록 하겠습니다.

p.s. 파섹에 대한 또다른 글인 '하스켈로 파서 만들기'도 참고하세요

덧글

댓글 입력 영역