IArrayの怠け度合い

今日はちょっとしたメモ…

リストのレイジーな振舞い方は割りとはっきりとイメージできるのですが、ちょっと遊びでIArrayを使っていて遅延評価がどういった形で動いているのかが気になったので、ちょっと実験してみました。

IArray、つまりImmutableなArrayを作る方法はいくつかあるのですが、基本は配列に変換したいデータをリストとして渡す形になります。僕が気になったのは、リストはいつどどういった形で評価されるのかということですね…たとえば、

getContents :: IO String

などは、unsafeを使ってそれぞれの文字が必要になったときにIOを行うようになっています。getContentsで取得した文字列を配列に変換するとして、ファイルIOはいったいどの時点で起きるのでしょう?

ということで書いてみた実験コードはこんな感じ:

module Main where

import Data.Array.IArray
import Debug.Trace

data SqueakyList a = Sq a (SqueakyList a) | Nil

toSql :: [a] -> SqueakyList a
toSql [ ] = Nil
toSql (a:as) = Sq a $ toSql as

toLst :: SqueakyList a -> [a]
toLst Nil = [ ]
toLst (Sq a as) = trace "list" $ a : (toLst as)

squeak a = trace ("element" ++ (show a) ) a

str = "test"
strSq = map (squeak) str

print3rd :: Array Int Char -> IO ()
print3rd ar = trace "!2" $ print (ar ! 2)

ar :: Array Int Char
ar = arit $ toLst $ toSql $ map (squeak) str

arit :: [a] -> Array Int a
arit as = listArray (0, 3) as

main = do
	putStrLn "Test1"
	print3rd $ arit $ toLst $ toSql strSq
	putStrLn "Test2"
	print3rd $ arit $ toLst $ toSql strSq
	putStrLn "Test3"
	print $ take 2 $ assocs ar
	putStrLn "Test4"
	print $ assocs $ ar

リストの各要素が展開されていくのを見るためにSqueakyListという型を定義しました。これはリストの要素が展開されるたびにトレースを出すようになってます。ここはもっとシンプルなやり方もありそうですね…

そして、このコードの出力はこうなりました:

Test1
!2
list
list
list
list
element's'
's'
Test2
!2
list
list
list
list
's'
Test3
list
list
list
list
element't'
element'e'
[(0,'t'),(1,'e')]
Test4
element's'
element't'
[(0,'t'),(1,'e'),(2,'s'),(3,'t')]

さて、何がわかったかというと:
1.配列の生成に渡したリストは配列の要素への最初のアクセスが起きた時点で配列の生成に使われる部分はすべて展開される。ということで、getContentsの結果をそのまま配列の生成に使うと、必要なIOはすべて配列の要素に始めてアクセスする時点で起きるようです。これはTest1のトレースが!2の後にlistが4回立て続けに起きていることでわかりますね…

さらに、ほかにもわかったことがあります。
2.どうやらリストの各要素に格納されている値もボックス型になっているようで、値が評価されるのはどうやら配列の中のその要素がアクセスされた時点のようです。Test1では3文字目の's'を取り出すのにelementトレースが一つ出てますね…でも、Test2のほうではメモ化が効いているようでelementトレースが出ていません。ということで、メモ化はstrSqの内部で起きているということがいえそうですね。

3.さらにTest3とTest4を見てもメモ化の作用が見て取れますね。

ちなみにリストは今回のSqueakyListでもわかるようにHaskellの言語の範疇でで簡単に作ることができるのですが、配列のほうはどうもそうはいかないようですね。IArrayのコードをたどっていくと最終的にはコンパイラなのか、ランタイムなのかわかんないんですが、unsafeな配列生成内部関数が呼び出されているのがわかります。リンクリストよりも配列のほうがインプリメントが難しい言語というのも面白いですね…

きょうはこのへんで。
ではでは。

Programming Windows in Haskell、次の一歩。

もうかれこれ半年以上前のポストの続きになるんですが、なんというか、HaskellでWin32を直にたたくスタイルでGUIアプリを書いてみたわけなんですが、前回挫折したのは、ウィンドウごとのステートをどうやって保持するかというところだったわけです。IORefを作ってその中にステートを書き込んだり読み出したりする方法をHaskellCafeなどでも見かけたりしたのですが、個人的にはなんともしっくり来ていなかったわけです。

