從字元串到能用的數據結構到底有多遠?——Haskell的Parsec實戰
首先,為什麼要用Parsec解析文本而不是正則表達式?在其它語言中,將內容分割成數組,用正則表達式來解析內容是普遍存在的。在Haskell中也可以沿著這一條路線走下去。但Parsec是一個更好的方式。看了Parsec之後我就用Parsec解析JSON文本(作為學習),從字元串到JSON類型沒有想像中的那麼遠,以後再也不用害怕字元串了。
Parsec簡介
依賴包: - parsec
必要的引入: import qualified Text.Parsec as P
定義:type P.Parsec s u = P.ParsecT s u Identity :: * -> *
Parsec是一個ParsecT一個簡寫,ParsecT主要是以下這些class的示例
instance [safe] A.Alternative (P.ParsecT s u m) -- Defined in 『Text.Parsec.Prim』instance [safe] Applicative (P.ParsecT s u m) -- Defined in 『Text.Parsec.Prim』instance [safe] Functor (P.ParsecT s u m) -- Defined in 『Text.Parsec.Prim』instance [safe] Monad (P.ParsecT s u m) -- Defined in 『Text.Parsec.Prim』instance [safe] (Monoid a, Data.Semigroup.Semigroup (P.ParsecT s u m a)) => Monoid (P.ParsecT s u m a)
以下只列出關鍵點
Alternative : 我們可以使用<|>來表達『邏輯或』的關係。Applicative: 可以使用pure,<*> ,<*,*> 可以使用函數,可以更改容器內的值。Functor: fmap 一般我用<$>替代它,<$ 更改容器內的值
Monad: do 語句塊當然上面列的不全,ParsecT也是一個MonadTrans ,可以嵌入Monad(如最常用的IO)用lift升格。Parsec s u a : s是源(也就是要解析的文本)類型,u是用戶狀態類型,a是結果
Parsec一些關鍵的函數(常用的函數)
P.parse 解析入口函數P.parse :: P.Stream s Identity t => P.Parsec s () a -> P.SourceName -> s -> Either P.ParseError a
parse Parsec 源文件名字(解析失敗時用來定位文件) 待解析文本 -> Either
P.char、P.oneOf、P.digit、P.string、P.noneOf 、P.anyChar等都會返回Parsec用來解析 一個字元、多個字元之一、數字、字元串、非多個字元之一、任意字元等。P.many Parsec 解析零個或多個Parsec直到解析失敗
P.many1 Parsec 至少解析一個或者多個P.skipMany、P.skipMany1 與上面many、many1一樣只不過忽略返回結果P.sepBy a b 用parsec b來分割parsec a(如解析1,12,34,4以固定字元隔開的token)P.lookAhead Parsec 主要作用是不產生消耗還會給你結果P.try 由於 a<|>b只對第一個字元做判斷如果第一個字元成功了就返回a而不管整體失敗與否,try是為了整體失敗走bP.eof 表示文件結尾的Parsec
了解了上面這些函數,類型類就可以完成解析JSON字元串的任務了。
JSON解析
解析之前,一般我會先定義適用於JSON的模型
data Number = Int Int | Float Float deriving Showdata JSON = Null | Number Number | String String | Bool Bool | Undefined | Object [(String ,JSON)] | List [JSON] deriving (Show)
寫好『骨架』
parse :: String -> Either P.ParseError JSONparse text = P.parse jsonParsec "JSON:" textjsonParsec :: P.Parsec String () JSONjsonParsec = P.spaces *> myParsec <* P.spaces <* P.eof
parse使用jsonParsec解析text, P.spaces表示空白字元,前後可有任意空白字元最終返回myParsec(*>、<* 函數來自於Applicative)
解析Null,和undefined
myParsec = nullParsec <|> undefinedParsecundefinedParsec = Undefined <$ P.string "undefined"nullParsec = P.string "null" >> return Null
myParsec: <|>函數來自Alternative,如果NullParsec解析失解析undefinedParsec
undefinedParsec : <$來自Functor ,如果解析成功內部的值改為UndefinednullParsec : >> 來自Monad , 如果解析成功內部值改為Null。和undefinedParsec的功能相同。
現在可以運行parse函數,輸入null,undefined能得到正確的結果。
*Main Lib A> parse "null"Right Null*Main Lib A> parse "undefined"Right Undefined*Main Lib A>
解析String和Bool
js的字元串分為兩種寫法(es6以下),單引號,雙引號,需要寫兩個Parsec。
寫完Parsec之後在myParsec中加入進來
myParsec = nullParsec <|> stringParsec <|> stringParsec1 <|> boolParsec <|> undefinedParsecboolParsec = (Bool True <$ P.string "true") <|> (Bool False <$ P.string "false")stringParsec = do P.oneOf """ x <- P.many $ P.noneOf """ P.oneOf """ return $ String xstringParsec1 = do P.oneOf "" x <- P.many $ P.noneOf "" P.oneOf "" return $ String x
boolParsec中所涵蓋的內容前面已介紹過了,這裡不在介紹
stringParsec解析雙引號的字元串: 消耗雙引號->消耗非雙引號並把結果給x->消耗雙引號->返回。裡面的P.oneOf """ 也可以換為 P.char "。stringParsec1於stringParsec雷同。運行parse函數
*Main Lib A> parse "hello"Right (String "hello")*Main Lib A> parse ""diqye""Right (String "diqye")*Main Lib A> parse "abc"Left "JSON:" (line 1, column 1):unexpected "a"
解析Array和Object
原本以為這塊會很困難,沒想到很自然而然的寫出來了。
listParsec = do P.char [ P.spaces a <- P.sepBy myParsec (P.try symbol1) P.spaces P.char ] return $ List asymbol1 = do P.spaces P.char , P.spaceskeyParsec :: P.Parsec String () StringkeyParsec = do c <- P.lookAhead P.anyChar let val | C.isDigit c = fail "非法的key" | otherwise = P.many1 $ P.noneOf ": "objectInnerParsec = do (String key) <- stringParsec <|> stringParsec1 <|> (pure String <*> keyParsec) P.<?> "符合規定的key" P.spaces P.char : P.spaces val <- myParsec return (key,val)objectParsec = do P.char { P.spaces a <- P.sepBy objectInnerParsec (P.try symbol1) P.spaces P.char } return $ Object a
symbol1 只解析了一個逗號,只不過前後都忽略了空白字元, try symbol1是為了整體失敗之後不在做消耗(主要是空白字元),參見簡介處的介紹。
listParsec: 以[開頭]結尾的字元,通過sepBy解析逗號分割開來的項,每一項使用myParsec來解析(遞歸解析)。 別忘了在myParsec中加入 listParsec、objectParsecobjectParsec: 以{開頭}結尾,中間部分通過objectInnerParsec解析key和val,key可以是一個字元串也可以是普通的key。這裡的keyParsec只是簡單的解析為不能以數字開頭。lookAhead不消耗字元,使報錯可以精準定位到某行某列。解析數字數字比較麻煩,分為整數,浮點數,負整數,負浮點數。
negdigit = pure (:) <*> p.char - <*> posdigitposdigit = p.many1 p.digitnegfloat = pure (:) <*> p.char - <*> posfloatposfloat = do digits <- p.many1 p.digit dot <- p.char . rdigits <- p.many1 p.digit return $ digits ++ (dot:rdigits)digitparsec = number . int . (read :: string -> int) <$> (posdigit <|> negdigit)floatparsec = number . float . (read :: string -> float) <$> (posfloat <|> negfloat)
有了Parsec這些也不再困難咯。
完整的myParsec
myParsec = nullParsec <|> stringParsec <|> stringParsec1 <|> listParsec <|> objectParsec <|> boolParsec <|> undefinedParsec <|> floatParsec <|> digitParsec
repl中的測驗
*Main Lib A> parse "[abc,{name:diqye,age:10},-10,1.01]"Right (List [String "abc",Object [("name",String "diqye"),("age",Number (Int 10))],Number (Int (-10)),Number (Float 1.01)])*Main Lib A> parse "[abc,{name:diqye,age:10},-10,1.01] i"Left "JSON:" (line 1, column 40):unexpected iexpecting space or end of input
以上代碼以上傳至 ppzzppz/json-demo
這個JSON作為學習來說沒毛病,作為使用來說,還有很多不足,一些特殊情況沒有做處理。代碼上可能有一些更好改進,歡迎指正和建議。
推薦閱讀:
※Haskell 怎麼推導函數類型呢?
※haskell 能像python """ """ 那樣聲明字元串嗎?
※函數式編程如何模擬有限狀態機?
TAG:Haskell |