parsec 포스트처럼 아이디어를 찾아 본 게 아니라, 그저 사용법만 알아 봤습니다.
newtype Parser a = Parser { runParser :: forall r. S -> Failure r -> Success a r -> Result r }
문자열과 실패를 처리할 함수, 성공을 처리할 함수를 받아 결과를 돌려주는 타입입니다.
parsec 라이브러리에서는 다음과 같습니다.
newtype Parser a = Parser { parse :: String -> [(a,String)] }
parsec 에서는 결과값을 리스트로 표시해서 실패를 []로 표현했는데, 여기서는 명시적으로 실패, 부분 매칭, 성공 3가지를 결과 타입으로 정의했습니다. Partial
이 눈에 띕니다.
data Result r = Fail S [String] String
| Partial (B.ByteString -> Result r)
| Done S r
Q.
Fail
,Done
은 그럭 저럭 넘어 가겠는데,Partial
의 함수(B.ByteString -> Result r)
가 목에 걸립니다. 결과로 파서 함수를 돌려주는 이유가 뭘까요?
A. 프로그래머가 지정하는 함수가 아니라, parse 함수가 결과로 만들어내는, 내부에 일부 파싱 작업이 진행된 결과를 갖고 있는 함수입니다. 이 함수를 추가 문자열에 적용하는 것만으로 기존 파싱 작업했던 것과 합쳐져서 최종 결과를 만들어 냅니다.
parse
에 파서와 문자열을 넣어주면 기본 끝난 문자열로 인식하지 않습니다. 문자열이 더 들어오길 기다리다가 ““이 들어오면 파싱을 끝마칩니다. feed
로 추가 문자열을 넣어줍니다. 추가 문자열 없이 끝내려면 parseOnly
를 씁니다. 이런 특성 때문에, Lazy하게 스트림을 처리할 때 parsec보다 유리합니다.
> r = parse (double) "1"
> print r
Partial _
> feed r ""
Done "" 1.0
물론 매칭 실패하면 추가 문자열을 기다리지 않습니다.
= ...소켓에서 읽은 ByteString 리턴
recvByte = parse double
dParser
dParser recvByte
만일 recvByte
가 "1"
만 리턴하고, 다음은 아직 읽기 전이라면,
"1" dParser
에서 Done "" 1.0
으로 끝나면 안됩니다. 다음에 들어오는게 구분자(공백 같은 것들..)일지 이어지는 숫자일지 알 수 없으니 일단 Partial
상태로 두어야 합니다.
feed
함수는 Partial p
를 받아 p
를 추가 문자열에 적용하는 함수입니다.
feed :: Monoid i => IResult i r -> i -> IResult i r
Fail ...
feed Partial p) moreByte = p moreByte
feed (Done ...
feed
"1"의 결과값 Partial p) "2" feed (dParser
이렇게 추가 적용했는데, 아직도 패킷이 끝났다는 표시가 없으면 어떻게 할까요? 또 feed
를 불러야 합니다. 현재 "12"
까지 매칭한 상태입니다. 파서를 한 두번 적용해서 끝나는 게 아니니 루프를 돌려야 합니다.
recvLoop1 :: MonadIO m => (ByteString -> Result a) -> TCP.Socket -> m (Maybe (a, ByteString))
= do
recvLoop1 p sock <- Network.Simple.TCP.recv sock 1 -- 소켓에서 1바이트 읽기.
resByte -- Network.Simple.TCP.recv는 결과값이 (Maybe ByteString) 입니다.
case p <$> resByte of
Nothing -> return Nothing -- 읽어 온 값이 없을 때
Just (Fail {}) -> return Nothing -- 매칭 실패. (resByte를 리턴해서 다른 파서를 시도할 수도
-- 있지만, 여기선 단순하게 Nothing을 리턴합니다.)
Just (Partial p') -> recvLoop1 p sock -- p와 일부 매칭이 되고,
-- 다음을 더 읽어봐야 아는 상태. feed와 비슷한 구현.
Just (Done left a) -> return (Just (a, left)) -- 완전히 p와 매칭 성공하고 남은 문자열이 있는 상황
끊임없는 스트림을 파싱할 때는 Done
이나 Fail
로 끝나게 하면 안될 때도 있습니다. 한 번에 한가지 메시지만 들어 오는 게 아니라, 여러 메시지가 함께 들어 올 경우도 있습니다. 아래 recvLoop2
는 들어온 패킷은 모두 파싱 시도해서 결과를 리스트에 담습니다. recvLoop1
과 recvLoop2
의 차이는, recvLoop1
은 한 번만 매칭 시도해서 성공하면 남은 패킷이 있더라도 리턴하고, recvLoop2
는 들어온 모든 패킷에 반복해서 매칭을 시도합니다.
cronokirby / haze - Haze/Peer에서 발췌
parse :: (ByteString -> Result String) -> ByteString -> Maybe ([Message], CallBack)
= do
parse callback bytes <- applyPartial callback bytes []
(msgs, callback') return (reverse msgs, callback')
where
=
applyPartial callback bs acc case callback bs of
Fail{} -> Nothing
Partial f -> Just (acc, f) -- Partial일 때만 값을 가지고 루프를 빠져 나간다.
Done left m -> applyPartial firstCallBack left (m : acc)
-- Done일때는 acc에 결과를 누적하고 다시 첫 파서부터 매칭 시도
recvLoop2 :: (ByteString -> Result String) -> IO ()
= do
recvLoop2 callback <- ... -- 소켓에서 데이터 읽기
bytes case parse callback bytes of
Nothing -> recvLoop2 firstCallBack -- 아무것도 매칭 못하면 다시 처음부터
Just (msgs, callback') -> do -- Partial 상태일 때만 여기에 도착한다.
메시지 처리 recvLoop2 callback'