先日久しぶりにこの辺のことをごにょごにょといじくってみる気になったので、コードをさわっていたわけです。HaskellでWin32アプリを書いたとき一番最初に僕が試したのはウィンドウのステートをShowしてSetWindowText/GetWindowText経由で保存するという技でした。これをやったときはまだIORefなんていうものも知らず、とにかく何か動かしたいと思ってやったことでした…

そして、次に出てきたのがIORef,Win32の常套手段ではSetWindowLong/GetWindowLongとかあるわけなんですが、なんか、やる気をそそられるものがなかったんですね…

今回、久々に気がむいて手を出してみたのはメッセージポンプの再帰呼び出しを利用して、変化するステートを維持するというモデルでした。スケッチとしては:

pump st = do
	(st', _) <- wndProc st
	pump st'
	
wndProc :: ST->HWND->MSG->WPARAM->LPARAM->
 IO(ST,LRESULT)

なんてものを夢見ていたわけです。でも、無理やり動かしてみると、これはもうWin32のプログラミングのルールを破りまくりで、いろんな形でおかしな動きをしてしまうわけです。

しかし、これをやって気づいたのは、ウィンドウプロシジャにひとつパラメタを追加してやったことで、そのウィンドウに独自の情報をCreateWindowじかに渡すことができることです。あとは、これがうまく変化させられれば良いわけなんです…
ぼーっとGraphics.Win32.Window.createWindowのコードを眺めていると、なぜか、このインプリメンテーションはAPIにユーザー指定のウィンドウプロシジャを渡さずに、その直後にsetWindowClosureという関数でウィンドウプロシジャを指定しています…不思議ですね…

setWindowClosureって、Haskellの関数をWin32のコールバック関数として登録できるのねーとおもって眺めていたら、ちょっと思いついちゃったわけです。といっても、そんな大それたことではないんですけど。

それはつまり、ウィンドウプロシジャ内でウィンドウのステートが変わるたんびに
  setWindowClosure wndProc st'
とやってやれば、そのウィンドウの状態をうまく管理できそうだということです。

それでやってみたのがこんなのです:

module Main
	where

import System.Win32.Types
import System.Win32.DLL
import Graphics.Win32
import Graphics.Win32.Misc
import Graphics.Win32.GDI.Types 
import Graphics.Win32.Message
import Graphics.Win32.Window
import Data.List (unfoldr)
import Data.Int (Int32)
import Data.Maybe
import Control.Monad
import Control.Arrow

foreign import stdcall "PostQuitMessage" 
   postQuitMessage :: Int32 -> IO ()


type Vector = [Float]
type Matrix = [ [Float] ]

transform :: Matrix -> Vector -> Vector
transform mx v = [ (mul r) v| r <- mx]
 where
  mul rs cs = sum $ zipWith ( * ) rs cs

mxRotXZ :: Float -> Matrix
mxRotXZ rot = [ [cos rot, 0, -1 * (sin rot) ],
  [0,       1,             0 ],
 [sin rot, 0,       cos rot ] ]

proj :: Vector -> Vector
proj v@(x: y: z: [ ]) = [x * fac, y * fac]
 where
  fac = (z - zvp) / (zvf - zvp)
  zvp = -4 :: Float
  zvf = -1 :: Float

cube :: [Vector]
cube = 	[ [1,  1,  1], [1, -1,  1], [-1, -1,  1], [-1,  1,  1],
  [1,  1, -1], [1, -1, -1], [-1, -1, -1], [-1,  1, -1],
  [1,  1,  1], [1,  1, -1], [-1,  1, -1], [-1,  1,  1],
  [1, -1,  1], [1, -1, -1], [-1, -1, -1], [-1, -1,  1] ]

toMaybe :: Bool -> a -> Maybe a
toMaybe p a
 | p = Just a
 | otherwise = Nothing

varMap :: ( [a] -> (b, [a] ) ) -> [a] -> [b]
varMap f a = unfoldr (uncurry toMaybe . (not.null &&& f) ) a

main = do
 let clsName 	=  mkClassName "My Window Class"
 hinst 		<- getModuleHandle Nothing
 whiteBrush	<- getStockBrush wHITE_BRUSH
 curArrow	<- loadCursor Nothing iDC_ARROW
 mAtom 		<- registerClass
  (cS_DBLCLKS, 
  hinst,		-- HINSTANCE
  Nothing,	-- Maybe HICON
  Just curArrow,	-- Maybe HCURSOR
  Just whiteBrush,-- Maybe HBRUSH
  Nothing,	-- Maybe LPCTSTR
  clsName)

 when (isJust mAtom) $ do
  hwnd <- createWindow 
   clsName
   "Rotating Box Demo" 
   (wS_THICKFRAME + wS_CAPTION + wS_SYSMENU) 
   Nothing 
   Nothing 
   Nothing 
   Nothing 
   Nothing 
   Nothing 
   hinst
   (wndProc 0)

  setWinTimer hwnd 0{-tid-} 50{-msec-}

  showWindow hwnd sW_SHOWNORMAL
  updateWindow hwnd
  allocaMessage $ pump

  unregisterClass clsName hinst

pump lpmsg = do
 fContinue <- getMessage lpmsg Nothing
 when fContinue $ do
  translateMessage lpmsg
  dispatchMessage lpmsg
  pump lpmsg

drawBox4 :: HDC -> [ [Int32] ] -> IO ()
drawBox4 hdc [ [x1, y1], [x2, y2], [x3, y3], [x4, y4] ] = do
 moveToEx hdc x1 y1
 lineTo   hdc x2 y2
 lineTo   hdc x3 y3
 lineTo   hdc x4 y4
 lineTo   hdc x1 y1

drawCube :: HDC -> Matrix -> IO ()
drawCube hdc rot = mapM_ (drawBox4 hdc) $ 
 varMap (splitAt 4) $ 
 map ( 
  (map $ ( + 400).round.( * 100) ) . proj . transform rot)
  cube

render :: HWND -> HDC -> Int -> IO ()
render hwnd hdc i  = (drawCube hdc . rot . fromIntegral) i
 where
  rot :: Float -> Matrix
  rot x = mxRotXZ $ (pi * x/ 180) 

onTimer :: HWND -> Int -> IO ()
onTimer hwnd i = do
  invalidateRect (Just hwnd) Nothing True
  setWindowClosure hwnd (wndProc $ (i + 2) `mod` 360) 

wndProc :: Int -> HWND -> WindowMessage -> WPARAM -> 
  LPARAM -> IO LRESULT
wndProc i hwnd wm wp lp
  | wm == wM_KEYDOWN 	= doFinish
  | wm == wM_LBUTTONDOWN 	= doFinish
  | wm == wM_DESTROY 	= killTimer (Just hwnd) 0 >> 
               postQuitMessage 0 >> return 0
  | wm == wM_SIZE	= doInvalRender
  | wm == wM_PAINT   	= onPaint
  | wm == wM_TIMER	= onTimer hwnd i >> return 0
  | otherwise 	   = defWindowProc (Just hwnd) wm wp lp
  where
    doFinish = sendMessage hwnd wM_CLOSE 1 0 >> return 0
    doInvalRender = do
         invalidateRect (Just hwnd) Nothing True
         return 0
    onPaint 	= allocaPAINTSTRUCT $ \ lpps -> do
     hdc <- beginPaint hwnd lpps
     render hwnd hdc i
     endPaint hwnd lpps
     return 0

昔、Doukakuのお題にあわせてやった簡易3Dアプリなんですが、今回の肝の部分は
wndProcの第一パラメタが(簡単すぎるという話はありますが)ウィンドウの状態を保持しているパラメタ、(回転する箱の角度)そしてそのパラメタがonTimerの中でsetWindowClosureでアップデートされています。

カリー化を使ってステートを管理できるとはなんともHaskellらしくていいかなと思ったわけです。よけいなIORefもいらないし…

ではでは。

PEGパーサーのバックトラッキング

前回、どちゃっと乗せたPEG電卓のコードなんですが、これはPappyやFrisbyのようにメモ化をまったくやっていないPEGパーサーです。ちょっと思いついて、いったいどれくらいの勢いでバックトラッキングが起きているのかを調べてみました。
バックトラッキングがたくさん起きているということはメモ化がそれだけ動作効率に影響しやすいという事になりますよね…

あれだけでかいコードでも、実際に文字をマッチしている関数は二つだけです。そこで、その2つの関数にDebug.Trace.traceをくっつけてみることにしました

_char :: (a -> Bool) -> [a] -> Maybe ([a], [a])
_char _  = Nothing
_char f (c:cs) =
	if (f c) then Just ([c], cs)
		else trace "_char failed" Nothing

eofP :: Parse [a] [a]
eofP = Parse $ \i -> f i
	where
		f  = Just(, )
		f _ = trace "eofP failed" Nothing

そして、コンパイルしなおした電卓に簡単な計算をさせます。

calc "1 + 1"

=>
_char failed
_char failed
  :
     中略
  :
_char failed
_char failed
_char failed
_char failed
_char failed
_char failed
_char failed
_char failed
2

ということで、トレースメッセージの出現回数をカウントするとなんと46回です!
何でこんなことになったのでしょう?入力したのはたったの5文字です…

今回のコードの元になったPEGを引っ張ってきましょう

OPTBLANK=(' '/'\t')*

DIGIT = (1/2/3/4/5/6/7/8/9)
DIGIT0 = (0/DIGIT)

NUMBER=(ZERO/-?NONZERO)

ZERO = 0!(DIGIT0)
NONZERO = (DIGIT, DIGIT0*)

BLOCK=(\(,OPTBLANK,FORMULA,OPTBLANK,\),OPTBLANK,)
BLOCKORNUM=(NUMBER/BLOCK)

ADDDEC=(MULDIVORNUM,OPTBLANK,(+/-),OPTBLANK,ADDDEC)
MULDIV=(BLOCKORNUM,OPTBLANK,(*/\//),OPTBLANK,MULDIV)
MULDIVORNUM=(MULDIV/BLOCKORNUM)

FORMULA=(OPTBLANK,(ADDDEC/MULDIVORNUM))

バックトラックという観点から見ると、'9'をDIGITがパースするのには実はバックトラックを8回することになります。同じ'9'をNUMBERがパースするためには、まずZEROが失敗して、その次にオプショナルな'-'、そしてやっとNONZEROにいきますが、そこで'9'を発見した後にまたDIGIT0*なんていうのがあって、残念ながら次の文字はないので、ここではすべての選択肢を試した挙句にフェイルして帰ってくることになります。

ということで、NUMBERだけでもかなり大変なことになっているのがわかりますね…

もう少し上のレベルのパーサーを見てみると、ADDDECもMULDIVもさらにそれらを統合しているFULDIVORNUMやFORMULAも選択オペレータをとても贅沢に使っていることがわかりますね…自分の頭でこのPEG文法に沿って数式をパースしようと思うと気が遠くなりそうです。1 + 1をパースするのに46回バックトラックしてもおかしくない気がしてきますね…

ちょっと考えてみると、バックトラックしたときの振る舞いはおそらく2つに大別できて、一度フェイルしたらもうその文字に対して同じパーサーが走ることはない場合(たとえばDIGITが'9'をパースするときの1−8まで)と、同じパーサーが同じ文字列に対して走る場合があるようです。

2つ目のほうの例を今回の電卓で見てみると、足し算は乗除よりもプライオリティが低いうえ、両方ともパラメタを2項とも含んだ形で定義されているので、1 + 1の最初の1はまずはMULDIVORNUM経由のMULDIVの1項目めとしてマッチして、そのあと*も/もないのでMULDIVが失敗して、次にMULDIVORNUMの第2選択肢であるBLOCKORNUMにもう一度マッチします…

この場合はメモ化が効果を発揮しそうですね…メモ化をしなくてもパーサーの定義のほうである程度のバックトラック量の削減はできそうな気もします。パーサーの可読性や再利用性は犠牲になるかもしれませんが…

今日はこの辺で。
ではでは。

PEG電卓をやってみた

前回のポストから日数がたってしまいましたが、たまにはちゃんと最後まで書こうということで、出来上がったPEG電卓のコードを乗せておきます。

パーサーの定義が前回までのポストとは微妙に変わっていますが、大筋は同じです。こっちのほうがコンパイルして動作確認も取れてます。

電卓のほうは以前に書いた四則演算のPEG表現を素直にPEGパーサモナドで書き換えたものです…空白の処理なんかが増えてますが…

コードが長いのでこのポストはこれでおしまい。

module Main where

import System (getArgs)
import Data.List (intercalate)

ifNothing Nothing v = Just v
ifNothing (Just _) _ = Nothing

newtype Parse i o = Parse {
		runParse :: i -> Maybe (o, i)
	}

instance Monad (Parse i) where
	(>>=) p f = Parse $ \i ->
		case (runParse p i) of
			Nothing -> Nothing
			Just (o, i') -> runParse (f o) i'
	return o = Parse $ \i -> Just (o, i)
	fail _ = Parse $ \_ -> Nothing

_char :: (a -> Bool) -> [a] -> Maybe ([a], [a])
_char _  = Nothing
_char f (c:cs) =
	if (f c) then Just ([c], cs)
		else Nothing

eofP :: Parse [a] [a]
eofP = Parse $ \i -> f i
	where
		f  = Just(, )
		f _ = Nothing

charP :: Eq a => a -> Parse [a] [a]
charP ch = Parse $ \str -> _char (ch ==) str

strP :: Eq a => [a] -> Parse [a] [a]
strP str = foldr seqP emptyP $ map (charP) str

anyOfP :: Eq a => [a] -> Parse [a] [a]
anyOfP chs = Parse $ \str -> _char ( (flip elem) chs) str

anyButP :: Eq a => [a] -> Parse [a] [a]
anyButP chs = Parse $ \str -> _char (not.(flip elem) chs) str

anyP :: Parse [a] [a]
anyP = Parse $ \str -> _char (\_ -> True) str

followedP :: Parse i [o] -> Parse i [o]
followedP pf = Parse $ \i -> (runParse pf i) >> return ([ ], i)

notFollowedP :: Parse i [o] -> Parse i [o]
notFollowedP nf = Parse $ \i -> ifNothing (runParse nf i) ([ ], i)

ifP:: Parse i o -> (o -> Parse i o) -> Parse i o -> Parse i o
ifP p ps pf = Parse $ \i ->
	case (runParse p i) of
		Nothing -> runParse pf i
		Just (o, i') -> runParse (ps o) i'

emptyP :: Parse a [b]
emptyP = return  []

optP :: Parse i [o] -> Parse i [o]
optP p = ifP p return emptyP

repeatP :: Parse i [o] ->  Parse i [o]
repeatP p = ifP p (\o -> (repeatP p) >>= return.(o++)) emptyP

repeatP1 :: Parse i [o] -> Parse i [o]
repeatP1 p = seqP p (repeatP p)

choiceP :: Parse i o -> Parse i o -> Parse i o
choiceP p q = ifP p return q

seqP :: Parse i [o] -> Parse i [o] -> Parse i [o]
seqP p q = do
	v <- p
	w <- q
	return (v ++ w)
	
type StrParser = Parse String String
digitP :: StrParser
digitP = anyOfP ['1'..'9']

digit0P :: StrParser
digit0P = anyOfP ['0'..'9']

unaryP = charP '-'

numberP :: StrParser
numberP = ( (charP '0') `seqP` (notFollowedP digitP) )
	`choiceP` 
	( (optP unaryP) `seqP` digitP `seqP` (repeatP digit0P) )

numPI :: Parse String [Int]
numPI = do
	num <- numberP
	return [read num]

blankChP :: StrParser
blankChP = anyOfP " \t"

blankP = repeatP blankChP

blockPI = do
	(charP '(') `seqP` blankP
	val <- expPI 
	blankP `seqP` (charP ')') `seqP` blankP
	return val

addDecPI = addPI `choiceP` decPI

numBlkMulDivPI = mulPI `choiceP` divPI `choiceP` numBlkPI

numBlkPI :: Parse String [Int]
numBlkPI = blockPI `choiceP` numPI

addPI :: Parse String [Int]
addPI = do
	(val1:_) <- numBlkMulDivPI
	blankP `seqP` (charP '+') `seqP` blankP 
	(val2:_) <- expPI
	return [val1 + val2] 

decPI :: Parse String [Int]
decPI = do
	(val1:_) <- numBlkMulDivPI
	blankP `seqP` (charP '+') `seqP` blankP 
	(val2:_) <- expPI
	return [val1 - val2]

mulPI :: Parse String [Int]
mulPI = do
	(val1:_) <- numBlkPI 
	blankP `seqP` (charP '*') `seqP` blankP 
	(val2:_) <- numBlkMulDivPI 
	return [val1 * val2]

divPI :: Parse String [Int]
divPI = do
	(val1:_) <- numBlkPI
	blankP `seqP` (charP '/') `seqP` blankP 
	(val2:_) <- numBlkMulDivPI
	return [val1 `div` val2]

expPI = do
	blankP
	(addDecPI `choiceP` numBlkMulDivPI)

main = getArgs >>= putStrLn.show.head.fst.unJust.
	compute.intercalate " "
	where
		unJust (Just a) = a
		compute = runParse expPI

PEGパーサーを書いてみる:後半

前回、シグネチャだけで終わっていたパーサーを実装してみました。

seqP :: Parser i o -> Parser i o -> Parser i o
seqP p q = do
  v <- p
  w <- q
  return (v ++ w)

ifP:: Parser i o -> ([o] -> Parser i o) -> Parser i o -> Parser i o
ifP p ps pf = Parser $ \i ->
  case (runParser p i) of
    Nothing -> runParser pf i
    Just (o, i') -> runParser (ps o) i'

orP :: Parser i o -> Parser i o -> Parser i o
orP p q = ifP p return q

emptyP :: Parser i o
emptyP = return 

manyP :: Parser i o -> Parser i o
manyP p = ifP p (recurse) emptyP
  where
    recurse o = manyP p >>= return .(o++)

optP :: Parser i o -> Parser i o
optP p = ifP p return emptyP

followedByP :: Parser i o -> Parser i o
followedByP pf = Parser $ \i -> (runParser pf i) >> return (, i)

notFollowedByP :: Parser i o -> Parser i o
notFollowedP nf = Parser $ \i -> ifNothing (runParser nf i) ([], i)
  where
    ifNothing Nothing v = Just v
    ifNothing (Just _) _ = Nothing

といった感じで、それぞれの振る舞いを記述するのにseqPのようにモナドインターフェースの上のレベルで書くこともできれば、followedByPのようにモナドの下のレベルで書くことも簡単できて、うまいことできてるなーという感じですね…

ちなみにifPという補助コンビネータが結構便利でした。

ちょっと簡単ですが、今日はこの辺で。

ではでは。

PEGパーサーを書いてみる

PEGで文法を表現してみて、ルールのそれぞれの使い方動きにもなじみが出てきました。

ここで、PEGパーサーをちょっと書いてみたいと思います。PappyやFrisbyのようにO(N)というわけに行くかどうかはわかりませんが…

方針としては、Parsecのまねということで、モナドのインターフェースにあわせて、ステートモナドのパターンを踏襲していきたいと思います。

次に、モナドパーサーの入出力についてみてみたいと思います。まず、パーサーとして、成功のときはパースの結果と、残りの入力文字列を返す必要があります。さらに、PEGパーサーのルールの中では空文字:()のように成功しても何も出力しないものがあります。つまり、出力型は「空」というコンセプトが表現できなくてはいけませんね。それ以外にも順序:(e1,e2,e3,e4)のように複数のパーサーを順次マッチさせた結果を返すときは各パーサーの要素をうまくつなぎ合わせることができると話が単純になりそうです。ということで、ちょっと無駄なような気もするのですが、出力は常にリストになるようにするとして

data Parser i o = Parser { runParser :: \i -> Maybe ([o], i)}

ということにして見ます。パース失敗のときはNothing、成功の場合はJust([o], i)といった感じで…その辺を指定するべくモナドインスタンス宣言を書いて見ます:

instance Monad (Parser i) where
  m >>= f = Parser $ \i ->
    case (runParser m i) of
      Nothing -> Nothing
      Just (o, i') -> runParse (f o) i'
  return o = Parse \i -> Just (o, i)

PEGのルールを見てみると、いくつかの種類に分けることができることに気づきます。
まずはパーサーの核というべき単純なパーサーです:

emptyP :: Parser i o
emptyP = Parser $ \i -> Just ([], i)

itemP :: o -> Parser i o
itemP o = Parser $ \is@(i:iss) -> 
  if (i == 0) then Just ([o], iss)
    else Nothing

ここから先はとりあえずシグネチャだけ押さえていきたいと思います。

そして、それを組み合わせていくコンビネータのうち次の2つを2項演算子として定義します

seqP :: Parser i o -> Parser i o -> Parser i o

orP :: Parser i o -> Parser i o -> Parser i o

そして、単項演算子

manyP :: Parser i o -> Parser i o

many1P p = p `seqP` (manyP p)

optP :: Parser i o -> Parser i o

最後に、迷ったのがfollowedByPとnotFollowedByPです。これは2項でも単項でもかけるんですが、followedByという意味からすると2項で行く方法もありだと思うのですが、単項でやって見ましたということで:

followedByP :: Parser i o -> Parser i o
notFollowedByP :: Parser i o -> Parser i o

この2つを単項演算子で定義することのメリットは単体で使えることなんですが、まぁ、そんなに重要なことではないかもしれません。さらに、2項で定義したとしても、emptyPを組み合わせれば簡単に単項バージョンと同じように使えます。

これが完成すれば、前回の四則演算のPEGを使って電卓が作れます…
ではでは。

PEGで四則演算を表現してみる

前回PEGで整数をパースするパターンをやってみました:

NUMBER = (ZERO/NONZERO)
NONZERO = (DIGIT, DIGIT0*)
ZERO = 0!(DIGIT0)
DIGIT = (1/2/3/4/5/6/7/8/9)
DIGIT0 = (0/DIGIT)

これに加減乗除を追加して四則演算をパースするPEGを書いてみます。

まずは整数の表記がマイナス値も表現できるように単項演算氏のマイナスを取り扱えるようにします。マイナス記号はあっても無くてもよいので省略可能にします:

NUMBER=(ZERO/-?NONZERO)

こうすることで、0は許されても-0はエラーになるようになってます。

次に、足し算をやって見ましょう:

ADDITION=(NUMBER,+,NUMBER)

これで

1+1
1231+2343
1231+-2

はパース可能ですが、空白が途中にある文字列はパースできません。それを直すためにOPTBLANKを導入しましょう

ADDITION=(NUMBER, OPTBLANK, +, OPTBLANK, NUMBER)
OPTBLANK=(' '/'\t')*

こうすることで、数と演算子の間の任意個のタブ、空白文字をスキップできるようになります。
次に1+1+1などのような2項以上の式を取り扱えるようにします。PEGでは左再帰が必ず無限ループになってしまうので次のようにします:

ADDITION=(NUMBER,OPTBLANK,+,OPTBLANK,VALUE)
VALUE=(ADDITION\NUMBER)

こうすることで+はleft associative(左結合)になりますね。

これを拡張して次のようにやるとどうなるでしょう?

COMPUTE=(NUMBER,OPTBLANK,(+/-/*/\//),OPTBLANK,VALUE)
VALUE=(COMPUTE\NUMBER)

これだと加減と乗除の間の優先順位の違いがうまく取り扱えません。なので、加減と乗除を行うPEGを分離します。加減のほうが乗除よりも優先順位は低いので:

ADDDEC=(MULDIVORNUM,OPTBLANK,(+/-),OPTBLANK,ADDDEC)
MULDIV=(NUMBER,OPTBLANK,(*/\//),OPTBLANK,MULDIV)
MULDIVORNUM=(MULDIV/NUMBER)

FORMULA=(ADDDEC/MULDIVORNUM)

とやってやれば、
1*2+3+4+5/2/1*3
なんていうのも
(1*2)+3+4+((5/2)/1)*3
という解釈をしてくれます。

最後におまけでカッコを使えるようにして出来上がったのがこれです:

OPTBLANK=(' '/'\t')*

DIGIT = (1/2/3/4/5/6/7/8/9)
DIGIT0 = (0/DIGIT)

NUMBER=(ZERO/-?NONZERO)

ZERO = 0!(DIGIT0)
NONZERO = (DIGIT, DIGIT0*)

BLOCK=(\(,OPTBLANK,FORMULA,OPTBLANK,\),OPTBLANK,)
BLOCKORNUM=(NUMBER/BLOCK)

ADDDEC=(MULDIVORNUM,OPTBLANK,(+/-),OPTBLANK,ADDDEC)
MULDIV=(BLOCKORNUM,OPTBLANK,(*/\//),OPTBLANK,MULDIV)
MULDIVORNUM=(MULDIV/BLOCKORNUM)

FORMULA=(OPTBLANK,(ADDDEC/MULDIVORNUM))

PEGは高々10個程度の文法要素でわりと簡単に、しかも結構簡潔に文法の記述ができますね。

今回参考にしているPackrat Parsing: a Practical Linear-Time Algorithm with Backtrackingという論文ではPappyというパーサージェネレーターライブラリについて解説しているのですが、Haskell上ではこの次の世代に当たるFrisbyというライブラリも開発されているようです。

FrisbyはPappyとは違って、Parsecに近いパーサーコンビネータ・ライブラリなようです。それでいて、Pappyと同じO(N)ドメインでパースをすることができるようです。僕はまだPEGの勉強をしているだけで、どちらも使ってみてはいないのですが…

今日はこの辺で。
ではでは。