さらなるパーサへの誘い
引き続き、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な演算子を試してみる必要がありそうですね。