みどりねこ日記

よくわからないけど、頑張りますよ。

Clojure の パーサコンビネータライブラリ Kern

Clojure でパースするとき、みんなどうしてるんだろう。 Parsec のようなものがあればいいのに〜、と思って探してみたところ、この Kern が一番よさげだった。

特徴

  • 状態モナドベースのコンビネータ
  • C, Java, Haskell, Shell の構文をサポート
  • パースと式の評価をサポート
  • 正確で詳しいエラーメッセージ
  • エラーメッセージは簡単に多言語化可能
  • パーサの内部状態にクライアントコードからアクセス可能
  • サンプルあり

セットアップ

leiningen がインストールされているなら、 project.clj の :dependencies に

[org.blancas/kern "0.6.1"]

で、コード中に

(use 'blancas.kern.core)

Haskell の Parsec っぽい

HaskellのParsecですねという感じ。というか記号とかもそのまんま。 ただちょいちょい違うところがあるかなあという印象。

例えば数字のパース。

doc.core> (run digit "123")
\1
nil
doc.core> (run (many digit) "123")
[\1 \2 \3]
nil

run はパース結果を印字する。パースしてない残りの入力はそのまま。パーサの状態は run* で見れる。

doc.core> (run* digit "123")
{:input (\2 \3),
 :pos {:src "", :line 1, :col 2},
 :value \1,
 :ok true,
 :empty false,
 :user nil,
 :error nil}
nil

文字列以外にも入力としてファイルとかも受け付ける。 runf は パーサとファイル名、文字コードを受け取ってパースする。

コンビネータ <*> は与えられたパーサを順に実行し結果をベクタにして返す。

doc.core> (run (<*> letter letter digit) "os8")
[\o \s \8]
nil

parse は run みたいな動作をするけれどパーサの状態を返すという点で異なる。 :ok フィールドをチェックすればパースに成功したかどうか分かる。 :value セレクタを用いればパーサの結果が得られる。

doc.core> (let [st (parse (token* "abc") "abc")]
        (when (:ok st) (str "got " (:value st))))
"got abc"

どういったときに IORef を使うべきか

どういったときに IORef を使うべきか、という疑問が湧いてきたので、ぐぐってみると stackoverflow に同じ質問してる人がいたのでそれを訳してみる(2009年のものだから情報古いかも)。



"いつ IORef を使うべきか混乱しています。 IORef を使うべきか否かを判断するとき、従うべきガイドラインはありますか? どのような状況なら、IORef でなくて State モナドを用いるべきなのでしょうか。"



以下がこの質問に対しての返答です。



" State やそれに関連した ST は、どれもユニットとしてのモノリシックな状態を持つ計算を生成します。これらは、ふつう書き換え可能な状態を一時的なデータとして扱います。つまり、結果を得るために必要なものですが、使ったあとは気にする必要がない(というよりすべき得ない)ということです。

一方で、 IORef に入れるモノは"計算"ではありません。 IORef は IO 内で自由に使える値を保持できるただの箱に過ぎません。この箱はデータ構造も入れられますし、( IO 部分の)プログラムの中で自由に渡せます。保持する値の中身を自由に改変することだってできますし、関数で覆い隠すこともできます。実際、C言語の変数やポインタといった、かなりごちゃごちゃした性質も IORef でモデリングすることもできます。

変数がコードのブロック内に存在するのに、インタラクションと他を完全に分けることは、極めて奇妙、または率直に言ってありえなく見えてしまいます。状態を渡したい時や、データ構造の中に入れたい時もあるかもしれません。そういったときには、”箱”を用意するというアプローチ以外に道はないかもしれません。 Write Yourself a Scheme in 48 hours の”変数と代入”のチャプターが例を示しています。リンク先では、何故 State または ST ではなく、 IORef を用いて Scheme の環境をモデリングすることが最適化がうまく示されています。

簡単に言うと、( Scheme での)環境は任意のネストを構成し、ユーザーインタラクションによるインスタンスを保持したり( (define x 1) と Scheme REPL に入力されたら、 x と入力した際に1が返ってくることを期待しますよね)、 Scheme の関数からの参照を受ける働きなどがあります(Schemeでは関数は環境を捕捉します)。

まとめると、したいことが向いているならば State を用いると綺麗に仕上がる傾向があります。もし複数の状態が必要ならば、 ST を用いるとよいでしょう。しかしもし、状態を計算として扱うのが不便に感じるのであれば、可変のものとして扱うといいでしょう。そのとき IORef は最適解となります。"

実はこのコメントの最後に STM とか TVar を勧めていたんですけれど、一体どうなんでしょう…(^_^;)

構文メモ

関数型言語

<ラムダ式> ::= <変数> | <ラムダ式> <ラムダ式> | λ <変数> . <ラムダ式> | ( <ラムダ式> )
<関数定義> ::= ( <関数名> <ラムダ式> )

論理型言語

<論理式> ::= <リテラル> | <論理式> -> <論理式> | <論理式> ∧ <論理式> | <論理式> ∨ <論理式> | ∀ <変数> <論理式> | ∃ <変数> <論理式>
<リテラル> ::= <述語> | ¬ <述語>
<述語> ::= <述語名> ( <項> {, <項> }* )
<項> ::= <定数> | <変数名> | <関数名> ( <項> {, <項> }* )
<節> ::= { ∀ <変数名> }* <リテラル> { ∀ <リテラル> }*

命令型言語

<文> ::= <代入文> | <制御文> | <名前> : <文> | begin <文>* end
<制御文> ::= <条件文> | <繰り返し文> | <ジャンプ文>

<if 文> ::= if <式> <文>
<if-else 文> ::= if-else <式> <文> <文>
<while 文> ::= while <式> <文>
<for 文> ::= for ( <式> ; <式> ; <式> ) <文>

Parsec を使って Apache のログファイルをパースしてみる2

さて、ログの行をパースすることができるようになったので、これをコマンドラインツールにしましょう。今回は IO と do 記法を用いて作業します。ですので、少なくとも基本は知っていることが望ましいです。もし不安なら、Learn You a Haskell を読みましょう。

ユースケースを見ると、圧縮されていないログファイル一つがあるとして、そのログファイルは複数行のログを保持していると推測できます。私たちのパーサは一行をパースします。なので、どうすればいいかを書いてみましょう。

  1. ファイルを読み込む
  2. 行ごとに切り離す
  3. 各行をパースする
  4. 結果を表示する

最初の操作は Prelude にある readFile を使います。つまり、標準でインポートされるということです。 readFile は IO String 型を持ちます。これは、 IO の文脈を壊せないことを表しています。もしこのことで混乱したならば、上のリンクのチャプターを読んでみてください。

main = do
	file <- readFile "logfile.txt"

main 関数、またはアクションは、すべての Haskell プログラムのスタート地点です。 readFile "logfile.txt" の結果を file という名に束縛するところから始めます。今、 file の型は String です。 String 型ということは、 Prelude の関数 lines に渡せるということですね!しかし、 readFile は IO モナドのなかに包んで渡してくるので、そのままでは lines に渡せません。ですので、 <- ではなく let による束縛を使いましょう。

main = do
	file <- readFile "logfile.txt"
	let logLines = lines file

良い感じですね!もしこれをコンパイルしようとすると、エラーを吐かれます。なぜなら、 do 記法の最後は let による束縛で終わらせることはできないからです。直しましょう。しかしその前に、 Parsec の parse を見てみましょう。ここでは parse の型を書きませんが、 ghci で

 :t parse

とすると見ることができます。さっぱりわかりませんね!
しかし幸運にも、この関数 parse は使いやすいです。基本的には以下のように使います:

parse line "(test)" testLine

つまり、なにか一つ Parser をとり、パーサ名 String をとり、パース対象の String を受け取る関数です。このコードは Either ParserError String を返します。しかし、私たちのパース対象は [String] なので(lines による分割)、 mapM (map のモナド版)を用いてログファイル全体をパースしましょう。

main = do
	file <- readFile "logfile.txt"
	let logLines = lines file
	result <- mapM (parse line "(test)") logLines

result の方は [Either ParseError String] です。これを展開するために、 Prelude の either を用いましょう。 either は2つ関数を取り、 Left の場合は最初の関数を適用、 Right の場合は2つ目の関数を適用する関数です。取り扱うものが Either のリストなので、 either もマッピングする必要があります。 ParseError は Show クラスのインスタンスなので、どちらのケースでも単純に関数 print を使って印字することにしましょう。

main = do
	file <- readFile "logfile.txt"
	let logLines = lines file
	result <- mapM (parse line "(test)") logLines
	mapM_ (either print print) result

できました!このスクリプトはパーサの各結果、エラーまたはデータ型 LogLine を印字します。

あまり努力してもいないのにもかかわらず、他言語ではなかなか難しいことを、このスクリプトは少ないメモリで実現します!遅延評価のマジックの恩恵により、各行は読まれ、パースされ、印字され、ガベージコレクションされます。とは言っても、これはあまり効率のいい実装ではないので、もっと最適化してみましょう。

Parsec を使って Apache のログファイルをパースしてみる

この記事について

この記事は、 Erik さんのを翻訳したものです。

導入

Haskell について知っていればいるほど良いですが、あなたが熱心な Haskeller であるならこの記事は少々退屈に感じるでしょう。

もし Haskell の基本について学びたいのであれば、Learn You A Haskell (邦訳:すごいHaskellたのしく学ぼう)をおすすめします。この本は何度も読みたくなります!また、どのようにプログラムを組んでいくかを詳しく知りたい人は、 Real World Haskell を読みましょう。実践に生かせる練習問題やアドバイスが沢山載っています。私はこのコードを書くとき、この本を頼りに作業をしていました。

Apache はおそらく世界で最も普及しているウェブサーバでしょう。多くのウェブサーバと同じように、様々な情報とともに各リクエストのログをとっています。大きなIT企業はこうした情報を活用しているでしょうが、私達のほとんどはこのログを増やすだけで放置しています。このログを使って何か素晴らしい情報を引き出してみましょう!この記事で取り扱うログフォーマットは Apache combined log format です。これはサンプルです。

192.168.1.80 - - [18/Feb/2011:20:21:30 +0100] "GET / HTTP/1.0" 503 2682 "-" "-"

もしあなたのログファイルが違うフォーマットであるなら、多少の調節が必要です。フィールドは順に:クライアントの IP アドレス、クライアントのアイデンティティ、ユーザ ID、リクエストを受け取った日時、リクエスト、ステータスコード、バイト、 "referer" HTTP リクエストヘッダ、 "user-agent" HTTP リクエストヘッダです。これらの情報が確実に保持できるように、ログのそれぞれの値をパースして、データ型にまとめましょう。さて、定義してみましょう!

data LogLine = LogLine {
	  getIP :: String
	, getIdent :: String
	, getUser :: String
	, getDate :: String
	, getReq :: String
	, getStatus :: String
	, getBytes :: String
	,getRef :: String
	, getUA :: String
} deriving (Ord, Show, Eq)

レコードシンタックスを使ってアクセサを用意しました。また、便利のためにこれらをOrd, Show, Eq 型クラスのインスタンスにしました。

次は、行のパースを実際にパースしましょう。以前、このスクリプトPython で書いた時、正規表現を用いてパースをしました。すると、コーディングに時間がかかり、また使っている間はずっとバグに出会う羽目になりました。しかし、 Haskell はもっとよい方法を提示してくれています。正規表現を用いるより、 Parsec ライブラリのパーサコンビネータを使いましょう。 小さなパーサをつなげて大きなパーサにするという Parsec のアイデアは、対象物が何であるかを一歩一歩確実に定義していくことを可能にしています。例を見ながらが理解しやすいと思います。

Parsec

上にあるログの例の一行をみると、それぞれの値がスペースで区切られています。しかし、いくつかの値はクォートや角括弧で囲まれた値の中にスペースを含んでいます。ですので、私たちは3つの種類の値をパースすることになります。まずは一番簡単な、スペースを含んでいない値のパーサを定義してみましょう。

plainValue :: Parser String
plainValue = many1 (noneOf " \n")

型シグナチャは、「 plainValue の結果は、 Parser モナドに包まれた String 」と読めます。 Parsec がモナドであることの利点のひとつとして、他のパーサの中でも do 記法で使えるようになることです。 plainValue パーサに話を戻しますが、 最初に many1 を使っています。これは、パーサをひとつとって、それを1つ以上適用し、最終的に値のリストを返すアクションです。私たちのケースでは、スペースでない限り文字を消費したいのです。なぜなら、 Apache のログ行の値はスペースで区切られているからです。このため、文字リストを受け取って、文字が渡されたリストでない場合、その文字をひとつ消費する noneOf を用いています。

これらをつなげて、 many1 (noneOf " \n") はスペースまたは改行に当たるまで文字を消費します。素晴らしい!

次は、角括弧で囲まれた値とクォートで囲まれた値をどうするかを考えましょう。どちらもその地の中にスペースを含む可能性があるため、これらのために特別にパーサを作る必要があります。

brancketedValue :: Parser String
brancketedValue = do
	char '['
	content <- many (noneOf "]")
	char ']'
	return content

quotedValue :: Parser String
quotedValue = do
	char '"'
	content <- many (noneOf "\"")
	char '"'
	return content

これらは do 記法で書かれたパーサです。簡潔なだけでなく、ログ行のフォーマットを表しています! brancketedValue をみてみましょう:まず、左端にある括弧を消費しています。そのあと、 many1 (noneOf "]") の結果を content という名に束縛します。先ほどの many1 (noneOf " \n") のように、与えられた文字列に該当しない限り、文字を消費していきます。最終的に、最後の文字である右角括弧を消費し、 return によって結果を Parser モナドで包みます。

これですべての値をパースできるようになったので、これらのパーサをどのように使って1行をパースするかを定義しましょう。それぞれの結果を LogLine データ型にまとめたいので、それぞれのパーサの結果を対応する値に結びつける必要があります。さあ、やってみましょう!

logLine :: Parser LogLine
logLine = do
	ip <- plainValue
	space -- スペースをパースしてその結果を捨てる
	ident <- plainValue
	space
	user <- plainValue
	space
	date <- brancketedValue
	space
	req <- quotedValue
	space
	status <- plainValue
	space
	bytes <- plainValue
	space
	ref <- quotedValue
	space
	ua <- quotedValue
	return $ LogLine ip ident user date req status bytes ref ua

多少余分な記述がありそうですが、このパーサを解釈するのは容易です。信じられないもしれませんが、これで終わりです。このスクリプトApache combined log スタイルの行をパースします。では実際に例をパースしてみましょう!

testLine = "192.168.1.80 - - [18/Feb/2011:20:21:30 +0100] \"GET / HTTP/1.0\" 503 2682 \"-\" \"-\""

main = case parse logLine "(test)" testLine of
	Left err -> print err
	Right res -> print res

parse は入力に対するパーサの結果を Parsec モナドから引き出し、エラーまたは結果を返します。結果は以下のようになります。

LogLine { getIP = "192.168.1.80"
	, getIdent = "-"
	, getUser = "-"
	, getDate = "18/Feb/2011:20:21:30 +0100"
	, getReq = "GET / HTTP/1.0"
	, getStatus = "503"
	, getBytes = "2682"
	, getRef = "-"
	, getUA = "-"
	}

サンプルコードはここにあります。

さらなるパーサへの誘い

引き続き、Magnus氏のブログの翻訳です。


前回の記事に対して、Conal Elliottから興味深いコメントを頂きました。

もっと関数的でApplicativeな書き方をしてもいいかもね。
doで繋がったParsecコードをliftM, liftM2, ...といったように置換してみよう。読んだ感じできそうだし。parseRegionのところは、残りのスペースを捨てる"thenSpace (many1 digit)"みたいな補助的な関数を用意するといいよ。
あと、parseAddressやparsePerms、parseDeviceのなかにある似たようなパターンをくくり出してみるのもいいね。
くくりだしをすればするほど、よりエレガントなコードになるし、理解もしやすくなるよ。

最初は何を言っているのかわからなかったのですが、(今でも怪しいところだけれど)だいたい意味が分かって来ました。

基本的に、私のコードはdoをすべてのパースするコードに利用しているため、非常に連続的です。個人的にはこれが流れによく似ていて見やすく感じていました。ただ、私自身もこのコードが”関数的”とは言い難いことはわかっていたので、Conalのコメントに従ってみて、どうなるかを見てみたいと思います。

まずは、行の中にあるすべての要素は空白文字で切り離されているものだとします。ただし、いくつかのものは他の文字によって切り離されているとします。なので、最初に私がしたことは、「文字を読み込んで、その次に文字列を読み込んで、(文字によって区切られた、)最初に読み込んだものを返す高階関数」を作ることでした。

thenChar c f = f >>= (\r -> char c >> return r)

スペースがセパレータの役目を果たすため、そういった関数を定義しましょう。

thenSpace = thenChar ' '

parseAddressのなかでこれを使ってみます。

parseAddress = let
	hexStr2Int = Prelude.read . ("0x" ++)
	in do
		start <- thenChar '-' $ many1 hexDigit
		end <- many1 hexDigit
		return $ Address (hexStr2Int start) (hexStr2Int end)

他のパーサ関数も同じようにthenCharやthenSpaceを使って素直に書き直せます。

私が、Conalがコメントで言及していたliftMの部分を完全に理解できたかはちょっと怪しいです。
おそらく私が文字列を読み込んで、それを構造体(データ型)に変換していることについて言及しているのではないかと。liftMを使うことで、その変換作業をコードの中で行うことができます。下のコードは、parseAddressをhexStr2Intを移動させたものです。

parseAddress = let
	hexStr2Int = Prelude.read . ("0x" ++)
	in do
		start <- liftM hexStr2Int $ thenChar '-' $ many1 hexDigit
		end <- liftM hexStr2Int $ many1 hexDigit
		return $ Address start end

こんな感じで他のパース関数も変えて行きました。

parsePerms = let
	cA a = case a of
		'p' -> Private
		's' -> Shared
	in do
		r <- liftM (== 'r') anyChar
		w <- liftM (== 'w') anyChar
		x <- liftM (== 'x') anyChar
		a <- liftM cA anyChar
		return $ Perms r w x a
parseDevice = let
	hexStr2Int = Prelude.read . ("0x" ++)
	in do
		addr <- thenSpace parseAddress
		perm <- thenSpace parsePerms
		offset <- liftM hexStr2Int $ thenSpace $ many1 hexDigit
		dev <- thenSpace parseDevice
		inode <- liftM Prelude.read $ thenSpace $ many1 digit
		path <- parsePath <|> string ""
		return $ MemRegion addr perm offset dev inode path

このコードが果たして関数型っぽいかどうかは自信がもてません…。読みやすい…?



以下はコメントです。

Holger

あなたの記事とConalのコメントに触発されてliftMと遊んでみて、別のヴァージョンのparseAddressを作ってみました。
確かにすべての関数をそのようにかけますね。
けど正直なところ、do記法で書かれたコードのほうがよっぽど読みやすく感じます。

Twan van Laarhoven

パーサを読みやすくする一番の方法はData.Applicativeを使うことです。また、多くの人はlet ... inを好んで使いますね。例えばこんな感じに:

parseHexStr = Prelude.read . ("0x" ++) many1 hexDigit
parsePath = many1 (char ' ') *> many1 anyChar

parseRegion = MemRegion
	parseAddress *> char ' '
	parsePerms *> char ' '
	parseHexStr *> char ' '
	parseDevice *> char ' '
	(Prelude.read many1 digit) *> char ' '
	(parsePath return "")

あと、基本的に、(liftM# f x y z)は f <$> x <*> y <*> zのようにかけます。

Conal Elliott

そうですね、大体あってます。一度doスタイルからliftM#スタイルに変わったら、モナド演算子をApplicativeファンクタ演算子に置き換えていくのは簡単です。

Magnus

Conalへ。
これは、applicativeな演算子を試してみる必要がありそうですね。

パーサへの誘い

Magnusさんのご厚意により、邦訳してもよいことになりましたので掲載させて頂きます。
もとの記事はこちらのAdventures in parsingです。


私はParsecが使いこなせるようになりたいと常日頃から思っていました。何度か試してみたのですが、そのいずれもどこかで躓いて失敗し、そういった期間が非常に長かったので、何度試したかは忘れてしまいました。私の~/devo/test/haskellのなかに書きちらされたコードたちが私の失敗の数々を物語っています。

さて、私の一番最初の試みとして、/proc/< pid >/mapsの中身をパースすることを考えました。
mapsのマニュアルを見てみましょう。

どうやら、

address perms offset dev inode pathname
08048000-08056000 r-xp 00000000 03:0c 64593 /usr/sbin/gpm

という構造になっているようです。

まず、いくつかのデータ型を用意し、つなげていきます。最初にアドレスの範囲を表します。

data Address = Address { start :: Integer, end :: Integer }
	deriving (Show)

パーミッションにsまたはpが入っているものはAccessと呼ぶことにします。

data Access = Shared | Private
	deriving (Show)

rwxのようなパーミッションは簡単にブール型として表しましょう。

data Perms = Perms {
	read :: Bool,
	write :: Bool,
	executable :: Bool,
	access :: Access
}
deriving (Show)

デバイスは素直に。

data Device = Device { major :: Integer, minor :: Integer }
	deriving (Show)

最後に、これらをメモリ空間を表すひとつのデータ型に固めます。

data MemRegion = MemRegion {
	address :: Address,
	perms :: Perms,
	offset :: Integer,
	device :: Device,
	inode :: Integer,
	pathname :: String
}
deriving (Show)

すべてのデータ型はShowのインスタンスとしている(つまり最低でもshowは使える)ので、それらの出力は簡単になっています。

さて、Parsecを使っていきます。トップダウンかボトムアップの方法で開発ができますが、私は後者を選ぶことにしました。しかし、mapsファイルの中の一行がシンプルな形式なので、最終的にどういった関数になるのかが容易に想像できますね。私がボトムアップですると決めたのは、データ型のおかげで、どのように一行を切り離すかが簡単にわかるからです。まず、アドレスの範囲をパースしてみます。

parseAddress = let hexStr2Int = Prelude.read . ("0x" ++)
	in do
		start <- many1 hexDigit
		char '-'
		end <- many1 hexDigit
		return $ Address (hexStr2Int start) (hexStr2Int end)

アドレス自体が16進数表記で表されており、少なくとも1文字以上の長さがあるので、many1 hexDigitを使っています。count 8 hexDigitのように、アドレスの長さが8文字であるかをチェックするのが安全であるとは思いますが(少なくとも32bitマシンでは)、私は試していません。
16進数からIntegerに変える方法は2つあります。一つは上でしたようにPrelude.readが0xから始まる文字列を16進数として読みこんでくれることを利用する方法です。もう一つは fst . (!! 0) . readHexを使ってする方法です。
マニュアルページによると、アドレスはダッシュ'-'で区切られているようなので、char '-'で処理しています。

この関数をテストするのは簡単です。ghciを使ってロードし、parseを使用します。

*Main> parse parseAddress "" "0-1"
Right (Address { start = 0, end = 1 } )
*Main> parse parseAddress "hhh" "01234567-89abcdef"
Right (Address { start = 19088743, end = 2309737967 } )

うまくいっていますね。

次に、パーミッションをパースします。これは素直にかけるのでコメントは必要ないかもしれません。

parsePerms = let
	cA a = case a of
		'p' -> Private
		's' -> Shared
	in do
		r <- anyChar
		w <- anyChar
		x <- anyChar
		a <- anyChar
		return $ Perms (r == 'r') (w == 'w') (x == 'x') (cA a)

デバイス情報も上記のアドレスのものと同じ書き方で実装できますが、今回はダッシュではなくコロン':'で分けられています。

parseDevice = let
	hexStr2Int = Prelude.read . ("0x" ++)
	in do
		maj <- many1 digit
		char ':'
		min <- many1 digit
		return $ Device (hexStr2Int maj) (hexStr2Int min)

次に、MemRegionインスタンスを作り出すためにこれらをつなげましょう。

parseRegion = let
	hexStr2Int = Prelude.read . ("0x" ++)
	parsePath = (many1 $ char ' ') >> (many1 $ anyChar)
	in do
		addr <- parseAddress
		char ' '
		perm <- parsePerms
		char ' '
		offset <- many1 hexDigit
		char ' '
		dev <- parseDevice
		char ' '
		inode <- many1 digit
		char ' '
		path <- parsePath <|> string ""
		return $ MemRegion addr perm (hexStr2Int offset) dev (Prelude.read inode) path

問題は、パス名のない行なんかもあることです。こんな感じで。

address perms offset dev inode pathname
09058000-0805b000 rwxp 00000000 00:00 0

inodeのあとにスペースがあるようなので、parseRegionの中のchar ' 'は残しています。もしパスをパースしようとして失敗したら、空の文字列をパースすることで解決しました。それがparsePath <|> string ""の意味です。パスネームはどうやらスペースの数によって整えられているようですが、面倒なのでスペースをすべて消費しています。パスネームとしてどの文字が使用できるのか把握していないので、文字があったらとにかく消費していくことにしています。

ここで、つくったこの関数で遊ぶために、pidによる指定で特定のプロセスのmapsファイルをパースし、その結果をMemRegionインスタンスのリストで返す関数を考えました。

getMemRegions pid = let
	fp = "/proc" </> show pid </> "maps"
	doParseLine' = parse parseRegion "parseRegion"
	doParseLine l = case (doParseLine' l) of
		Left _ -> "Failed to parse line"
		Right x -> x
	in do
		mapContent <- liftM lines $ readFile fp
		return $ map doParseLine mapContent

上のコードでは、行をIOモナドからうけとり、Parserモナドにそれを渡し、再びIOモナドに返しています。試してみましょう。

*Main> getMemRegions 1

これで実行できますが、ものすごい数で出力されたりするのでtakeを使って制限したりするとよいでしょう。ですので、最後の行は、

return $ map doParseLine (take 4 mapContent)

となります。
さて、これで最初のコマンドライン引数をpidとして受け取るプログラムを書いてみましょう。

main = do
	pid <- liftM (Prelude.read . (!! 0)) getArgs
	regs <- getMemRegions pid
	mapM_ (putStrLn . show) regs

これでパーサは完成です。

ちなみに、以下のモジュールをインクルードする必要があります。

import Control.Monad
import System
import System.FilePath
import Text.ParserCombinators.Parsec