2023年夏のブログリレー16日目の記事です。
学部3年のseasonです。最近は将来のこと考えたくなくてずっと小説読んだりアニメ見たりしてます。
パーサコンビネータとは
パーサを文字列(トークン列)から構文木への関数と見ると、部品となる単純なパーサを合成して複雑なパーサを作ることができます。このような仕組みをパーサコンビネータと言います。
Haskellでパーサコンビネータを作ってみます。コンパイラはGHC9.0.1です。
簡単な書き方
一文字パーサ
Haskellの関数は純粋なので、解析対象の文字列を引数に取り、結果と残りの文字列を返すことになります。失敗したらエラーメッセージを返します。
type Parser a = String -> Either String (a, String)
disp :: Show a => Either String (a, String) -> IO ()
disp (Right (res, _)) = print res
disp (Left err) = print $ "error: " ++ err
文字を一つだけパースする関数は以下のようになります。
anyChar :: Parser Char
anyChar (x:xs) = Right (x, xs)
anyChar _ = Left "empty string"
main :: IO ()
main = do
disp $ anyChar "abc" -- 'a'
disp $ anyChar "" -- "error: empty string"
数字やアルファベットを一つだけパースする関数も同様。
digit :: Parser Char
digit (x:xs) | isDigit x = Right (x, xs)
digit _ = Left "expect digit"
letter :: Parser Char
letter (x:xs) | isLetter x = Right (x, xs)
letter _ = Left "expect letter"
連結
S = A B C ...
という文脈自由文法があったとき、Sのパーサはどのようになるでしょうか。
右辺の全てのパーサが受理したときに限り左辺のパーサも受理となります。aのパーサのリストを取り、aのリストのパーサを返します。Eitherモナドを<-
で取り出すようにすると、失敗した時点でその値が返ります。
sequence' :: [Parser a] -> Parser [a]
sequence' [] s = Right ([], s)
sequence' (p:ps) s = do
(x, s') <- p s
(xs, s'') <- sequence' ps s'
Right (x:xs, s'')
main :: IO ()
main = do
disp $ sequence' [letter, digit] "a23" -- "a2"
disp $ sequence' [letter, digit] "ab3" -- "error: expect digit"
繰り返し
0回以上の繰り返し
S = A*
aのパーサを取り、aのリストのパーサを返します。エラーが出た時点で空文字列を返すのでエラーは返しません。
many :: Parser a -> Parser [a]
many p s = case p s of
Left _ -> Right ([], s)
Right (x, s') -> case many p s' of
Right (xs, s'') -> Right (x:xs, s'')
main :: IO ()
main = do
disp $ many digit "123" -- "123"
disp $ many digit "abc" -- ""
1回以上の繰り返し
S = A A*
many1 :: Parser a -> Parser [a]
many1 p s = do
(x, s') <- p s
(xs, s'') <- many p s'
Right (x:xs, s'')
main :: IO ()
main = do
disp $ many digit "123" -- "123"
disp $ many digit "abc" -- "error: expect digit"
選択
選択
S = A | B
2つのパーサを取り、どちらかを受理するパーサを返します。
(<|>) :: Parser a -> Parser a -> Parser a
(p1 <|> p2) s = (p1 s) <|> (p2 s) where
Left a <|> Left b = Left $ a ++ " or " ++ b
Left _ <|> b = b
a <|> _ = a
main :: IO ()
main = do
disp $ (digit <|> letter) "a23" -- 'a'
disp $ (digit <|> letter) "123" -- '1'
disp $ (digit <|> letter) "" -- "error: expect digit or expect letter"
全体のコード
import Data.Char
type Parser a = String -> Either String (a, String)
disp :: Show a => Either String (a, String) -> IO ()
disp (Right (res, _)) = print res
disp (Left err) = print $ "error: " ++ err
sequence' :: [Parser a] -> Parser [a]
sequence' [] s = Right ([], s)
sequence' (p:ps) s = do
(x, s') <- p s
(xs, s'') <- sequence' ps s'
Right (x:xs, s'')
many :: Parser a -> Parser [a]
many p s = case p s of
Left _ -> Right ([], s)
Right (x, s') -> case many p s' of
Right (xs, s'') -> Right (x:xs, s'')
many1 :: Parser a -> Parser [a]
many1 p s = do
(x, s') <- p s
(xs, s'') <- many p s'
Right (x:xs, s'')
(<|>) :: Parser a -> Parser a -> Parser a
(p1 <|> p2) s = (p1 s) <|> (p2 s) where
Left a <|> Left b = Left $ a ++ " or " ++ b
Left _ <|> b = b
a <|> _ = a
anyChar :: Parser Char
anyChar (x:xs) = Right (x, xs)
anyChar _ = Left "empty string"
digit :: Parser Char
digit (x:xs) | isDigit x = Right (x, xs)
digit _ = Left "expect digit"
letter :: Parser Char
letter (x:xs) | isLetter x = Right (x, xs)
letter _ = Left "expect letter"
main :: IO ()
main = do
disp $ anyChar "abc"
disp $ anyChar ""
disp $ digit "123"
disp $ digit "abc"
disp $ letter "123"
disp $ letter "abc"
disp $ sequence' [letter, digit] "a23"
disp $ sequence' [letter, digit] "ab3"
disp $ many digit "123"
disp $ many digit "abc"
disp $ many1 digit "123"
disp $ many1 digit "abc"
disp $ (digit <|> letter) "a23"
disp $ (digit <|> letter) "123"
disp $ (digit <|> letter) ""
モナドを使って書き直す
手続き型言語で構文解析をするとき、文字列をどこまで読んだかを更新し、状態を破壊しながらパースすることが多い(?)と思います。Haskellでも文字列の状態を明示的に考えることなく手続き的に書きたいです。
一文字パーサ
State
モナドとEither
モナドをStateT
モナド変換子で合成します。StateT s m a
でs
が状態、m
がモナド、a
が値です。まずパーサを定義します。
type Parser a = StateT String (Either String) a
StateT s m a
はs -> m (a, s)
型の関数を保持しています。
anyChar :: Parser Char
anyChar = StateT anyChar where
anyChar (x:xs) = Right (x, xs)
anyChar _ = Left "empty string"
runStateT :: StateT s m a -> s -> m (a, s)
を使うと内部関数を取り出すことができて、そのまま文字列を適用すれば解析結果が得られます。
main = do
disp $ runStateT anyChar "abc"
連結
連結はsequence :: Monad m => [m a] -> m [a]
を使えば良いです。
繰り返し
many
はほぼ同じように書けます。
many :: Parser a -> Parser [a]
many p = StateT $ \s -> case runStateT p s of
Left _ -> Right ([], s)
Right (x, s') -> case runStateT (many p) s' of
Right (xs, s'') -> Right (x:xs, s'')
many1
は、StateT
アクションを使うことで、パース途中の文字列を明示せずに書くことができます。
many1 :: Parser a -> Parser [a]
many1 p = do
x <- p
xs <- many p
return (x:xs)
選択
選択もほぼ同じです。
(<|>) :: Parser a -> Parser a -> Parser a
StateT a <|> StateT b = StateT $ \s -> (a s) <|> (b s) where
Left a <|> Left b = Left $ a ++ " or " ++ b
Left _ <|> b = b
a <|> _ = a
main :: IO ()
main = do
disp $ runStateT (digit <|> letter) "a23"
disp $ runStateT (digit <|> letter) "123"
disp $ runStateT (digit <|> letter) ""
全体のコード
import Control.Monad.State
import Data.Char
type Parser a = StateT String (Either String) a
disp :: Show a => Either String (a, String) -> IO ()
disp (Right (res, _)) = print res
disp (Left err) = print $ "error: " ++ err
many :: Parser a -> Parser [a]
many p = StateT $ \s -> case runStateT p s of
Left _ -> Right ([], s)
Right (x, s') -> case runStateT (many p) s' of
Right (xs, s'') -> Right (x:xs, s'')
many1 :: Parser a -> Parser [a]
many1 p = do
x <- p
xs <- many p
return (x:xs)
(<|>) :: Parser a -> Parser a -> Parser a
StateT a <|> StateT b = StateT $ \s -> (a s) <|> (b s) where
Left a <|> Left b = Left $ a ++ " or " ++ b
Left _ <|> b = b
a <|> _ = a
anyChar :: Parser Char
anyChar = StateT anyChar where
anyChar (x:xs) = Right (x, xs)
anyChar _ = Left "empty string"
digit :: Parser Char
digit = StateT digit where
digit (x:xs) | isDigit x = Right (x, xs)
digit _ = Left "expect digit"
letter :: Parser Char
letter = StateT letter where
letter (x:xs) | isLetter x = Right (x, xs)
letter _ = Left "expect letter"
main :: IO ()
main = do
disp $ runStateT anyChar "abc"
disp $ runStateT anyChar ""
disp $ runStateT digit "123"
disp $ runStateT digit "abc"
disp $ runStateT letter "123"
disp $ runStateT letter "abc"
disp $ runStateT (sequence [letter, digit]) "a23"
disp $ runStateT (sequence [letter, digit]) "ab3"
disp $ runStateT (many digit) "123"
disp $ runStateT (many digit) "abc"
disp $ runStateT (many1 digit) "123"
disp $ runStateT (many1 digit) "abc"
disp $ runStateT (digit <|> letter) "a23"
disp $ runStateT (digit <|> letter) "123"
disp $ runStateT (digit <|> letter) ""
簡単な数式のパース
数値
手続き的に書いてみます。
num :: Parser Int
num = do
digits <- many1 digit
return $ read digits
main :: IO ()
main = do
disp $ runStateT num "123abc"
disp $ runStateT num "abc"
足し算
add = num ('+' num)*
をパースします。後の0回以上の繰り返しはmany
を利用できます。特定の一文字を読み込むchar :: Char -> Parser Char
を定義して、足し算のパーサを次のように書きます。
char :: Char -> Parser Char
char c = StateT char where
char (x:xs) | x == c = Right (x, xs)
char _ = Left $ "expect " ++ [c]
add :: Parser Int
add = do
x <- num
xs <- many $ do
char '+'
y <- num
return y
return $ sum (x:xs)
main :: IO ()
main = do
disp $ runStateT add "1+2+3+4abc"
余談:Applicativeスタイル
(<$>) :: Functor f => (a -> b) -> f a -> f b | infixl 4 |
(<*>) :: Applicative f => f (a -> b) -> f a -> f b | infixl 4 |
を用いると
func = do
a <- m1
b <- m2
...
return $ f a b ...
を
func = f <$> a <*> b ...
と書くことができる。これをApplicativeスタイルという。
Applicativeスタイルを用いると、例えば繰り返しを簡潔に書ける。
many p = ((:) <$> p <*> many p) <|> StateT (\s -> Right ([], s))
many1 p = (:) <$> p <*> many p
参考
- parsec: Monadic parser combinators: HaskellのパーサコンビネータライブラリParsecのドキュメント。選択演算子
<|>
の挙動が本記事と少し違うので注意(参考)。 - Haskell 構文解析 超入門 - Qiita: StateやEitherモナドを使ったパーサコンビネータの実装。
- Haskell 状態系モナド 超入門 - Qiita: 状態モナドの詳しい解説。
- Applicativeのススメ - あどけない話: Applicativeスタイルの紹介とパーサコンビネータ実装への応用。
おわり
明日はいくらはむさんの記事です。