みどりねこ日記

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

さらなるパーサへの誘い

引き続き、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な演算子を試してみる必要がありそうですね。