人気ブログランキング | 話題のタグを見る

Parsec の実例: HTMangl

Parsec の使い方の実例がないかと検索していたら、つぎのページを見つけた。

Simple Parsec Example: HTMangL

Parsec を使って Haskell の Birs-style の文書のうち、コード部分に HTML のタグをつけ、更に特殊文字もエスケープ表現に置き換えるというプログラムだ。Parsec の基本的な使い方がわかる上に、いろいろなテクニックが洒落ていて面白い。

実は、前回までの数回分の記事は、このプログラムを読みこなすための準備だった。

コード部分だけを抜き出したものを次に示す。

module Main where

import Text.ParserCombinators.Parsec

eol = newline <|> (eof >> return '\n')
tilEOL = manyTill (noneOf "\n") eol

codeLine = do
    string "> "
    code <- tilEOL
    return $ "> " ++ code

litLine = do
    ch <- noneOf "\n"
    if ch == '>' then
          notFollowedBy space
      else
          return ()
    text <- tilEOL
    return $ ch:text

blankLine = char '\n' >> return ""

code = many1 (try codeLine)

lit = many1 (try litLine <|> blankLine)

data LiterateCode = Literate [String]
                  | Code [String]
                    deriving (Show, Eq)

literateCode = many (Code `fmap` code <|> Literate `fmap` lit)

printBlock (Literate ls) = mapM_ putStrLn ls
printBlock (Code cs)     = do
    putStrLn "<code>\n"
    mapM_ (putStrLn . subEntities) cs
    putStrLn "\n</code><br/>"

subEntities = (>>= \c ->
    case c of
        '>' -> ">"
        '<' -> "<"
        '&' -> "&"
        c   -> [c])

main = do
    s <- getContents
    case parse literateCode "stdin" s of
          Left err -> putStr "Error: " >> print err
          Right cs -> mapM_ printBlock cs

このプログラムを読むと、Parsec を特定の目的に使うときの考え方の手順のままにプログラムが書かれているような気がする。

最初は、パターンマッチの際の最も基本的なパターンを作成している。すなわち eol は newline と eof を統一的に含んだ区切り文字のパターンだ。また、tillEOL は文字列の先頭から eol の直前までの文字列のパターンだ。

この2つの基本的なパターンをビルディングブロックにして、次の行ではコメント部分と、コード部分にマッチするパターンを作る。

codeLine パターンは、コードが記述されている行を、先頭の "> " で識別し、その行を取り出してパターンの戻値として返す。

litLine パターンは、行の先頭が "\n" でも "> " でもないものにマッチして、その行を戻値として返す。

blankLine パターンは、行の先頭が "\n" のものにマッチして戻値に空行 "" を返す。

さらに、これらのパターンを使って次のような高次元のパターンを作る。

code パターンは、複数の codeLine パターンから構成されているパターンで、コードのブロックにマッチする。戻値はコードの記述された行のリストで、このリストは many によって作成される。

また、lit パターンは、複数のコメント行からなるパターンにマッチするが、戻値はコメント行のリストだ。

ここで、突然プログラムは、代数型 のユーザデータ型を定義する。その目的はコードブロックとコメントブロックを統一的に LiterateCode として扱うためだ。こうすることでこれらの性質の異なるデータを扱うのに同じ名前の関数で処理できる。

次の literateCode パターンは本質的には literateCode = many1 (code <|> lit) だが、fmap を利用することによって戻値を Code [String] または Literate [String] 型に、code や lit をいじることな戻値変換をしている。

これは、LiterateCode の定義を先にしておいて、code = do ls <- many codeLine; return (Code ls) としておいてもできるが、上のやり方でやったほうが code のパターンを流用できるし記述もスマートになる。

printBlock は LiterateCode 型のデータを印字する関数だが、LiterateCode データ型を定義したおかげで Code [String] 型と、Literate [String] 型に対する動作を変えることができる。代数型の採用理由の一つだ。

subEntities 関数は、特殊文字をエスケープ表現にするフィルターだ、これについては前回の記事で考察した。

main 関数は単純だ。getContents で標準入力からテキストファイルを読とり、parse 関数でこのテキストファイルのデータにトップレベルのパターン literateCode でパターンマッチをかけ、case ~ of でその戻値を取り出し、printBlock 関数で印字させている。

これらの流れをまとめてみると次のようになる。先ずビルディングブロックになる基本的なパターンを作成し、次々に階層的に複雑なパターンにまとめていく。また、パターンを作るときは、戻値がそのパターンの情報を戻すようにする。これらのパターンは最終的にトップレベルのパターン literateCode にまとめ上げられる。

構文解析は、parse 関数によってこのトップレベルパターン lietrateCode をテキストファイルのデータにマッチさせ、parse 関数の戻値を利用することで行われる。

parse 関数の戻値は case ~ of 文で取り出し、Letf と Right のパターンマッチからコンテナに収められている目的のデータを取り出す。

取り出されたデータは最終的に printBlock 関数に渡され、目的の処理が実行できる。

Parsec を使ったプログラムは多かれ少なかれ上に述べたような手順で作成することができる。従って、Parsec を利用するためのポイントは、どういうパターンを記述すればいいかというパターンの作り方についての知識とパターンマッチがおきたとき、どのような戻値を戻せばいいかという戻値についての知識と、それらの戻値を case 文を使って取り出す方法についての知識の3点であることが分かる。

したがって、パターンを設計できて、パターンの戻値を設計できて、トップレベルのパターンの戻値であるParsecモナド値からのコンテナのデータの取り出し方を知っていれば、Parsec を利用して自在にプログラムを作ることができる。摩訶不思議な構文解析プログラムも意外に単純な手順で作成できるようだ。
by tnomura9 | 2011-12-07 20:27 | Haskell | Comments(0)
<< 48時間でSchemeを書こう... Parsec Bird-syl... >>