標籤:

從字元串到能用的數據結構到底有多遠?——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是為了整體失敗走b

P.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 ,如果解析成功內部的值改為Undefined

nullParsec : >> 來自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、objectParsec

objectParsec: 以{開頭}結尾,中間部分通過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 |