Haskellにもscanfを
Haskellでも、scanfが欲しいぜという意見をどっかで見た。まあ確かに、"2007/01/18"とかを、(2007,01,18)に分解するときにParsecを使うとかは、オーバーキルな感が。いや、そういうときは、lex使うんだよ!Preludeにはいってる。
Prelude> lex "2004/03/12" [("2004","/03/12")] Prelude> lex "/03/12" [("/","03/12")]
これ凄い使いづらいんだけど、何でこんな意味不明にリストにしてるんだろう。さて、というわけで、lexでいいんですが、scanfを書こう。まあ、scanfの挙動を完全に再現するのは大変なので、そのへんは臨機応変にscanfっぽいものを
module Scanf(sscanf) where import Text.ParserCombinators.Parsec import Data.Maybe import Text.Printf import Text.Read.Lex --defined in Text.Printf,but not exported data UPrintf = UChar Char | UString String | UInt Int | UInteger Integer | UFloat Float | UDouble Double deriving Show --R:raw text data PrintfFmt = C | D | O | X | U | F | G | E | S | R String deriving Show parseFmt :: String -> IO [PrintfFmt] parseFmt fmtString = case (parse p "" fmtString) of Left err -> fail "illegal format" Right x -> return x where p = many (p1 <|> p2) p1 = do{char '%'; x <- oneOf "cdoxufges"; return (charToFmt x)} p2 = do{s<-many1 (satisfy$not.('%'==)); return (R s)} charToFmt c = fromJust$lookup c fmtTable fmtTable = [('c',C),('d',D),('o',O),('x',X),('u',U),('f',F),('g',G),('e',E),('s',S)] scanfParser :: [PrintfFmt] -> Parser [UPrintf] scanfParser [] = return [] scanfParser (R s:xs) = do{ s <- string s; rest <- (scanfParser xs); return rest} scanfParser (C:xs) = do{ c <- letter; rest <- (scanfParser xs); return $(UChar c):rest } scanfParser (D:xs) = do{ n <- many1 digit; rest <- scanfParser xs; return (UInt ((read n)::Int):rest)} scanfParser (S:xs) = do{ s <- many1 letter; rest<- scanfParser xs; return (UString s:rest)} sscanf :: String -> String -> IO [UPrintf] sscanf input fmtString = (parseFmt fmtString)>>=(\fmt -> case (parse (scanfParser fmt) "" input) of Left err -> fail "illegal input of sscanf\n" Right x -> return x)
こんな感じでの使用を想定
*Scanf> do{[a,b,c] <- sscanf "2005/12/02" "%d/%d/%d" ;print (a,b,c)} (UInt 2005,UInt 12,UInt 2)
まあ、template Haskell使わないと、これが限度で、[UPrintf]ってのも値取り出すのめんどくさいよなー。
既知の問題点
・"%%"が使えない
・%dはIntegerとして読み込まれる(のはむしろ利点な気もするが)
・%sが"letter"にしかマッチしない
・scanset(%[...]みたいなの)が使えない
・c,d,s以外のフォーマットを無視
・フォーマット中の空白は、任意の長さにマッチすべきだけど、そうなってない
・%2dとか%3sみたいなのが使えない
・その他scanfと挙動が違うとこがあるかも
まあ、大体は、ちょっとコード追加するだけで対応できるような。で、これは返り値が[UPrint]なのがださい。そのへんを解決するためにtemplate Haskellでscanfを書く
genScanfParser :: [PrintfFmt] -> ExpQ genScanfParser ls = do{varlist<-mkVarlist ls; return $ DoE $ (mkBinds varlist)++ [NoBindS (AppE (VarE (mkName "return")) (TupE (mkRets varlist)))]} where mkVarlist [] = return [] mkVarlist (x:xs) = do{rest <- mkVarlist xs; case x of (R _)-> return (Nothing:rest) otherwise->do{n<-newName "c";return ((Just n):rest)}} parserList = map toParser ls mkBinds vlist = map mkBind (zip vlist parserList) mkBind (Just var , parser) = (BindS (VarP var) parser) mkBind (Nothing , parser) = (NoBindS parser) mkRets vlist = [mkRetVal(c,var)| (c,Just var) <- (zip ls vlist)] mkRetVal (c,var) = case c of C -> (VarE var) D -> SigE (AppE (VarE (mkName "read")) (VarE var)) (ConT (mkName "Integer")) toParser c = case c of C -> (VarE (mkName "Scanf.scanfParserC")) D -> (VarE (mkName "Scanf.scanfParserD")) S -> (VarE (mkName "Scanf.scanfParserS")) (R s) -> (AppE (VarE (mkName "Scanf.scanfParserR")) (stringToExpr s)) where stringToExpr s = ListE [LitE (CharL c)|c<-s] scanfParserC = do{c<-letter;return c;} scanfParserD = do{c<-many1 digit;return c;} scanfParserS = do{s<-many1 letter;return s} scanfParserR str = do{s<-string str;return s} scanf fmtString = [|do{ln<-getLine; case (parse $p "" ln) of Left err -> fail "scanf parser error:bad input" Right x -> return x} |] where p= join $ runIO $ (parseFmt fmtString)>>=(return.genScanfParser)
使い方は、
{-# OPTIONS -fth #-} module Main where import Scanf main = do{(x,y) <- $(scanf "%d/%d");print x}
基本的には、パーサをコンパイル時に生成しているだけなので、何をやってるかは"runQ(scanf "%d/%d")>>=putStrLn.pprint"とかやれば、すぐ分かるような。と言いつつ三ヵ月後の自分が読めない気がしなくもない。とりあえず、trivialなケース以外では、[| ..|]とか使わず、地味に、構文木構築していく方がいいと思った。というか、それでえらい罠にはまった。newNameの返す型がQ Nameなので非常にうっとうしい(やむをえないことだが)。あと、VarEとか型構成子の"monadic"版varEとかいらん気も
それから、HaskellのprintfはTemplate Haskell使ってるのかと思ってたら、そうではなかった。なんか型クラスを非常にうまく使ってるっぽい(多分)。これは一度読むべき(いや、読んでも役に立たないと思うが)
Template Haskell使わずにprintfを書くッ!
Lennartッ!おまえの命がけの行動ッ! ぼくは敬意を表するッ!